Philipp Hörist pushed to branch master at gajim / python-nbxmpp

Commits:

6 changed files:

Changes:

  • doc/examples/xsend.py
    ... ... @@ -2,18 +2,20 @@
    2 2
     
    
    3 3
     import sys
    
    4 4
     import os
    
    5
    -import nbxmpp
    
    6
    -import time
    
    7 5
     import logging
    
    8
    -try:
    
    9
    -    from gi.repository import GObject as gobject
    
    10
    -except Exception:
    
    11
    -    import gobject
    
    6
    +
    
    7
    +import nbxmpp
    
    8
    +from nbxmpp.const import Realm
    
    9
    +from nbxmpp.const import Event
    
    10
    +from gi.repository import GLib
    
    11
    +
    
    12
    +if sys.platform in ('win32', 'darwin'):
    
    13
    +    import certifi
    
    12 14
     
    
    13 15
     consoleloghandler = logging.StreamHandler()
    
    14
    -root_log = logging.getLogger('nbxmpp')
    
    15
    -#root_log.setLevel('DEBUG')
    
    16
    -root_log.addHandler(consoleloghandler)
    
    16
    +log = logging.getLogger('nbxmpp')
    
    17
    +log.setLevel('INFO')
    
    18
    +log.addHandler(consoleloghandler)
    
    17 19
     
    
    18 20
     if len(sys.argv) < 2:
    
    19 21
         print("Syntax: xsend JID text")
    
    ... ... @@ -34,42 +36,93 @@ for mandatory in ['jid', 'password']:
    34 36
             print('Please point ~/.xsend config file to valid JID for sending messages.')
    
    35 37
             sys.exit(0)
    
    36 38
     
    
    39
    +
    
    37 40
     class Connection:
    
    38 41
         def __init__(self):
    
    39 42
             self.jid = nbxmpp.protocol.JID(jidparams['jid'])
    
    40 43
             self.password = jidparams['password']
    
    41
    -        self.sm = nbxmpp.Smacks(self) # Stream Management
    
    42 44
             self.client_cert = None
    
    45
    +        self.idle_queue = nbxmpp.idlequeue.get_idlequeue()
    
    46
    +        self.client = None
    
    43 47
     
    
    44
    -    def on_auth(self, con, auth):
    
    45
    -        if not auth:
    
    46
    -            print('could not authenticate!')
    
    47
    -            sys.exit()
    
    48
    -        print('authenticated using ' + auth)
    
    49
    -        self.send_message(to_jid, text)
    
    48
    +    def _on_auth_successful(self):
    
    49
    +        print('authenticated')
    
    50
    +        self.client.bind()
    
    50 51
     
    
    51
    -    def on_connected(self, con, con_type):
    
    52
    +    def _on_connection_active(self):
    
    53
    +        print('Connection active')
    
    54
    +        self.client.RegisterHandler('message', self._on_message)
    
    55
    +        self.send_presence()
    
    56
    +
    
    57
    +    def _on_auth_failed(self, reason, text):
    
    58
    +        log.debug("Couldn't authenticate")
    
    59
    +        log.error(reason, text)
    
    60
    +        exit()
    
    61
    +
    
    62
    +    def _on_message(self, con, stanza):
    
    63
    +        print('message received')
    
    64
    +        print(stanza.getBody())
    
    65
    +
    
    66
    +    def _on_connected(self, con, con_type):
    
    52 67
             print('connected with ' + con_type)
    
    53
    -        auth = self.client.auth(self.jid.getNode(), self.password, resource=self.jid.getResource(), sasl=1, on_auth=self.on_auth)
    
    68
    +        self.client.auth(self.jid.getNode(),
    
    69
    +                         get_password=self._get_password,
    
    70
    +                         resource=self.jid.getResource())
    
    54 71
     
    
    55
    -    def get_password(self, cb, mech):
    
    56
    -        cb(self.password)
    
    72
    +    def _get_password(self, mech, password_cb):
    
    73
    +        password_cb(self.password)
    
    57 74
     
    
    58
    -    def on_connection_failed(self):
    
    75
    +    def _on_connection_failed(self):
    
    59 76
             print('could not connect!')
    
    60 77
     
    
    61 78
         def _event_dispatcher(self, realm, event, data):
    
    62
    -        pass
    
    79
    +        if realm == Realm.CONNECTING:
    
    80
    +            if event == Event.AUTH_SUCCESSFUL:
    
    81
    +                log.info(event)
    
    82
    +                self._on_auth_successful()
    
    83
    +
    
    84
    +            elif event == Event.AUTH_FAILED:
    
    85
    +                log.error(event)
    
    86
    +                log.error(data)
    
    87
    +                self._on_auth_failed(*data)
    
    88
    +
    
    89
    +            elif event == Event.SESSION_FAILED:
    
    90
    +                log.error(event)
    
    91
    +
    
    92
    +            elif event == Event.BIND_FAILED:
    
    93
    +                log.error(event)
    
    94
    +
    
    95
    +            elif event == Event.CONNECTION_ACTIVE:
    
    96
    +                log.info(event)
    
    97
    +                self._on_connection_active()
    
    98
    +            return
    
    63 99
     
    
    64 100
         def connect(self):
    
    65
    -        idle_queue = nbxmpp.idlequeue.get_idlequeue()
    
    66
    -        self.client = nbxmpp.NonBlockingClient(self.jid.getDomain(), idle_queue, caller=self)
    
    67
    -        self.con = self.client.connect(self.on_connected, self.on_connection_failed, secure_tuple=('tls', '', '', None, None))
    
    101
    +        cacerts = ''
    
    102
    +        if sys.platform in ('win32', 'darwin'):
    
    103
    +            cacerts = certifi.where()
    
    104
    +
    
    105
    +        self.client = nbxmpp.NonBlockingClient(self.jid.getDomain(),
    
    106
    +                                               self.idle_queue,
    
    107
    +                                               caller=self)
    
    108
    +
    
    109
    +        self.client.connect(self._on_connected,
    
    110
    +                            self._on_connection_failed,
    
    111
    +                            secure_tuple=('tls', cacerts, '', None, None, False))
    
    112
    +
    
    113
    +        if sys.platform == 'win32':
    
    114
    +            timeout, in_seconds = 20, None
    
    115
    +        else:
    
    116
    +            timeout, in_seconds = 100, False
    
    68 117
     
    
    69
    -    def send_message(self, to_jid, text):
    
    70
    -        id_ = self.client.send(nbxmpp.protocol.Message(to_jid, text, typ='chat'))
    
    71
    -        print('sent message with id ' + id_)
    
    72
    -        gobject.timeout_add(1000, self.quit)
    
    118
    +        if in_seconds:
    
    119
    +            GLib.timeout_add_seconds(timeout, self.process_connections)
    
    120
    +        else:
    
    121
    +            GLib.timeout_add(timeout, self.process_connections)
    
    122
    +
    
    123
    +    def send_presence(self):
    
    124
    +        presence = nbxmpp.Presence()
    
    125
    +        self.client.send(presence)
    
    73 126
     
    
    74 127
         def quit(self):
    
    75 128
             self.disconnect()
    
    ... ... @@ -78,8 +131,28 @@ class Connection:
    78 131
         def disconnect(self):
    
    79 132
             self.client.start_disconnect()
    
    80 133
     
    
    134
    +    def process_connections(self):
    
    135
    +        try:
    
    136
    +            self.idle_queue.process()
    
    137
    +        except Exception:
    
    138
    +            # Otherwise, an exception will stop our loop
    
    139
    +
    
    140
    +            if sys.platform == 'win32':
    
    141
    +                # On Windows process() calls select.select(), so we need this
    
    142
    +                # executed as often as possible.
    
    143
    +                timeout, in_seconds = 1, None
    
    144
    +            else:
    
    145
    +                timeout, in_seconds = 100, False
    
    146
    +
    
    147
    +            if in_seconds:
    
    148
    +                GLib.timeout_add_seconds(timeout, self.process_connections)
    
    149
    +            else:
    
    150
    +                GLib.timeout_add(timeout, self.process_connections)
    
    151
    +            raise
    
    152
    +        return True # renew timeout (loop for ever)
    
    153
    +
    
    81 154
     
    
    82 155
     con = Connection()
    
    83 156
     con.connect()
    
    84
    -ml = gobject.MainLoop()
    
    157
    +ml = GLib.MainLoop()
    
    85 158
     ml.run()

  • nbxmpp/const.py
    ... ... @@ -290,6 +290,12 @@ class BookmarkStoreType(Enum):
    290 290
         PRIVATE = 'private'
    
    291 291
     
    
    292 292
     
    
    293
    +class AnonymityMode(Enum):
    
    294
    +    UNKNOWN = None
    
    295
    +    SEMI = 'semi'
    
    296
    +    NONE = 'none'
    
    297
    +
    
    298
    +
    
    293 299
     MOODS = [
    
    294 300
         'afraid',
    
    295 301
         'amazed',
    

  • nbxmpp/dispatcher_nb.py
    ... ... @@ -66,6 +66,7 @@ from nbxmpp.modules.bookmarks import Bookmarks
    66 66
     from nbxmpp.modules.openpgp import OpenPGP
    
    67 67
     from nbxmpp.modules.omemo import OMEMO
    
    68 68
     from nbxmpp.modules.annotations import Annotations
    
    69
    +from nbxmpp.modules.muclumbus import Muclumbus
    
    69 70
     from nbxmpp.modules.misc import unwrap_carbon
    
    70 71
     from nbxmpp.modules.misc import unwrap_mam
    
    71 72
     from nbxmpp.util import get_properties_struct
    
    ... ... @@ -199,6 +200,7 @@ class XMPPDispatcher(PlugIn):
    199 200
             self._modules['OpenPGP'] = OpenPGP(self._owner)
    
    200 201
             self._modules['OMEMO'] = OMEMO(self._owner)
    
    201 202
             self._modules['Annotations'] = Annotations(self._owner)
    
    203
    +        self._modules['Muclumbus'] = Muclumbus(self._owner)
    
    202 204
     
    
    203 205
             for instance in self._modules.values():
    
    204 206
                 for handler in instance.handlers:
    

  • nbxmpp/modules/muclumbus.py
    1
    +# Copyright (C) 2019 Philipp Hörist <philipp AT hoerist.com>
    
    2
    +#
    
    3
    +# This file is part of nbxmpp.
    
    4
    +#
    
    5
    +# This program is free software; you can redistribute it and/or
    
    6
    +# modify it under the terms of the GNU General Public License
    
    7
    +# as published by the Free Software Foundation; either version 3
    
    8
    +# of the License, or (at your option) any later version.
    
    9
    +#
    
    10
    +# This program is distributed in the hope that it will be useful,
    
    11
    +# but WITHOUT ANY WARRANTY; without even the implied warranty of
    
    12
    +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
    
    13
    +# GNU General Public License for more details.
    
    14
    +#
    
    15
    +# You should have received a copy of the GNU General Public License
    
    16
    +# along with this program; If not, see <http://www.gnu.org/licenses/>.
    
    17
    +
    
    18
    +import logging
    
    19
    +
    
    20
    +from nbxmpp.protocol import NS_MUCLUMBUS
    
    21
    +from nbxmpp.protocol import NS_DATA
    
    22
    +from nbxmpp.protocol import NS_RSM
    
    23
    +from nbxmpp.protocol import Node
    
    24
    +from nbxmpp.protocol import Iq
    
    25
    +from nbxmpp.protocol import isResultNode
    
    26
    +from nbxmpp.structs import MuclumbusItem
    
    27
    +from nbxmpp.structs import MuclumbusResult
    
    28
    +from nbxmpp.const import AnonymityMode
    
    29
    +from nbxmpp.modules.dataforms import extend_form
    
    30
    +from nbxmpp.util import call_on_response
    
    31
    +from nbxmpp.util import callback
    
    32
    +from nbxmpp.util import raise_error
    
    33
    +
    
    34
    +
    
    35
    +log = logging.getLogger('nbxmpp.m.muclumbus')
    
    36
    +
    
    37
    +# API Documentation
    
    38
    +# https://search.jabber.network/docs/api
    
    39
    +
    
    40
    +class Muclumbus:
    
    41
    +    def __init__(self, client):
    
    42
    +        self._client = client
    
    43
    +        self.handlers = []
    
    44
    +
    
    45
    +    @call_on_response('_parameters_received')
    
    46
    +    def request_parameters(self, jid):
    
    47
    +        query = Iq(to=jid, typ='get')
    
    48
    +        query.addChild(node=Node('search', attrs={'xmlns': NS_MUCLUMBUS}))
    
    49
    +        return query
    
    50
    +
    
    51
    +    @callback
    
    52
    +    def _parameters_received(self, stanza):
    
    53
    +        if not isResultNode(stanza):
    
    54
    +            return raise_error(log.info, stanza)
    
    55
    +
    
    56
    +        search = stanza.getTag('search', namespace=NS_MUCLUMBUS)
    
    57
    +        if search is None:
    
    58
    +            return raise_error(log.warning, stanza, 'stanza-malformed')
    
    59
    +
    
    60
    +        dataform = search.getTag('x', namespace=NS_DATA)
    
    61
    +        if dataform is None:
    
    62
    +            return raise_error(log.warning, stanza, 'stanza-malformed')
    
    63
    +
    
    64
    +        log.info('Muclumbus parameters received')
    
    65
    +        return extend_form(node=dataform)
    
    66
    +
    
    67
    +    @call_on_response('_search_received')
    
    68
    +    def set_search(self, jid, dataform, items_per_page=50, after=None):
    
    69
    +        search = Node('search', attrs={'xmlns': NS_MUCLUMBUS})
    
    70
    +        search.addChild(node=dataform)
    
    71
    +        rsm = search.addChild('set', namespace=NS_RSM)
    
    72
    +        rsm.addChild('max').setData(items_per_page)
    
    73
    +        if after is not None:
    
    74
    +            rsm.addChild('after').setData(after)
    
    75
    +        query = Iq(to=jid, typ='get')
    
    76
    +        query.addChild(node=search)
    
    77
    +        return query
    
    78
    +
    
    79
    +    @callback
    
    80
    +    def _search_received(self, stanza):
    
    81
    +        if not isResultNode(stanza):
    
    82
    +            return raise_error(log.info, stanza)
    
    83
    +
    
    84
    +        result = stanza.getTag('result', namespace=NS_MUCLUMBUS)
    
    85
    +        if result is None:
    
    86
    +            return raise_error(log.warning, stanza, 'stanza-malformed')
    
    87
    +
    
    88
    +        items = result.getTags('item')
    
    89
    +        if not items:
    
    90
    +            return MuclumbusResult(first=None,
    
    91
    +                                   last=None,
    
    92
    +                                   max=None,
    
    93
    +                                   end=True,
    
    94
    +                                   items=[])
    
    95
    +
    
    96
    +        set_ = result.getTag('set', namespace=NS_RSM)
    
    97
    +        if set_ is None:
    
    98
    +            return raise_error(log.warning, stanza, 'stanza-malformed')
    
    99
    +
    
    100
    +        first = set_.getTagData('first')
    
    101
    +        last = set_.getTagData('last')
    
    102
    +        try:
    
    103
    +            max_ = int(set_.getTagData('max'))
    
    104
    +        except Exception:
    
    105
    +            return raise_error(log.warning, stanza, 'stanza-malformed')
    
    106
    +
    
    107
    +        results = []
    
    108
    +        for item in items:
    
    109
    +            jid = item.getAttr('address')
    
    110
    +            name = item.getTagData('name')
    
    111
    +            nusers = item.getTagData('nusers')
    
    112
    +            description = item.getTagData('description')
    
    113
    +            language = item.getTagData('language')
    
    114
    +            is_open = item.getTag('is-open') is not None
    
    115
    +
    
    116
    +            try:
    
    117
    +                anonymity_mode = AnonymityMode(item.getTagData('anonymity-mode'))
    
    118
    +            except ValueError:
    
    119
    +                anonymity_mode = AnonymityMode.UNKNOWN
    
    120
    +            results.append(MuclumbusItem(jid=jid,
    
    121
    +                                         name=name,
    
    122
    +                                         nusers=nusers,
    
    123
    +                                         description=description,
    
    124
    +                                         language=language,
    
    125
    +                                         is_open=is_open,
    
    126
    +                                         anonymity_mode=anonymity_mode))
    
    127
    +        return MuclumbusResult(first=first,
    
    128
    +                               last=last,
    
    129
    +                               max=max_,
    
    130
    +                               end=len(items) < max_,
    
    131
    +                               items=results)

  • nbxmpp/protocol.py
    ... ... @@ -196,6 +196,7 @@ NS_OMEMO_TEMP_BUNDLE = 'eu.siacs.conversations.axolotl.bundles'
    196 196
     NS_BOOKMARK_CONVERSION = 'urn:xmpp:bookmarks-conversion:0'
    
    197 197
     NS_DOMAIN_BASED_NAME = 'urn:xmpp:domain-based-name:1'
    
    198 198
     NS_HINTS = 'urn:xmpp:hints'
    
    199
    +NS_MUCLUMBUS = 'https://xmlns.zombofant.net/muclumbus/search/1.0'
    
    199 200
     
    
    200 201
     #xmpp_stream_error_conditions = '''
    
    201 202
     #bad-format --  --  -- The entity has sent XML that cannot be processed.
    

  • nbxmpp/structs.py
    ... ... @@ -100,6 +100,10 @@ AnnotationNote.__new__.__defaults__ = (None, None)
    100 100
     
    
    101 101
     EMEData = namedtuple('EMEData', 'name namespace')
    
    102 102
     
    
    103
    +MuclumbusResult = namedtuple('MuclumbusResult', 'first last max end items')
    
    104
    +
    
    105
    +MuclumbusItem = namedtuple('MuclumbusItem', 'jid name nusers description language is_open anonymity_mode')
    
    106
    +
    
    103 107
     
    
    104 108
     class OMEMOBundle(namedtuple('OMEMOBundle', 'spk spk_signature ik otpks')):
    
    105 109
         def pick_prekey(self):