Philipp Hörist pushed to branch master at gajim / python-nbxmpp
Commits: 8f08104b by Philipp Hörist at 2019-02-25T21:54:02Z Fix xsend example
Fixes #71
- - - - - 7d1b7f39 by Philipp Hörist at 2019-02-26T21:50:23Z Add Muclumbus API module
- - - - -
6 changed files:
- doc/examples/xsend.py - nbxmpp/const.py - nbxmpp/dispatcher_nb.py - + nbxmpp/modules/muclumbus.py - nbxmpp/protocol.py - nbxmpp/structs.py
Changes:
===================================== doc/examples/xsend.py ===================================== @@ -2,18 +2,20 @@
import sys import os -import nbxmpp -import time import logging -try: - from gi.repository import GObject as gobject -except Exception: - import gobject + +import nbxmpp +from nbxmpp.const import Realm +from nbxmpp.const import Event +from gi.repository import GLib + +if sys.platform in ('win32', 'darwin'): + import certifi
consoleloghandler = logging.StreamHandler() -root_log = logging.getLogger('nbxmpp') -#root_log.setLevel('DEBUG') -root_log.addHandler(consoleloghandler) +log = logging.getLogger('nbxmpp') +log.setLevel('INFO') +log.addHandler(consoleloghandler)
if len(sys.argv) < 2: print("Syntax: xsend JID text") @@ -34,42 +36,93 @@ for mandatory in ['jid', 'password']: print('Please point ~/.xsend config file to valid JID for sending messages.') sys.exit(0)
+ class Connection: def __init__(self): self.jid = nbxmpp.protocol.JID(jidparams['jid']) self.password = jidparams['password'] - self.sm = nbxmpp.Smacks(self) # Stream Management self.client_cert = None + self.idle_queue = nbxmpp.idlequeue.get_idlequeue() + self.client = None
- def on_auth(self, con, auth): - if not auth: - print('could not authenticate!') - sys.exit() - print('authenticated using ' + auth) - self.send_message(to_jid, text) + def _on_auth_successful(self): + print('authenticated') + self.client.bind()
- def on_connected(self, con, con_type): + def _on_connection_active(self): + print('Connection active') + self.client.RegisterHandler('message', self._on_message) + self.send_presence() + + def _on_auth_failed(self, reason, text): + log.debug("Couldn't authenticate") + log.error(reason, text) + exit() + + def _on_message(self, con, stanza): + print('message received') + print(stanza.getBody()) + + def _on_connected(self, con, con_type): print('connected with ' + con_type) - auth = self.client.auth(self.jid.getNode(), self.password, resource=self.jid.getResource(), sasl=1, on_auth=self.on_auth) + self.client.auth(self.jid.getNode(), + get_password=self._get_password, + resource=self.jid.getResource())
- def get_password(self, cb, mech): - cb(self.password) + def _get_password(self, mech, password_cb): + password_cb(self.password)
- def on_connection_failed(self): + def _on_connection_failed(self): print('could not connect!')
def _event_dispatcher(self, realm, event, data): - pass + if realm == Realm.CONNECTING: + if event == Event.AUTH_SUCCESSFUL: + log.info(event) + self._on_auth_successful() + + elif event == Event.AUTH_FAILED: + log.error(event) + log.error(data) + self._on_auth_failed(*data) + + elif event == Event.SESSION_FAILED: + log.error(event) + + elif event == Event.BIND_FAILED: + log.error(event) + + elif event == Event.CONNECTION_ACTIVE: + log.info(event) + self._on_connection_active() + return
def connect(self): - idle_queue = nbxmpp.idlequeue.get_idlequeue() - self.client = nbxmpp.NonBlockingClient(self.jid.getDomain(), idle_queue, caller=self) - self.con = self.client.connect(self.on_connected, self.on_connection_failed, secure_tuple=('tls', '', '', None, None)) + cacerts = '' + if sys.platform in ('win32', 'darwin'): + cacerts = certifi.where() + + self.client = nbxmpp.NonBlockingClient(self.jid.getDomain(), + self.idle_queue, + caller=self) + + self.client.connect(self._on_connected, + self._on_connection_failed, + secure_tuple=('tls', cacerts, '', None, None, False)) + + if sys.platform == 'win32': + timeout, in_seconds = 20, None + else: + timeout, in_seconds = 100, False
- def send_message(self, to_jid, text): - id_ = self.client.send(nbxmpp.protocol.Message(to_jid, text, typ='chat')) - print('sent message with id ' + id_) - gobject.timeout_add(1000, self.quit) + if in_seconds: + GLib.timeout_add_seconds(timeout, self.process_connections) + else: + GLib.timeout_add(timeout, self.process_connections) + + def send_presence(self): + presence = nbxmpp.Presence() + self.client.send(presence)
def quit(self): self.disconnect() @@ -78,8 +131,28 @@ class Connection: def disconnect(self): self.client.start_disconnect()
+ def process_connections(self): + try: + self.idle_queue.process() + except Exception: + # Otherwise, an exception will stop our loop + + if sys.platform == 'win32': + # On Windows process() calls select.select(), so we need this + # executed as often as possible. + timeout, in_seconds = 1, None + else: + timeout, in_seconds = 100, False + + if in_seconds: + GLib.timeout_add_seconds(timeout, self.process_connections) + else: + GLib.timeout_add(timeout, self.process_connections) + raise + return True # renew timeout (loop for ever) +
con = Connection() con.connect() -ml = gobject.MainLoop() +ml = GLib.MainLoop() ml.run()
===================================== nbxmpp/const.py ===================================== @@ -290,6 +290,12 @@ class BookmarkStoreType(Enum): PRIVATE = 'private'
+class AnonymityMode(Enum): + UNKNOWN = None + SEMI = 'semi' + NONE = 'none' + + MOODS = [ 'afraid', 'amazed',
===================================== nbxmpp/dispatcher_nb.py ===================================== @@ -66,6 +66,7 @@ from nbxmpp.modules.bookmarks import Bookmarks from nbxmpp.modules.openpgp import OpenPGP from nbxmpp.modules.omemo import OMEMO from nbxmpp.modules.annotations import Annotations +from nbxmpp.modules.muclumbus import Muclumbus from nbxmpp.modules.misc import unwrap_carbon from nbxmpp.modules.misc import unwrap_mam from nbxmpp.util import get_properties_struct @@ -199,6 +200,7 @@ class XMPPDispatcher(PlugIn): self._modules['OpenPGP'] = OpenPGP(self._owner) self._modules['OMEMO'] = OMEMO(self._owner) self._modules['Annotations'] = Annotations(self._owner) + self._modules['Muclumbus'] = Muclumbus(self._owner)
for instance in self._modules.values(): for handler in instance.handlers:
===================================== nbxmpp/modules/muclumbus.py ===================================== @@ -0,0 +1,131 @@ +# Copyright (C) 2019 Philipp Hörist <philipp AT hoerist.com> +# +# This file is part of nbxmpp. +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 3 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; If not, see http://www.gnu.org/licenses/. + +import logging + +from nbxmpp.protocol import NS_MUCLUMBUS +from nbxmpp.protocol import NS_DATA +from nbxmpp.protocol import NS_RSM +from nbxmpp.protocol import Node +from nbxmpp.protocol import Iq +from nbxmpp.protocol import isResultNode +from nbxmpp.structs import MuclumbusItem +from nbxmpp.structs import MuclumbusResult +from nbxmpp.const import AnonymityMode +from nbxmpp.modules.dataforms import extend_form +from nbxmpp.util import call_on_response +from nbxmpp.util import callback +from nbxmpp.util import raise_error + + +log = logging.getLogger('nbxmpp.m.muclumbus') + +# API Documentation +# https://search.jabber.network/docs/api + +class Muclumbus: + def __init__(self, client): + self._client = client + self.handlers = [] + + @call_on_response('_parameters_received') + def request_parameters(self, jid): + query = Iq(to=jid, typ='get') + query.addChild(node=Node('search', attrs={'xmlns': NS_MUCLUMBUS})) + return query + + @callback + def _parameters_received(self, stanza): + if not isResultNode(stanza): + return raise_error(log.info, stanza) + + search = stanza.getTag('search', namespace=NS_MUCLUMBUS) + if search is None: + return raise_error(log.warning, stanza, 'stanza-malformed') + + dataform = search.getTag('x', namespace=NS_DATA) + if dataform is None: + return raise_error(log.warning, stanza, 'stanza-malformed') + + log.info('Muclumbus parameters received') + return extend_form(node=dataform) + + @call_on_response('_search_received') + def set_search(self, jid, dataform, items_per_page=50, after=None): + search = Node('search', attrs={'xmlns': NS_MUCLUMBUS}) + search.addChild(node=dataform) + rsm = search.addChild('set', namespace=NS_RSM) + rsm.addChild('max').setData(items_per_page) + if after is not None: + rsm.addChild('after').setData(after) + query = Iq(to=jid, typ='get') + query.addChild(node=search) + return query + + @callback + def _search_received(self, stanza): + if not isResultNode(stanza): + return raise_error(log.info, stanza) + + result = stanza.getTag('result', namespace=NS_MUCLUMBUS) + if result is None: + return raise_error(log.warning, stanza, 'stanza-malformed') + + items = result.getTags('item') + if not items: + return MuclumbusResult(first=None, + last=None, + max=None, + end=True, + items=[]) + + set_ = result.getTag('set', namespace=NS_RSM) + if set_ is None: + return raise_error(log.warning, stanza, 'stanza-malformed') + + first = set_.getTagData('first') + last = set_.getTagData('last') + try: + max_ = int(set_.getTagData('max')) + except Exception: + return raise_error(log.warning, stanza, 'stanza-malformed') + + results = [] + for item in items: + jid = item.getAttr('address') + name = item.getTagData('name') + nusers = item.getTagData('nusers') + description = item.getTagData('description') + language = item.getTagData('language') + is_open = item.getTag('is-open') is not None + + try: + anonymity_mode = AnonymityMode(item.getTagData('anonymity-mode')) + except ValueError: + anonymity_mode = AnonymityMode.UNKNOWN + results.append(MuclumbusItem(jid=jid, + name=name, + nusers=nusers, + description=description, + language=language, + is_open=is_open, + anonymity_mode=anonymity_mode)) + return MuclumbusResult(first=first, + last=last, + max=max_, + end=len(items) < max_, + items=results)
===================================== nbxmpp/protocol.py ===================================== @@ -196,6 +196,7 @@ NS_OMEMO_TEMP_BUNDLE = 'eu.siacs.conversations.axolotl.bundles' NS_BOOKMARK_CONVERSION = 'urn:xmpp:bookmarks-conversion:0' NS_DOMAIN_BASED_NAME = 'urn:xmpp:domain-based-name:1' NS_HINTS = 'urn:xmpp:hints' +NS_MUCLUMBUS = 'https://xmlns.zombofant.net/muclumbus/search/1.0'
#xmpp_stream_error_conditions = ''' #bad-format -- -- -- The entity has sent XML that cannot be processed.
===================================== nbxmpp/structs.py ===================================== @@ -100,6 +100,10 @@ AnnotationNote.__new__.__defaults__ = (None, None)
EMEData = namedtuple('EMEData', 'name namespace')
+MuclumbusResult = namedtuple('MuclumbusResult', 'first last max end items') + +MuclumbusItem = namedtuple('MuclumbusItem', 'jid name nusers description language is_open anonymity_mode') +
class OMEMOBundle(namedtuple('OMEMOBundle', 'spk spk_signature ik otpks')): def pick_prekey(self):
View it on GitLab: https://dev.gajim.org/gajim/python-nbxmpp/compare/f37f2c9dfdbcb6f6fd5f9bd00c...