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

Commits:

22 changed files:

Changes:

  • nbxmpp/client.py
    ... ... @@ -119,6 +119,7 @@ class Client(Observable):
    119 119
             self._mode = Mode.CLIENT
    
    120 120
     
    
    121 121
             self._ping_source_id = None
    
    122
    +        self._tasks = []
    
    122 123
     
    
    123 124
             self._dispatcher = StanzaDispatcher(self)
    
    124 125
             self._dispatcher.subscribe('before-dispatch', self._on_before_dispatch)
    
    ... ... @@ -130,6 +131,15 @@ class Client(Observable):
    130 131
     
    
    131 132
             self._state = StreamState.DISCONNECTED
    
    132 133
     
    
    134
    +    def add_task(self, task):
    
    135
    +        self._tasks.append(task)
    
    136
    +
    
    137
    +    def remove_task(self, task, _context):
    
    138
    +        try:
    
    139
    +            self._tasks.remove(task)
    
    140
    +        except Exception:
    
    141
    +            pass
    
    142
    +
    
    133 143
         @property
    
    134 144
         def log_context(self):
    
    135 145
             return self._log_context
    
    ... ... @@ -447,6 +457,8 @@ class Client(Observable):
    447 457
     
    
    448 458
         def _on_disconnected(self, _connection, _signal_name):
    
    449 459
             self.state = StreamState.DISCONNECTED
    
    460
    +        for task in self._tasks:
    
    461
    +            task.cancel()
    
    450 462
             self._remove_ping_timer()
    
    451 463
             self._dispatcher.remove_ping_callback(self._ping_id)
    
    452 464
             self._reset_stream()
    
    ... ... @@ -833,6 +845,8 @@ class Client(Observable):
    833 845
             self._dispatcher.unregister_handler(*args, **kwargs)
    
    834 846
     
    
    835 847
         def destroy(self):
    
    848
    +        for task in self._tasks:
    
    849
    +            task.cancel()
    
    836 850
             self._remove_ping_timer()
    
    837 851
             self._smacks = None
    
    838 852
             self._sasl = None
    

  • nbxmpp/connection.py
    ... ... @@ -134,6 +134,12 @@ class Connection(Observable):
    134 134
         def start_tls_negotiation(self):
    
    135 135
             raise NotImplementedError
    
    136 136
     
    
    137
    +    def shutdown_output(self):
    
    138
    +        raise NotImplementedError
    
    139
    +
    
    140
    +    def shutdown_input(self):
    
    141
    +        raise NotImplementedError
    
    142
    +
    
    137 143
         def destroy(self):
    
    138 144
             self.remove_subscriptions()
    
    139 145
             self._peer_certificate = None
    

  • nbxmpp/dispatcher.py
    ... ... @@ -75,6 +75,7 @@ from nbxmpp.modules.chatstates import Chatstates
    75 75
     from nbxmpp.modules.register import Register
    
    76 76
     from nbxmpp.modules.http_upload import HTTPUpload
    
    77 77
     from nbxmpp.modules.mam import MAM
    
    78
    +from nbxmpp.modules.vcard_temp import VCardTemp
    
    78 79
     from nbxmpp.modules.misc import unwrap_carbon
    
    79 80
     from nbxmpp.modules.misc import unwrap_mam
    
    80 81
     from nbxmpp.structs import StanzaTimeoutError
    
    ... ... @@ -180,6 +181,7 @@ class StanzaDispatcher(Observable):
    180 181
             self._modules['Register'] = Register(self._client)
    
    181 182
             self._modules['HTTPUpload'] = HTTPUpload(self._client)
    
    182 183
             self._modules['MAM'] = MAM(self._client)
    
    184
    +        self._modules['VCardTemp'] = VCardTemp(self._client)
    
    183 185
     
    
    184 186
             for instance in self._modules.values():
    
    185 187
                 for handler in instance.handlers:
    

  • nbxmpp/errors.py
    1
    +# Copyright (C) 2020 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
    +from nbxmpp.namespaces import Namespace
    
    19
    +
    
    20
    +
    
    21
    +def is_error(error):
    
    22
    +    return isinstance(error, BaseError)
    
    23
    +
    
    24
    +
    
    25
    +class BaseError(Exception):
    
    26
    +    def __init__(self, is_fatal=False):
    
    27
    +        self.is_fatal = is_fatal
    
    28
    +
    
    29
    +
    
    30
    +class StanzaError(BaseError):
    
    31
    +
    
    32
    +    log_level = 'info'
    
    33
    +    app_namespace = None
    
    34
    +
    
    35
    +    def __init__(self, stanza):
    
    36
    +        BaseError.__init__(self)
    
    37
    +        self.stanza = stanza
    
    38
    +        self._stanza_name = stanza.getName()
    
    39
    +        self._error_node = stanza.getTag('error')
    
    40
    +        self.condition = stanza.getError()
    
    41
    +        self.condition_data = self._error_node.getTagData(self.condition)
    
    42
    +        self.app_condition = self._get_app_condition()
    
    43
    +        self.type = stanza.getErrorType()
    
    44
    +        self.jid = stanza.getFrom()
    
    45
    +        self.id = stanza.getID()
    
    46
    +        self._text = {}
    
    47
    +
    
    48
    +        text_elements = self._error_node.getTags('text',
    
    49
    +                                                 namespace=Namespace.STANZAS)
    
    50
    +        for element in text_elements:
    
    51
    +            lang = element.getXmlLang()
    
    52
    +            text = element.getData()
    
    53
    +            self._text[lang] = text
    
    54
    +
    
    55
    +    def _get_app_condition(self):
    
    56
    +        if self.app_namespace is None:
    
    57
    +            return None
    
    58
    +
    
    59
    +        for node in self._error_node.getChildren():
    
    60
    +            if node.getNamespace() == self.app_namespace:
    
    61
    +                return node.getName()
    
    62
    +        return None
    
    63
    +
    
    64
    +    def get_text(self, pref_lang=None):
    
    65
    +        if pref_lang is not None:
    
    66
    +            text = self._text.get(pref_lang)
    
    67
    +            if text is not None:
    
    68
    +                return text
    
    69
    +
    
    70
    +        if self._text:
    
    71
    +            text = self._text.get('en')
    
    72
    +            if text is not None:
    
    73
    +                return text
    
    74
    +
    
    75
    +            text = self._text.get(None)
    
    76
    +            if text is not None:
    
    77
    +                return text
    
    78
    +            return self._text.popitem()[1]
    
    79
    +        return ''
    
    80
    +
    
    81
    +    def set_text(self, lang, text):
    
    82
    +        self._text[lang] = text
    
    83
    +
    
    84
    +    def __str__(self):
    
    85
    +        condition = self.condition
    
    86
    +        if self.app_condition is not None:
    
    87
    +            condition = '%s (%s)' % (self.condition, self.app_condition)
    
    88
    +        text = self.get_text('en') or ''
    
    89
    +        if text:
    
    90
    +            text = ' - %s' % text
    
    91
    +        return 'Error from %s: %s%s' % (self.jid, condition, text)
    
    92
    +
    
    93
    +
    
    94
    +class PubSubStanzaError(StanzaError):
    
    95
    +
    
    96
    +    app_namespace = Namespace.PUBSUB_ERROR
    
    97
    +
    
    98
    +
    
    99
    +class MalformedStanzaError(BaseError):
    
    100
    +
    
    101
    +    log_level = 'warning'
    
    102
    +
    
    103
    +    def __init__(self, text, stanza, is_fatal=True):
    
    104
    +        BaseError.__init__(self, is_fatal=is_fatal)
    
    105
    +        self.stanza = stanza
    
    106
    +        self.text = str(text)
    
    107
    +
    
    108
    +
    
    109
    +class CancelledError(BaseError):
    
    110
    +
    
    111
    +    log_level = 'info'
    
    112
    +
    
    113
    +    def __init__(self):
    
    114
    +        BaseError.__init__(self, is_fatal=True)
    
    115
    +        self.text = 'Task has been cancelled'

  • nbxmpp/modules/activity.py
    ... ... @@ -25,6 +25,11 @@ from nbxmpp.modules.base import BaseModule
    25 25
     
    
    26 26
     
    
    27 27
     class Activity(BaseModule):
    
    28
    +
    
    29
    +    _depends = {
    
    30
    +        'publish': 'PubSub'
    
    31
    +    }
    
    32
    +
    
    28 33
         def __init__(self, client):
    
    29 34
             BaseModule.__init__(self, client)
    
    30 35
     
    
    ... ... @@ -91,6 +96,4 @@ class Activity(BaseModule):
    91 96
                 if data.text:
    
    92 97
                     item.addChild('text', payload=data.text)
    
    93 98
     
    
    94
    -        jid = self._client.get_bound_jid().bare
    
    95
    -        self._client.get_module('PubSub').publish(
    
    96
    -            jid, Namespace.ACTIVITY, item, id_='current')
    99
    +        self.publish(Namespace.ACTIVITY, item, id_='current')

  • nbxmpp/modules/base.py
    ... ... @@ -21,7 +21,17 @@ from nbxmpp.util import LogAdapter
    21 21
     
    
    22 22
     
    
    23 23
     class BaseModule:
    
    24
    +
    
    25
    +    _depends = {}
    
    26
    +
    
    24 27
         def __init__(self, client):
    
    25 28
             logger_name = 'nbxmpp.m.%s' % self.__class__.__name__.lower()
    
    26 29
             self._log = LogAdapter(logging.getLogger(logger_name),
    
    27 30
                                    {'context': client.log_context})
    
    31
    +
    
    32
    +    def __getattr__(self, name):
    
    33
    +        if name not in self._depends:
    
    34
    +            raise AttributeError
    
    35
    +
    
    36
    +        module = self._client.get_module(self._depends[name])
    
    37
    +        return getattr(module, name)

  • nbxmpp/modules/bookmarks.py
    ... ... @@ -30,11 +30,9 @@ from nbxmpp.util import to_xs_boolean
    30 30
     from nbxmpp.util import call_on_response
    
    31 31
     from nbxmpp.util import callback
    
    32 32
     from nbxmpp.util import raise_error
    
    33
    -from nbxmpp.util import is_error_result
    
    34 33
     from nbxmpp.modules.pubsub import get_pubsub_item
    
    35 34
     from nbxmpp.modules.pubsub import get_pubsub_items
    
    36 35
     from nbxmpp.modules.pubsub import get_pubsub_request
    
    37
    -from nbxmpp.modules.pubsub import get_publish_options
    
    38 36
     from nbxmpp.modules.base import BaseModule
    
    39 37
     
    
    40 38
     
    
    ... ... @@ -47,13 +45,19 @@ BOOKMARK_2_OPTIONS = {
    47 45
         'pubsub#notify_delete': 'true',
    
    48 46
         'pubsub#notify_retract': 'true',
    
    49 47
         'pubsub#persist_items': 'true',
    
    50
    -    'pubsub#max_items': '128',
    
    48
    +    'pubsub#max_items': 'max',
    
    51 49
         'pubsub#access_model': 'whitelist',
    
    52 50
         'pubsub#send_last_published_item': 'never',
    
    53 51
     }
    
    54 52
     
    
    55 53
     
    
    56 54
     class Bookmarks(BaseModule):
    
    55
    +
    
    56
    +    _depends = {
    
    57
    +        'retract': 'PubSub',
    
    58
    +        'publish': 'PubSub',
    
    59
    +    }
    
    60
    +
    
    57 61
         def __init__(self, client):
    
    58 62
             BaseModule.__init__(self, client)
    
    59 63
     
    
    ... ... @@ -71,8 +75,6 @@ class Bookmarks(BaseModule):
    71 75
     
    
    72 76
             self._bookmark_2_queue = {}
    
    73 77
             self._bookmark_1_queue = []
    
    74
    -        self._node_configuration_in_progress = False
    
    75
    -        self._node_configuration_not_possible = False
    
    76 78
     
    
    77 79
         def _process_pubsub_bookmarks(self, _client, stanza, properties):
    
    78 80
             if not properties.is_pubsub_event:
    
    ... ... @@ -315,115 +317,44 @@ class Bookmarks(BaseModule):
    315 317
     
    
    316 318
         def retract_bookmark(self, bookmark_jid):
    
    317 319
             self._log.info('Retract Bookmark: %s', bookmark_jid)
    
    318
    -        jid = self._client.get_bound_jid().bare
    
    319
    -        self._client.get_module('PubSub').retract(jid,
    
    320
    -                                                  Namespace.BOOKMARKS_2,
    
    321
    -                                                  str(bookmark_jid))
    
    320
    +
    
    321
    +        self.retract(Namespace.BOOKMARKS_2, str(bookmark_jid))
    
    322 322
     
    
    323 323
         def _store_bookmark_1(self, bookmarks):
    
    324 324
             self._log.info('Store Bookmarks 1 (PubSub)')
    
    325
    -        jid = self._client.get_bound_jid().bare
    
    325
    +
    
    326 326
             self._bookmark_1_queue = bookmarks
    
    327 327
             item = self._build_storage_node(bookmarks)
    
    328
    -        options = get_publish_options(BOOKMARK_1_OPTIONS)
    
    329
    -        self._client.get_module('PubSub').publish(
    
    330
    -            jid,
    
    331
    -            Namespace.BOOKMARKS,
    
    332
    -            item,
    
    333
    -            id_='current',
    
    334
    -            options=options,
    
    335
    -            callback=self._on_store_bookmark_result,
    
    336
    -            user_data=Namespace.BOOKMARKS)
    
    328
    +        self.publish(Namespace.BOOKMARKS,
    
    329
    +                     item,
    
    330
    +                     id_='current',
    
    331
    +                     options=BOOKMARK_1_OPTIONS,
    
    332
    +                     force_node_options=True,
    
    333
    +                     callback=self._on_store_bookmark_result,
    
    334
    +                     user_data=Namespace.BOOKMARKS)
    
    337 335
     
    
    338 336
         def _store_bookmark_2(self, bookmarks):
    
    339
    -        if self._node_configuration_not_possible:
    
    340
    -            self._log.warning('Node configuration not possible')
    
    341
    -            return
    
    342
    -
    
    343 337
             self._log.info('Store Bookmarks 2 (PubSub)')
    
    344
    -        jid = self._client.get_bound_jid().bare
    
    338
    +
    
    345 339
             for bookmark in bookmarks:
    
    346 340
                 self._bookmark_2_queue[bookmark.jid] = bookmark
    
    347 341
                 item = self._build_conference_node(bookmark)
    
    348
    -            options = get_publish_options(BOOKMARK_2_OPTIONS)
    
    349
    -            self._client.get_module('PubSub').publish(
    
    350
    -                jid,
    
    351
    -                Namespace.BOOKMARKS_2,
    
    352
    -                item,
    
    353
    -                id_=str(bookmark.jid),
    
    354
    -                options=options,
    
    355
    -                callback=self._on_store_bookmark_result,
    
    356
    -                user_data=Namespace.BOOKMARKS_2)
    
    357
    -
    
    358
    -    def _on_store_bookmark_result(self, result, node):
    
    359
    -        if not is_error_result(result):
    
    360
    -            self._bookmark_1_queue = []
    
    361
    -            self._bookmark_2_queue.pop(result.id, None)
    
    362
    -            return
    
    363
    -
    
    364
    -        if (result.condition == 'conflict' and
    
    365
    -                result.app_condition == 'precondition-not-met'):
    
    366
    -            if self._node_configuration_in_progress:
    
    367
    -                return
    
    368
    -
    
    369
    -            self._node_configuration_in_progress = True
    
    370
    -            jid = self._client.get_bound_jid().bare
    
    371
    -            self._client.get_module('PubSub').get_node_configuration(
    
    372
    -                jid,
    
    373
    -                node,
    
    374
    -                callback=self._on_node_configuration_received)
    
    375
    -
    
    376
    -        else:
    
    377
    -            self._bookmark_1_queue = []
    
    378
    -            self._bookmark_2_queue.pop(result.id, None)
    
    379
    -            self._log.warning(result)
    
    380
    -
    
    381
    -    def _on_node_configuration_received(self, result):
    
    382
    -        if is_error_result(result):
    
    383
    -            self._log.warning(result)
    
    384
    -            self._bookmark_1_queue = []
    
    385
    -            self._bookmark_2_queue.clear()
    
    386
    -            return
    
    387
    -
    
    388
    -        if result.node == Namespace.BOOKMARKS:
    
    389
    -            config = BOOKMARK_1_OPTIONS
    
    390
    -        else:
    
    391
    -            config = BOOKMARK_2_OPTIONS
    
    392
    -        self._apply_config(result.form, config)
    
    393
    -        self._client.get_module('PubSub').set_node_configuration(
    
    394
    -            result.jid,
    
    395
    -            result.node,
    
    396
    -            result.form,
    
    397
    -            callback=self._on_node_configuration_finished)
    
    398
    -
    
    399
    -    def _on_node_configuration_finished(self, result):
    
    400
    -        self._node_configuration_in_progress = False
    
    401
    -        if is_error_result(result):
    
    402
    -            self._log.warning(result)
    
    403
    -            self._bookmark_2_queue.clear()
    
    404
    -            self._bookmark_1_queue = []
    
    405
    -            self._node_configuration_not_possible = True
    
    406
    -            return
    
    407
    -
    
    408
    -        self._log.info('Republish bookmarks')
    
    409
    -        if self._bookmark_2_queue:
    
    410
    -            bookmarks = self._bookmark_2_queue.copy()
    
    411
    -            self._bookmark_2_queue.clear()
    
    412
    -            self._store_bookmark_2(bookmarks.values())
    
    413
    -        else:
    
    414
    -            bookmarks = self._bookmark_1_queue.copy()
    
    415
    -            self._bookmark_1_queue.clear()
    
    416
    -            self._store_bookmark_1(bookmarks)
    
    342
    +            self.publish(Namespace.BOOKMARKS_2,
    
    343
    +                         item,
    
    344
    +                         id_=str(bookmark.jid),
    
    345
    +                         options=BOOKMARK_2_OPTIONS,
    
    346
    +                         force_node_options=True,
    
    347
    +                         callback=self._on_store_bookmark_result,
    
    348
    +                         user_data=Namespace.BOOKMARKS_2)
    
    349
    +
    
    350
    +    def _on_store_bookmark_result(self, task):
    
    351
    +        try:
    
    352
    +            result = task.finish()
    
    353
    +        except Exception as error:
    
    354
    +            self._log.warning(error)
    
    417 355
     
    
    418
    -    @staticmethod
    
    419
    -    def _apply_config(form, config):
    
    420
    -        for var, value in config.items():
    
    421
    -            try:
    
    422
    -                field = form[var]
    
    423
    -            except KeyError:
    
    424
    -                pass
    
    425
    -            else:
    
    426
    -                field.value = value
    
    356
    +        self._bookmark_1_queue = []
    
    357
    +        self._bookmark_2_queue.pop(result.id, None)
    
    427 358
     
    
    428 359
         @call_on_response('_on_private_store_result')
    
    429 360
         def _store_with_private(self, bookmarks):
    

  • nbxmpp/modules/location.py
    ... ... @@ -24,6 +24,11 @@ from nbxmpp.modules.base import BaseModule
    24 24
     
    
    25 25
     
    
    26 26
     class Location(BaseModule):
    
    27
    +
    
    28
    +    _depends = {
    
    29
    +        'publish': 'PubSub'
    
    30
    +    }
    
    31
    +
    
    27 32
         def __init__(self, client):
    
    28 33
             BaseModule.__init__(self, client)
    
    29 34
     
    
    ... ... @@ -70,6 +75,4 @@ class Location(BaseModule):
    70 75
                     if value is not None:
    
    71 76
                         item.addChild(tag, payload=value)
    
    72 77
     
    
    73
    -        jid = self._client.get_bound_jid().bare
    
    74
    -        self._client.get_module('PubSub').publish(
    
    75
    -            jid, Namespace.LOCATION, item, id_='current')
    78
    +        self.publish(Namespace.LOCATION, item, id_='current')

  • nbxmpp/modules/mood.py
    ... ... @@ -25,6 +25,11 @@ from nbxmpp.modules.base import BaseModule
    25 25
     
    
    26 26
     
    
    27 27
     class Mood(BaseModule):
    
    28
    +
    
    29
    +    _depends = {
    
    30
    +        'publish': 'PubSub'
    
    31
    +    }
    
    32
    +
    
    28 33
         def __init__(self, client):
    
    29 34
             BaseModule.__init__(self, client)
    
    30 35
     
    
    ... ... @@ -80,6 +85,4 @@ class Mood(BaseModule):
    80 85
                 if data.text:
    
    81 86
                     item.addChild('text', payload=data.text)
    
    82 87
     
    
    83
    -        jid = self._client.get_bound_jid().bare
    
    84
    -        self._client.get_module('PubSub').publish(
    
    85
    -            jid, Namespace.MOOD, item, id_='current')
    88
    +        self.publish(Namespace.MOOD, item, id_='current')

  • nbxmpp/modules/nickname.py
    ... ... @@ -23,6 +23,11 @@ from nbxmpp.modules.base import BaseModule
    23 23
     
    
    24 24
     
    
    25 25
     class Nickname(BaseModule):
    
    26
    +
    
    27
    +    _depends = {
    
    28
    +        'publish': 'PubSub'
    
    29
    +    }
    
    30
    +
    
    26 31
         def __init__(self, client):
    
    27 32
             BaseModule.__init__(self, client)
    
    28 33
     
    
    ... ... @@ -87,6 +92,5 @@ class Nickname(BaseModule):
    87 92
             item = Node('nick', {'xmlns': Namespace.NICK})
    
    88 93
             if nickname is not None:
    
    89 94
                 item.addData(nickname)
    
    90
    -        jid = self._client.get_bound_jid().bare
    
    91
    -        self._client.get_module('PubSub').publish(
    
    92
    -            jid, Namespace.NICK, item, id_='current')
    95
    +
    
    96
    +        self.publish(Namespace.NICK, item, id_='current')

  • nbxmpp/modules/omemo.py
    ... ... @@ -35,6 +35,11 @@ from nbxmpp.modules.base import BaseModule
    35 35
     
    
    36 36
     
    
    37 37
     class OMEMO(BaseModule):
    
    38
    +
    
    39
    +    _depends = {
    
    40
    +        'publish': 'PubSub'
    
    41
    +    }
    
    42
    +
    
    38 43
         def __init__(self, client):
    
    39 44
             BaseModule.__init__(self, client)
    
    40 45
     
    
    ... ... @@ -192,9 +197,8 @@ class OMEMO(BaseModule):
    192 197
                 item.addChild('device').setAttr('id', device)
    
    193 198
     
    
    194 199
             self._log.info('Set devicelist: %s', devicelist)
    
    195
    -        jid = self._client.get_bound_jid().bare
    
    196
    -        self._client.get_module('PubSub').publish(
    
    197
    -            jid, Namespace.OMEMO_TEMP_DL, item, id_='current')
    
    200
    +
    
    201
    +        self.publish(Namespace.OMEMO_TEMP_DL, item, id_='current')
    
    198 202
     
    
    199 203
         @call_on_response('_devicelist_received')
    
    200 204
         def request_devicelist(self, jid=None):
    
    ... ... @@ -225,9 +229,8 @@ class OMEMO(BaseModule):
    225 229
             self._log.info('Set bundle')
    
    226 230
     
    
    227 231
             node = '%s:%s' % (Namespace.OMEMO_TEMP_BUNDLE, device_id)
    
    228
    -        jid = self._client.get_bound_jid().bare
    
    229
    -        self._client.get_module('PubSub').publish(
    
    230
    -            jid, node, item, id_='current')
    
    232
    +
    
    233
    +        self.publish(node, item, id_='current')
    
    231 234
     
    
    232 235
         @staticmethod
    
    233 236
         def _create_bundle(bundle):
    

  • nbxmpp/modules/openpgp.py
    ... ... @@ -38,6 +38,11 @@ from nbxmpp.modules.base import BaseModule
    38 38
     
    
    39 39
     
    
    40 40
     class OpenPGP(BaseModule):
    
    41
    +
    
    42
    +    _depends = {
    
    43
    +        'publish': 'PubSub'
    
    44
    +    }
    
    45
    +
    
    41 46
         def __init__(self, client):
    
    42 47
             BaseModule.__init__(self, client)
    
    43 48
     
    
    ... ... @@ -154,9 +159,8 @@ class OpenPGP(BaseModule):
    154 159
                     item.addChild('pubkey-metadata', attrs=attrs)
    
    155 160
     
    
    156 161
             self._log.info('Set keylist: %s', keylist)
    
    157
    -        jid = self._client.get_bound_jid().bare
    
    158
    -        self._client.get_module('PubSub').publish(
    
    159
    -            jid, Namespace.OPENPGP_PK, item, id_='current')
    
    162
    +
    
    163
    +        self.publish(Namespace.OPENPGP_PK, item, id_='current')
    
    160 164
     
    
    161 165
         def set_public_key(self, key, fingerprint, date):
    
    162 166
             date = time.strftime(
    
    ... ... @@ -168,9 +172,8 @@ class OpenPGP(BaseModule):
    168 172
             node = '%s:%s' % (Namespace.OPENPGP_PK, fingerprint)
    
    169 173
     
    
    170 174
             self._log.info('Set public key')
    
    171
    -        jid = self._client.get_bound_jid().bare
    
    172
    -        self._client.get_module('PubSub').publish(
    
    173
    -            jid, node, item, id_='current')
    
    175
    +
    
    176
    +        self.publish(node, item, id_='current')
    
    174 177
     
    
    175 178
         @call_on_response('_public_key_received')
    
    176 179
         def request_public_key(self, jid, fingerprint):
    
    ... ... @@ -274,9 +277,8 @@ class OpenPGP(BaseModule):
    274 277
                 item.setData(b64encode(secret_key))
    
    275 278
     
    
    276 279
             self._log.info('Set secret key')
    
    277
    -        jid = self._client.get_bound_jid().bare
    
    278
    -        self._client.get_module('PubSub').publish(
    
    279
    -            jid, Namespace.OPENPGP_SK, item, id_='current')
    
    280
    +
    
    281
    +        self.publish(Namespace.OPENPGP_SK, item, id_='current')
    
    280 282
     
    
    281 283
     
    
    282 284
     def parse_signcrypt(stanza):
    

  • nbxmpp/modules/pubsub.py
    1
    -# Copyright (C) 2018 Philipp Hörist <philipp AT hoerist.com>
    
    1
    +# Copyright (C) 2020 Philipp Hörist <philipp AT hoerist.com>
    
    2 2
     #
    
    3 3
     # This file is part of nbxmpp.
    
    4 4
     #
    
    ... ... @@ -15,20 +15,20 @@
    15 15
     # You should have received a copy of the GNU General Public License
    
    16 16
     # along with this program; If not, see <http://www.gnu.org/licenses/>.
    
    17 17
     
    
    18
    -from nbxmpp.namespaces import Namespace
    
    19
    -from nbxmpp.protocol import Node
    
    20
    -from nbxmpp.protocol import Iq
    
    21
    -from nbxmpp.protocol import isResultNode
    
    18
    +from collections import namedtuple
    
    19
    +
    
    20
    +from nbxmpp.task import iq_request_task
    
    21
    +from nbxmpp.errors import is_error
    
    22
    +from nbxmpp.errors import PubSubStanzaError
    
    23
    +from nbxmpp.errors import MalformedStanzaError
    
    22 24
     from nbxmpp.structs import StanzaHandler
    
    23 25
     from nbxmpp.structs import PubSubEventData
    
    24
    -from nbxmpp.structs import CommonResult
    
    25
    -from nbxmpp.structs import PubSubConfigResult
    
    26
    -from nbxmpp.structs import PubSubPublishResult
    
    27
    -from nbxmpp.modules.dataforms import extend_form
    
    28
    -from nbxmpp.util import call_on_response
    
    29
    -from nbxmpp.util import callback
    
    30
    -from nbxmpp.util import raise_error
    
    26
    +from nbxmpp.protocol import Iq
    
    27
    +from nbxmpp.protocol import Node
    
    28
    +from nbxmpp.namespaces import Namespace
    
    31 29
     from nbxmpp.modules.base import BaseModule
    
    30
    +from nbxmpp.modules.util import process_response
    
    31
    +from nbxmpp.modules.dataforms import extend_form
    
    32 32
     
    
    33 33
     
    
    34 34
     class PubSub(BaseModule):
    
    ... ... @@ -84,107 +84,101 @@ class PubSub(BaseModule):
    84 84
                 id_ = item.getAttr('id')
    
    85 85
                 properties.pubsub_event = PubSubEventData(node, id_, item)
    
    86 86
     
    
    87
    -    @call_on_response('_publish_result_received')
    
    88
    -    def publish(self, jid, node, item, id_=None, options=None):
    
    89
    -        query = Iq('set', to=jid)
    
    90
    -        pubsub = query.addChild('pubsub', namespace=Namespace.PUBSUB)
    
    91
    -        publish = pubsub.addChild('publish', {'node': node})
    
    92
    -        attrs = {}
    
    93
    -        if id_ is not None:
    
    94
    -            attrs = {'id': id_}
    
    95
    -        publish.addChild('item', attrs, [item])
    
    96
    -        if options:
    
    97
    -            publish = pubsub.addChild('publish-options')
    
    98
    -            publish.addChild(node=options)
    
    99
    -        return query
    
    100
    -
    
    101
    -    @callback
    
    102
    -    def _publish_result_received(self, stanza):
    
    103
    -        if not isResultNode(stanza):
    
    104
    -            return raise_error(self._log.warning, stanza)
    
    105
    -
    
    106
    -        jid = stanza.getFrom()
    
    107
    -        pubsub = stanza.getTag('pubsub', namespace=Namespace.PUBSUB)
    
    108
    -        if pubsub is None:
    
    109
    -            # XEP-0060: IQ payload is not mandatory on result
    
    110
    -            return PubSubPublishResult(jid, None, None)
    
    111
    -
    
    112
    -        publish = pubsub.getTag('publish')
    
    113
    -        if publish is None:
    
    114
    -            return raise_error(self._log.warning, stanza, 'stanza-malformed')
    
    115
    -
    
    116
    -        node = publish.getAttr('node')
    
    117
    -        item = publish.getTag('item')
    
    118
    -        if item is None:
    
    119
    -            return raise_error(self._log.warning, stanza, 'stanza-malformed')
    
    120
    -
    
    121
    -        id_ = item.getAttr('id')
    
    122
    -        return PubSubPublishResult(jid, node, id_)
    
    123
    -
    
    124
    -    @call_on_response('_default_response')
    
    125
    -    def retract(self, jid, node, id_, notify=True):
    
    126
    -        query = Iq('set', to=jid)
    
    127
    -        pubsub = query.addChild('pubsub', namespace=Namespace.PUBSUB)
    
    128
    -        attrs = {'node': node}
    
    129
    -        if notify:
    
    130
    -            attrs['notify'] = 'true'
    
    131
    -        retract = pubsub.addChild('retract', attrs=attrs)
    
    132
    -        retract.addChild('item', {'id': id_})
    
    133
    -        return query
    
    134
    -
    
    135
    -    @call_on_response('_default_response')
    
    136
    -    def set_node_configuration(self, jid, node, form):
    
    137
    -        self._log.info('Set configuration for %s %s', node, jid)
    
    138
    -        query = Iq('set', to=jid)
    
    139
    -        pubsub = query.addChild('pubsub', namespace=Namespace.PUBSUB_OWNER)
    
    140
    -        configure = pubsub.addChild('configure', {'node': node})
    
    141
    -        form.setAttr('type', 'submit')
    
    142
    -        configure.addChild(node=form)
    
    143
    -        return query
    
    144
    -
    
    145
    -    @call_on_response('_node_configuration_received')
    
    146
    -    def get_node_configuration(self, jid, node):
    
    147
    -        self._log.info('Request node configuration')
    
    148
    -        query = Iq('get', to=jid)
    
    149
    -        pubsub = query.addChild('pubsub', namespace=Namespace.PUBSUB_OWNER)
    
    150
    -        pubsub.addChild('configure', {'node': node})
    
    151
    -        return query
    
    152
    -
    
    153
    -    @callback
    
    154
    -    def _node_configuration_received(self, stanza):
    
    155
    -        if not isResultNode(stanza):
    
    156
    -            return raise_error(self._log.warning, stanza)
    
    157
    -
    
    158
    -        jid = stanza.getFrom()
    
    159
    -        pubsub = stanza.getTag('pubsub', namespace=Namespace.PUBSUB_OWNER)
    
    160
    -        if pubsub is None:
    
    161
    -            return raise_error(self._log.warning, stanza, 'stanza-malformed',
    
    162
    -                               'No pubsub node found')
    
    163
    -
    
    164
    -        configure = pubsub.getTag('configure')
    
    165
    -        if configure is None:
    
    166
    -            return raise_error(self._log.warning, stanza, 'stanza-malformed',
    
    167
    -                               'No configure node found')
    
    168
    -
    
    169
    -        node = configure.getAttr('node')
    
    170
    -
    
    171
    -        forms = configure.getTags('x', namespace=Namespace.DATA)
    
    172
    -        for form in forms:
    
    173
    -            dataform = extend_form(node=form)
    
    174
    -            form_type = dataform.vars.get('FORM_TYPE')
    
    175
    -            if form_type is None or form_type.value != Namespace.PUBSUB_CONFIG:
    
    176
    -                continue
    
    177
    -            self._log.info('Node configuration received from: %s', jid)
    
    178
    -            return PubSubConfigResult(jid=jid, node=node, form=dataform)
    
    179
    -
    
    180
    -        return raise_error(self._log.warning, stanza, 'stanza-malformed',
    
    181
    -                           'No valid form type found')
    
    182
    -
    
    183
    -    @callback
    
    184
    -    def _default_response(self, stanza):
    
    185
    -        if not isResultNode(stanza):
    
    186
    -            return raise_error(self._log.info, stanza)
    
    187
    -        return CommonResult(jid=stanza.getFrom())
    
    87
    +    @iq_request_task
    
    88
    +    def request_item(self, node, id_, jid=None):
    
    89
    +        task = yield
    
    90
    +
    
    91
    +        response = yield _make_pubsub_request(node, id_=id_, jid=jid)
    
    92
    +
    
    93
    +        if response.isError():
    
    94
    +            raise PubSubStanzaError(response)
    
    95
    +
    
    96
    +        item = _get_pubsub_item(response, node, id_)
    
    97
    +        yield task.set_result(item)
    
    98
    +
    
    99
    +    @iq_request_task
    
    100
    +    def request_items(self, node, max_items=None, jid=None):
    
    101
    +        _task = yield
    
    102
    +
    
    103
    +        response = yield _make_pubsub_request(node,
    
    104
    +                                              max_items=max_items,
    
    105
    +                                              jid=jid)
    
    106
    +
    
    107
    +        if response.isError():
    
    108
    +            raise PubSubStanzaError(response)
    
    109
    +
    
    110
    +        yield _get_pubsub_items(response, node)
    
    111
    +
    
    112
    +    @iq_request_task
    
    113
    +    def publish(self,
    
    114
    +                node,
    
    115
    +                item,
    
    116
    +                id_=None,
    
    117
    +                options=None,
    
    118
    +                jid=None,
    
    119
    +                force_node_options=False):
    
    120
    +
    
    121
    +        _task = yield
    
    122
    +
    
    123
    +        request = _make_publish_request(node, item, id_, options, jid)
    
    124
    +        response = yield request
    
    125
    +
    
    126
    +        if response.isError():
    
    127
    +            error = PubSubStanzaError(response)
    
    128
    +            if (not force_node_options or
    
    129
    +                    error.app_condition != 'precondition-not-met'):
    
    130
    +                raise error
    
    131
    +
    
    132
    +            result = yield self.reconfigure_node(node, options, jid)
    
    133
    +            if is_error(result):
    
    134
    +                raise result
    
    135
    +
    
    136
    +            response = yield request
    
    137
    +            if response.isError():
    
    138
    +                raise PubSubStanzaError(response)
    
    139
    +
    
    140
    +        jid = response.getFrom()
    
    141
    +        item_id = _get_published_item_id(response, node, id_)
    
    142
    +        yield PubSubPublishResult(jid, node, item_id)
    
    143
    +
    
    144
    +    @iq_request_task
    
    145
    +    def retract(self, node, id_, jid=None, notify=True):
    
    146
    +        _task = yield
    
    147
    +
    
    148
    +        response = yield _make_retract_request(node, id_, jid, notify)
    
    149
    +        yield process_response(response)
    
    150
    +
    
    151
    +    @iq_request_task
    
    152
    +    def reconfigure_node(self, node, options, jid=None):
    
    153
    +        _task = yield
    
    154
    +
    
    155
    +        result = yield self.get_node_configuration(node, jid)
    
    156
    +        if is_error(result):
    
    157
    +            raise result
    
    158
    +
    
    159
    +        _apply_options(result.form, options)
    
    160
    +        result = yield self.set_node_configuration(node, result.form, jid)
    
    161
    +        yield result
    
    162
    +
    
    163
    +    @iq_request_task
    
    164
    +    def set_node_configuration(self, node, form, jid=None):
    
    165
    +        _task = yield
    
    166
    +
    
    167
    +        response = yield _make_node_configuration(node, form, jid)
    
    168
    +        yield process_response(response)
    
    169
    +
    
    170
    +    @iq_request_task
    
    171
    +    def get_node_configuration(self, node, jid=None):
    
    172
    +        _task = yield
    
    173
    +
    
    174
    +        response = yield _make_node_configuration_request(node, jid)
    
    175
    +
    
    176
    +        if response.isError():
    
    177
    +            raise PubSubStanzaError(response)
    
    178
    +
    
    179
    +        jid = response.getFrom()
    
    180
    +        form = _get_configure_form(response, node)
    
    181
    +        yield PubSubNodeConfigurationResult(jid=jid, node=node, form=form)
    
    188 182
     
    
    189 183
     
    
    190 184
     def get_pubsub_request(jid, node, id_=None, max_items=None):
    
    ... ... @@ -225,3 +219,171 @@ def get_publish_options(config):
    225 219
             field = options.addChild('field', attrs={'var': var})
    
    226 220
             field.setTagData('value', value)
    
    227 221
         return options
    
    222
    +
    
    223
    +
    
    224
    +def _get_pubsub_items(response, node):
    
    225
    +    pubsub_node = response.getTag('pubsub', namespace=Namespace.PUBSUB)
    
    226
    +    if pubsub_node is None:
    
    227
    +        raise MalformedStanzaError('pubsub node missing', response)
    
    228
    +
    
    229
    +    items_node = pubsub_node.getTag('items')
    
    230
    +    if items_node is None:
    
    231
    +        raise MalformedStanzaError('items node missing', response)
    
    232
    +
    
    233
    +    if items_node.getAttr('node') != node:
    
    234
    +        raise MalformedStanzaError('invalid node attr', response)
    
    235
    +
    
    236
    +    return items_node.getTags('item')
    
    237
    +
    
    238
    +
    
    239
    +def _get_pubsub_item(response, node, id_):
    
    240
    +    items = _get_pubsub_items(response, node)
    
    241
    +
    
    242
    +    if len(items) > 1:
    
    243
    +        raise MalformedStanzaError('multiple items found', response)
    
    244
    +
    
    245
    +    if not items:
    
    246
    +        return None
    
    247
    +
    
    248
    +    item = items[0]
    
    249
    +    if item.getAttr('id') != id_:
    
    250
    +        raise MalformedStanzaError('invalid item id', response)
    
    251
    +
    
    252
    +    return item
    
    253
    +
    
    254
    +
    
    255
    +def _make_pubsub_request(node, id_=None, max_items=None, jid=None):
    
    256
    +    query = Iq('get', to=jid)
    
    257
    +    pubsub = query.addChild('pubsub', namespace=Namespace.PUBSUB)
    
    258
    +    items = pubsub.addChild('items', {'node': node})
    
    259
    +    if max_items is not None:
    
    260
    +        items.setAttr('max_items', max_items)
    
    261
    +    if id_ is not None:
    
    262
    +        items.addChild('item', {'id': id_})
    
    263
    +    return query
    
    264
    +
    
    265
    +
    
    266
    +def _get_configure_form(response, node):
    
    267
    +    pubsub = response.getTag('pubsub', namespace=Namespace.PUBSUB_OWNER)
    
    268
    +    if pubsub is None:
    
    269
    +        raise MalformedStanzaError('pubsub node missing', response)
    
    270
    +
    
    271
    +    configure = pubsub.getTag('configure')
    
    272
    +    if configure is None:
    
    273
    +        raise MalformedStanzaError('configure node missing', response)
    
    274
    +
    
    275
    +    if node != configure.getAttr('node'):
    
    276
    +        raise MalformedStanzaError('invalid node attribute', response)
    
    277
    +
    
    278
    +    forms = configure.getTags('x', namespace=Namespace.DATA)
    
    279
    +    for form in forms:
    
    280
    +        dataform = extend_form(node=form)
    
    281
    +        form_type = dataform.vars.get('FORM_TYPE')
    
    282
    +        if form_type is None or form_type.value != Namespace.PUBSUB_CONFIG:
    
    283
    +            continue
    
    284
    +
    
    285
    +        return dataform
    
    286
    +
    
    287
    +    raise MalformedStanzaError('no valid form type found', response)
    
    288
    +
    
    289
    +
    
    290
    +def _get_published_item_id(response, node, id_):
    
    291
    +    pubsub = response.getTag('pubsub', namespace=Namespace.PUBSUB)
    
    292
    +    if pubsub is None:
    
    293
    +        # https://xmpp.org/extensions/xep-0060.html#publisher-publish-success
    
    294
    +        # If the publish request did not include an ItemID,
    
    295
    +        # the IQ-result SHOULD include an empty <item/> element
    
    296
    +        # that specifies the ItemID of the published item.
    
    297
    +        #
    
    298
    +        # If the server did not add a payload we assume the item was
    
    299
    +        # published with the id we requested
    
    300
    +        return id_
    
    301
    +
    
    302
    +    publish = pubsub.getTag('publish')
    
    303
    +    if publish is None:
    
    304
    +        raise MalformedStanzaError('publish node missing', response)
    
    305
    +
    
    306
    +    if node != publish.getAttr('node'):
    
    307
    +        raise MalformedStanzaError('invalid node attribute', response)
    
    308
    +
    
    309
    +    item = publish.getTag('item')
    
    310
    +    if item is None:
    
    311
    +        raise MalformedStanzaError('item node missing', response)
    
    312
    +
    
    313
    +    item_id = item.getAttr('id')
    
    314
    +    if id_ is not None and item_id != id_:
    
    315
    +        raise MalformedStanzaError('invalid item id', response)
    
    316
    +
    
    317
    +    return item_id
    
    318
    +
    
    319
    +
    
    320
    +def _make_publish_request(node, item, id_, options, jid):
    
    321
    +    query = Iq('set', to=jid)
    
    322
    +    pubsub = query.addChild('pubsub', namespace=Namespace.PUBSUB)
    
    323
    +    publish = pubsub.addChild('publish', {'node': node})
    
    324
    +    attrs = {}
    
    325
    +    if id_ is not None:
    
    326
    +        attrs = {'id': id_}
    
    327
    +    publish.addChild('item', attrs, [item])
    
    328
    +    if options:
    
    329
    +        publish = pubsub.addChild('publish-options')
    
    330
    +        publish.addChild(node=_make_publish_options(options))
    
    331
    +    return query
    
    332
    +
    
    333
    +
    
    334
    +def _make_publish_options(options):
    
    335
    +    data = Node(Namespace.DATA + ' x', attrs={'type': 'submit'})
    
    336
    +    field = data.addChild('field', attrs={'var': 'FORM_TYPE', 'type': 'hidden'})
    
    337
    +    field.setTagData('value', Namespace.PUBSUB_PUBLISH_OPTIONS)
    
    338
    +
    
    339
    +    for var, value in options.items():
    
    340
    +        field = data.addChild('field', attrs={'var': var})
    
    341
    +        field.setTagData('value', value)
    
    342
    +    return data
    
    343
    +
    
    344
    +
    
    345
    +def _make_retract_request(node, id_, jid, notify):
    
    346
    +    query = Iq('set', to=jid)
    
    347
    +    pubsub = query.addChild('pubsub', namespace=Namespace.PUBSUB)
    
    348
    +    attrs = {'node': node}
    
    349
    +    if notify:
    
    350
    +        attrs['notify'] = 'true'
    
    351
    +    retract = pubsub.addChild('retract', attrs=attrs)
    
    352
    +    retract.addChild('item', {'id': id_})
    
    353
    +    return query
    
    354
    +
    
    355
    +
    
    356
    +def _make_node_configuration(node, form, jid):
    
    357
    +    query = Iq('set', to=jid)
    
    358
    +    pubsub = query.addChild('pubsub', namespace=Namespace.PUBSUB_OWNER)
    
    359
    +    configure = pubsub.addChild('configure', {'node': node})
    
    360
    +    form.setAttr('type', 'submit')
    
    361
    +    configure.addChild(node=form)
    
    362
    +    return query
    
    363
    +
    
    364
    +
    
    365
    +def _make_node_configuration_request(node, jid):
    
    366
    +    query = Iq('get', to=jid)
    
    367
    +    pubsub = query.addChild('pubsub', namespace=Namespace.PUBSUB_OWNER)
    
    368
    +    pubsub.addChild('configure', {'node': node})
    
    369
    +    return query
    
    370
    +
    
    371
    +
    
    372
    +def _apply_options(form, options):
    
    373
    +    for var, value in options.items():
    
    374
    +        try:
    
    375
    +            field = form[var]
    
    376
    +        except KeyError:
    
    377
    +            pass
    
    378
    +        else:
    
    379
    +            field.value = value
    
    380
    +
    
    381
    +
    
    382
    +PubSubNodeConfigurationResult = namedtuple('PubSubConfigResult',
    
    383
    +                                           'jid node form')
    
    384
    +
    
    385
    +PubSubConfigResult = namedtuple('PubSubConfigResult',
    
    386
    +                                'jid node form')
    
    387
    +
    
    388
    +PubSubPublishResult = namedtuple('PubSubPublishResult',
    
    389
    +                                 'jid node id')

  • nbxmpp/modules/tune.py
    ... ... @@ -24,6 +24,11 @@ from nbxmpp.modules.base import BaseModule
    24 24
     
    
    25 25
     
    
    26 26
     class Tune(BaseModule):
    
    27
    +
    
    28
    +    _depends = {
    
    29
    +        'publish': 'PubSub'
    
    30
    +    }
    
    31
    +
    
    27 32
         def __init__(self, client):
    
    28 33
             BaseModule.__init__(self, client)
    
    29 34
     
    
    ... ... @@ -70,6 +75,4 @@ class Tune(BaseModule):
    70 75
                     if value is not None:
    
    71 76
                         item.addChild(tag, payload=value)
    
    72 77
     
    
    73
    -        jid = self._client.get_bound_jid().bare
    
    74
    -        self._client.get_module('PubSub').publish(
    
    75
    -            jid, Namespace.TUNE, item, id_='current')
    78
    +        self.publish(Namespace.TUNE, item, id_='current')

  • nbxmpp/modules/user_avatar.py
    ... ... @@ -15,22 +15,37 @@
    15 15
     # You should have received a copy of the GNU General Public License
    
    16 16
     # along with this program; If not, see <http://www.gnu.org/licenses/>.
    
    17 17
     
    
    18
    -import base64
    
    18
    +from typing import List
    
    19
    +from typing import Dict
    
    20
    +
    
    21
    +import hashlib
    
    22
    +from dataclasses import dataclass
    
    23
    +from dataclasses import asdict
    
    24
    +from dataclasses import field
    
    19 25
     
    
    20 26
     from nbxmpp.namespaces import Namespace
    
    21 27
     from nbxmpp.protocol import NodeProcessed
    
    22
    -from nbxmpp.protocol import isResultNode
    
    28
    +from nbxmpp.protocol import Node
    
    23 29
     from nbxmpp.structs import StanzaHandler
    
    24
    -from nbxmpp.structs import AvatarMetaData
    
    25
    -from nbxmpp.structs import AvatarData
    
    26
    -from nbxmpp.util import call_on_response
    
    27
    -from nbxmpp.util import callback
    
    28
    -from nbxmpp.util import raise_error
    
    29
    -from nbxmpp.modules.pubsub import get_pubsub_request
    
    30
    +from nbxmpp.structs import CommonResult
    
    31
    +from nbxmpp.util import b64encode
    
    32
    +from nbxmpp.util import b64decode
    
    33
    +from nbxmpp.errors import MalformedStanzaError
    
    34
    +from nbxmpp.task import iq_request_task
    
    30 35
     from nbxmpp.modules.base import BaseModule
    
    36
    +from nbxmpp.modules.util import raise_if_error
    
    37
    +from nbxmpp.modules.util import finalize
    
    31 38
     
    
    32 39
     
    
    33 40
     class UserAvatar(BaseModule):
    
    41
    +
    
    42
    +    _depends = {
    
    43
    +        'get_node_configuration': 'PubSub',
    
    44
    +        'set_node_configuration': 'PubSub',
    
    45
    +        'publish': 'PubSub',
    
    46
    +        'request_item': 'PubSub',
    
    47
    +    }
    
    48
    +
    
    34 49
         def __init__(self, client):
    
    35 50
             BaseModule.__init__(self, client)
    
    36 51
     
    
    ... ... @@ -65,11 +80,10 @@ class UserAvatar(BaseModule):
    65 80
                                properties.jid)
    
    66 81
                 return
    
    67 82
     
    
    68
    -        info = metadata.getTags('info', one=True)
    
    69 83
             try:
    
    70
    -            data = AvatarMetaData(**info.getAttrs())
    
    71
    -        except Exception:
    
    72
    -            self._log.warning('Malformed user avatar data')
    
    84
    +            data = AvatarMetaData.from_node(metadata)
    
    85
    +        except Exception as error:
    
    86
    +            self._log.warning('Malformed user avatar data: %s', error)
    
    73 87
                 self._log.warning(stanza)
    
    74 88
                 raise NodeProcessed
    
    75 89
     
    
    ... ... @@ -79,42 +93,255 @@ class UserAvatar(BaseModule):
    79 93
     
    
    80 94
             properties.pubsub_event = pubsub_event
    
    81 95
     
    
    82
    -    @call_on_response('_avatar_data_received')
    
    83
    -    def request_avatar(self, jid, id_):
    
    84
    -        return get_pubsub_request(jid, Namespace.AVATAR_DATA, id_=id_)
    
    96
    +    @iq_request_task
    
    97
    +    def request_avatar(self, avatar_info, jid=None):
    
    98
    +        task = yield
    
    85 99
     
    
    86
    -    @callback
    
    87
    -    def _avatar_data_received(self, stanza):
    
    88
    -        jid = stanza.getFrom()
    
    89
    -        if jid is None:
    
    90
    -            jid = self._client.get_bound_jid().bare
    
    100
    +        item = yield self.request_item(Namespace.AVATAR_DATA,
    
    101
    +                                       id_=avatar_info.id,
    
    102
    +                                       jid=jid)
    
    91 103
     
    
    92
    -        if not isResultNode(stanza):
    
    93
    -            return raise_error(self._log.warning, stanza)
    
    104
    +        raise_if_error(item)
    
    94 105
     
    
    95
    -        pubsub_node = stanza.getTag('pubsub')
    
    96
    -        items_node = pubsub_node.getTag('items')
    
    97
    -        item = items_node.getTag('item')
    
    98 106
             if item is None:
    
    99
    -            return raise_error(self._log.warning, stanza, 'stanza-malformed',
    
    100
    -                               'No item in node found')
    
    107
    +            yield task.set_result(None)
    
    108
    +
    
    109
    +        yield _get_avatar_data(item, avatar_info.id)
    
    110
    +
    
    111
    +    @iq_request_task
    
    112
    +    def set_avatar(self, avatar, public=False):
    
    113
    +
    
    114
    +        task = yield
    
    115
    +
    
    116
    +        if avatar is None:
    
    117
    +            result = yield self._publish_avatar_metadata(None)
    
    118
    +            yield finalize(task, result)
    
    119
    +
    
    120
    +        access_model = 'open' if public else 'presence'
    
    121
    +
    
    122
    +        result = yield self._publish_avatar(avatar, access_model)
    
    123
    +
    
    124
    +        yield finalize(task, result)
    
    125
    +
    
    126
    +    @iq_request_task
    
    127
    +    def _publish_avatar(self, avatar, access_model):
    
    128
    +        task = yield
    
    129
    +
    
    130
    +        options = {
    
    131
    +            'pubsub#persist_items': 'true',
    
    132
    +            'pubsub#access_model': access_model,
    
    133
    +        }
    
    134
    +
    
    135
    +        for info, data in avatar.pubsub_avatar_info():
    
    136
    +            item = _make_avatar_data_node(data)
    
    137
    +            self._log.info('Publish avatar data: %s, %s', info, access_model)
    
    138
    +
    
    139
    +            result = yield self.publish(Namespace.AVATAR_DATA,
    
    140
    +                                        item,
    
    141
    +                                        id_=info.id,
    
    142
    +                                        options=options,
    
    143
    +                                        force_node_options=True)
    
    144
    +
    
    145
    +            raise_if_error(result)
    
    146
    +
    
    147
    +        result = yield self._publish_avatar_metadata(avatar.metadata)
    
    101 148
     
    
    102
    -        sha = item.getAttr('id')
    
    103
    -        data_node = item.getTag('data', namespace=Namespace.AVATAR_DATA)
    
    104
    -        if data_node is None:
    
    105
    -            return raise_error(self._log.warning, stanza, 'stanza-malformed',
    
    106
    -                               'No data node found')
    
    149
    +        yield finalize(task, result)
    
    107 150
     
    
    108
    -        data = data_node.getData()
    
    109
    -        if data is None:
    
    110
    -            return raise_error(self._log.warning, stanza, 'stanza-malformed',
    
    111
    -                               'Data node empty')
    
    151
    +    @iq_request_task
    
    152
    +    def _publish_avatar_metadata(self, metadata):
    
    153
    +        task = yield
    
    154
    +
    
    155
    +        self._log.info('Publish avatar meta data: %s', metadata)
    
    156
    +
    
    157
    +        result = yield self.publish(Namespace.AVATAR_METADATA,
    
    158
    +                                    metadata.to_node(),
    
    159
    +                                    id_='current')
    
    160
    +
    
    161
    +        yield finalize(task, result)
    
    162
    +
    
    163
    +    @iq_request_task
    
    164
    +    def get_access_model(self):
    
    165
    +        _task = yield
    
    166
    +
    
    167
    +        self._log.info('Request access model')
    
    168
    +
    
    169
    +        result = yield self.get_node_configuration(Namespace.AVATAR_DATA)
    
    170
    +
    
    171
    +        raise_if_error(result)
    
    172
    +
    
    173
    +        yield result.form['pubsub#access_model'].value
    
    174
    +
    
    175
    +    @iq_request_task
    
    176
    +    def set_access_model(self, model):
    
    177
    +        task = yield
    
    178
    +
    
    179
    +        if model not in ('open', 'presence'):
    
    180
    +            raise ValueError('Invalid access model')
    
    181
    +
    
    182
    +        result = yield self.get_node_configuration(Namespace.AVATAR_DATA)
    
    183
    +
    
    184
    +        raise_if_error(result)
    
    112 185
     
    
    113 186
             try:
    
    114
    -            data = base64.b64decode(data.encode('utf-8'))
    
    115
    -        except Exception as error:
    
    116
    -            return raise_error(self._log.warning, stanza,
    
    117
    -                               'stanza-malformed', str(error))
    
    187
    +            access_model = result.form['pubsub#access_model'].value
    
    188
    +        except Exception:
    
    189
    +            yield task.set_error('warning',
    
    190
    +                                 condition='access-model-not-supported')
    
    191
    +
    
    192
    +        if access_model == model:
    
    193
    +            jid = self._client.get_bound_jid().new_as_bare()
    
    194
    +            yield CommonResult(jid=jid)
    
    195
    +
    
    196
    +        result.form['pubsub#access_model'].value = model
    
    197
    +
    
    198
    +        self._log.info('Set access model %s', model)
    
    199
    +
    
    200
    +        result = yield self.set_node_configuration(Namespace.AVATAR_DATA,
    
    201
    +                                                   result.form)
    
    202
    +
    
    203
    +        yield finalize(task, result)
    
    204
    +
    
    205
    +
    
    206
    +def _get_avatar_data(item, id_):
    
    207
    +    data_node = item.getTag('data', namespace=Namespace.AVATAR_DATA)
    
    208
    +    if data_node is None:
    
    209
    +        raise MalformedStanzaError('data node missing', item)
    
    210
    +
    
    211
    +    data = data_node.getData()
    
    212
    +    if not data:
    
    213
    +        raise MalformedStanzaError('data node empty', item)
    
    214
    +
    
    215
    +    try:
    
    216
    +        avatar = b64decode(data, return_type=bytes)
    
    217
    +    except Exception as error:
    
    218
    +        raise MalformedStanzaError(f'decoding error: {error}', item)
    
    219
    +
    
    220
    +    avatar_sha = hashlib.sha1(avatar).hexdigest()
    
    221
    +    if avatar_sha != id_:
    
    222
    +        raise MalformedStanzaError(f'avatar does not match sha', item)
    
    223
    +
    
    224
    +    return AvatarData(data=avatar, sha=avatar_sha)
    
    225
    +
    
    226
    +
    
    227
    +def _make_metadata_node(infos):
    
    228
    +    item = Node('metadata', attrs={'xmlns': Namespace.AVATAR_METADATA})
    
    229
    +    for info in infos:
    
    230
    +        item.addChild('info', attrs=info.to_dict())
    
    231
    +    return item
    
    232
    +
    
    233
    +
    
    234
    +def _make_avatar_data_node(avatar):
    
    235
    +    item = Node('data', attrs={'xmlns': Namespace.AVATAR_DATA})
    
    236
    +    item.setData(b64encode(avatar.data))
    
    237
    +    return item
    
    238
    +
    
    239
    +
    
    240
    +def _get_info_attrs(avatar, avatar_sha, height, width):
    
    241
    +    info_attrs = {
    
    242
    +        'id': avatar_sha,
    
    243
    +        'bytes': len(avatar),
    
    244
    +        'type': 'image/png',
    
    245
    +    }
    
    246
    +
    
    247
    +    if height is not None:
    
    248
    +        info_attrs['height'] = height
    
    249
    +
    
    250
    +    if width is not None:
    
    251
    +        info_attrs['width'] = width
    
    252
    +
    
    253
    +    return info_attrs
    
    254
    +
    
    255
    +
    
    256
    +@dataclass
    
    257
    +class AvatarInfo:
    
    258
    +    bytes: int
    
    259
    +    id: str
    
    260
    +    type: str
    
    261
    +    url: str = None
    
    262
    +    height: int = None
    
    263
    +    width: int = None
    
    264
    +
    
    265
    +    def __post_init__(self):
    
    266
    +        if self.bytes is None:
    
    267
    +            raise ValueError
    
    268
    +        if self.id is None:
    
    269
    +            raise ValueError
    
    270
    +        if self.type is None:
    
    271
    +            raise ValueError
    
    272
    +
    
    273
    +        self.bytes = int(self.bytes)
    
    274
    +
    
    275
    +        if self.height is not None:
    
    276
    +            self.height = int(self.height)
    
    277
    +        if self.width is not None:
    
    278
    +            self.width = int(self.width)
    
    279
    +
    
    280
    +    def to_dict(self):
    
    281
    +        info_dict = asdict(self)
    
    282
    +        if self.height is None:
    
    283
    +            info_dict.pop('height')
    
    284
    +        if self.width is None:
    
    285
    +            info_dict.pop('width')
    
    286
    +        if self.url is None:
    
    287
    +            info_dict.pop('url')
    
    288
    +        return info_dict
    
    289
    +
    
    290
    +
    
    291
    +@dataclass
    
    292
    +class AvatarData:
    
    293
    +    data: bytes
    
    294
    +    sha: str
    
    295
    +
    
    296
    +
    
    297
    +@dataclass
    
    298
    +class AvatarMetaData:
    
    299
    +    infos: List[AvatarInfo] = field(default_factory=list)
    
    300
    +
    
    301
    +    @classmethod
    
    302
    +    def from_node(cls, node):
    
    303
    +        infos = []
    
    304
    +        info_nodes = node.getTags('info')
    
    305
    +        for info in info_nodes:
    
    306
    +            infos.append(AvatarInfo(
    
    307
    +                bytes=info.getAttr('bytes'),
    
    308
    +                id=info.getAttr('id'),
    
    309
    +                type=info.getAttr('type'),
    
    310
    +                url=info.getAttr('url'),
    
    311
    +                height=info.getAttr('height'),
    
    312
    +                width=info.getAttr('width')
    
    313
    +            ))
    
    314
    +        return cls(infos=infos)
    
    315
    +
    
    316
    +    def add_avatar_info(self, avatar_info):
    
    317
    +        self.infos.append(avatar_info)
    
    318
    +
    
    319
    +    def to_node(self):
    
    320
    +        return _make_metadata_node(self.infos)
    
    321
    +
    
    322
    +    @property
    
    323
    +    def avatar_shas(self):
    
    324
    +        return [info.id for info in self.infos]
    
    325
    +
    
    326
    +
    
    327
    +@dataclass
    
    328
    +class Avatar:
    
    329
    +    metadata: AvatarMetaData = field(default_factory=AvatarMetaData)
    
    330
    +    data: Dict[AvatarInfo, bytes] = field(init=False, default_factory=dict)
    
    331
    +
    
    332
    +    def add_image_source(self, data, type_, height, width, url=None):
    
    333
    +        sha = hashlib.sha1(data).hexdigest()
    
    334
    +        info = AvatarInfo(bytes=len(data),
    
    335
    +                          id=sha,
    
    336
    +                          type=type_,
    
    337
    +                          height=height,
    
    338
    +                          width=width,
    
    339
    +                          url=url)
    
    340
    +        self.metadata.add_avatar_info(info)
    
    341
    +        self.data[info] = AvatarData(data=data, sha=sha)
    
    118 342
     
    
    119
    -        self._log.info('Received avatar data: %s %s', jid, sha)
    
    120
    -        return AvatarData(jid, sha, data)
    343
    +    def pubsub_avatar_info(self):
    
    344
    +        for info, data in self.data.items():
    
    345
    +            if info.url is not None:
    
    346
    +                continue
    
    347
    +            yield info, data

  • nbxmpp/modules/util.py
    1
    +# Copyright (C) 2020 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
    +from nbxmpp.structs import CommonResult
    
    19
    +from nbxmpp.errors import StanzaError
    
    20
    +from nbxmpp.errors import is_error
    
    21
    +from nbxmpp.simplexml import Node
    
    22
    +
    
    23
    +
    
    24
    +def process_response(response):
    
    25
    +    if response.isError():
    
    26
    +        raise StanzaError(response)
    
    27
    +
    
    28
    +    return CommonResult(jid=response.getFrom())
    
    29
    +
    
    30
    +
    
    31
    +def raise_if_error(result):
    
    32
    +    if is_error(result):
    
    33
    +        raise result
    
    34
    +
    
    35
    +
    
    36
    +def finalize(task, result):
    
    37
    +    if is_error(result):
    
    38
    +        raise result
    
    39
    +    if isinstance(result, Node):
    
    40
    +        return task.set_result(result)
    
    41
    +    return result

  • nbxmpp/modules/vcard_temp.py
    1
    +# Copyright (C) 2020 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 hashlib
    
    19
    +from dataclasses import dataclass
    
    20
    +from dataclasses import field
    
    21
    +
    
    22
    +from nbxmpp.task import iq_request_task
    
    23
    +from nbxmpp.util import b64decode
    
    24
    +from nbxmpp.util import b64encode
    
    25
    +from nbxmpp.errors import StanzaError
    
    26
    +from nbxmpp.errors import MalformedStanzaError
    
    27
    +from nbxmpp.protocol import Iq
    
    28
    +from nbxmpp.simplexml import Node
    
    29
    +from nbxmpp.namespaces import Namespace
    
    30
    +from nbxmpp.modules.base import BaseModule
    
    31
    +from nbxmpp.modules.util import process_response
    
    32
    +
    
    33
    +
    
    34
    +class VCardTemp(BaseModule):
    
    35
    +    def __init__(self, client):
    
    36
    +        BaseModule.__init__(self, client)
    
    37
    +
    
    38
    +        self._client = client
    
    39
    +        self.handlers = []
    
    40
    +
    
    41
    +    @iq_request_task
    
    42
    +    def request_vcard(self, jid=None):
    
    43
    +        _task = yield
    
    44
    +
    
    45
    +        response = yield _make_vcard_request(jid)
    
    46
    +
    
    47
    +        if response.isError():
    
    48
    +            raise StanzaError(response)
    
    49
    +
    
    50
    +        vcard_node = _get_vcard_node(response)
    
    51
    +        yield VCard.from_node(vcard_node)
    
    52
    +
    
    53
    +    @iq_request_task
    
    54
    +    def set_vcard(self, vcard, jid=None):
    
    55
    +        _task = yield
    
    56
    +
    
    57
    +        response = yield _make_vcard_publish(jid, vcard)
    
    58
    +        yield process_response(response)
    
    59
    +
    
    60
    +
    
    61
    +def _make_vcard_request(jid):
    
    62
    +    iq = Iq(typ='get', to=jid)
    
    63
    +    iq.addChild('vCard', namespace=Namespace.VCARD)
    
    64
    +    return iq
    
    65
    +
    
    66
    +
    
    67
    +def _get_vcard_node(response):
    
    68
    +    vcard_node = response.getTag('vCard', namespace=Namespace.VCARD)
    
    69
    +    if vcard_node is None:
    
    70
    +        raise MalformedStanzaError('vCard node missing', response)
    
    71
    +    return vcard_node
    
    72
    +
    
    73
    +
    
    74
    +def _make_vcard_publish(jid, vcard):
    
    75
    +    iq = Iq(typ='set', to=jid)
    
    76
    +    iq.addChild(node=vcard.to_node())
    
    77
    +    return iq
    
    78
    +
    
    79
    +
    
    80
    +@dataclass
    
    81
    +class VCard:
    
    82
    +    data: dict = field(default_factory=dict)
    
    83
    +
    
    84
    +    @classmethod
    
    85
    +    def from_node(cls, node):
    
    86
    +        dict_ = {}
    
    87
    +        for info in node.getChildren():
    
    88
    +            name = info.getName()
    
    89
    +            if name in ('ADR', 'TEL', 'EMAIL'):
    
    90
    +                dict_.setdefault(name, [])
    
    91
    +                entry = {}
    
    92
    +                for child in info.getChildren():
    
    93
    +                    entry[child.getName()] = child.getData()
    
    94
    +                dict_[name].append(entry)
    
    95
    +            elif info.getChildren() == []:
    
    96
    +                dict_[name] = info.getData()
    
    97
    +            else:
    
    98
    +                dict_[name] = {}
    
    99
    +                for child in info.getChildren():
    
    100
    +                    dict_[name][child.getName()] = child.getData()
    
    101
    +
    
    102
    +        return cls(data=dict_)
    
    103
    +
    
    104
    +    def to_node(self):
    
    105
    +        vcard = Node(tag='vCard', attrs={'xmlns': Namespace.VCARD})
    
    106
    +        for i in self.data:
    
    107
    +            if i == 'jid':
    
    108
    +                continue
    
    109
    +            if isinstance(self.data[i], dict):
    
    110
    +                child = vcard.addChild(i)
    
    111
    +                for j in self.data[i]:
    
    112
    +                    child.addChild(j).setData(self.data[i][j])
    
    113
    +            elif isinstance(self.data[i], list):
    
    114
    +                for j in self.data[i]:
    
    115
    +                    child = vcard.addChild(i)
    
    116
    +                    for k in j:
    
    117
    +                        child.addChild(k).setData(j[k])
    
    118
    +            else:
    
    119
    +                vcard.addChild(i).setData(self.data[i])
    
    120
    +        return vcard
    
    121
    +
    
    122
    +    def set_avatar(self, avatar, type_=None):
    
    123
    +        avatar = b64encode(avatar)
    
    124
    +        if 'PHOTO' not in self.data:
    
    125
    +            self.data['PHOTO'] = {}
    
    126
    +
    
    127
    +        self.data['PHOTO']['BINVAL'] = avatar
    
    128
    +
    
    129
    +        if type_ is not None:
    
    130
    +            self.data['PHOTO']['TYPE'] = type_
    
    131
    +
    
    132
    +    def get_avatar(self):
    
    133
    +        try:
    
    134
    +            avatar = self.data['PHOTO']['BINVAL']
    
    135
    +        except Exception:
    
    136
    +            return None, None
    
    137
    +
    
    138
    +        if not avatar:
    
    139
    +            return None, None
    
    140
    +
    
    141
    +        avatar = b64decode(avatar, return_type=bytes)
    
    142
    +        avatar_sha = hashlib.sha1(avatar).hexdigest()
    
    143
    +        return avatar, avatar_sha

  • nbxmpp/namespaces.py
    ... ... @@ -121,6 +121,7 @@ class _Namespaces:
    121 121
         PUBKEY_PUBKEY: str = 'urn:xmpp:pubkey:2'
    
    122 122
         PUBKEY_REVOKE: str = 'urn:xmpp:revoke:2'
    
    123 123
         PUBSUB: str = 'http://jabber.org/protocol/pubsub'
    
    124
    +    PUBSUB_ERROR: str = 'http://jabber.org/protocol/pubsub#errors'
    
    124 125
         PUBSUB_CONFIG: str = 'http://jabber.org/protocol/pubsub#node_config'
    
    125 126
         PUBSUB_EVENT: str = 'http://jabber.org/protocol/pubsub#event'
    
    126 127
         PUBSUB_OWNER: str = 'http://jabber.org/protocol/pubsub#owner'
    

  • nbxmpp/protocol.py
    ... ... @@ -1072,6 +1072,12 @@ class Protocol(Node):
    1072 1072
             if timestamp is not None:
    
    1073 1073
                 self.setTimestamp(timestamp)
    
    1074 1074
     
    
    1075
    +    def isError(self):
    
    1076
    +        return self.getAttr('type') == 'error'
    
    1077
    +
    
    1078
    +    def isResult(self):
    
    1079
    +        return self.getAttr('type') == 'result'
    
    1080
    +
    
    1075 1081
         def getTo(self):
    
    1076 1082
             """
    
    1077 1083
             Return value of the 'to' attribute
    

  • nbxmpp/structs.py
    ... ... @@ -73,10 +73,6 @@ StanzaIDData.__new__.__defaults__ = (None, None)
    73 73
     PubSubEventData = namedtuple('PubSubEventData', 'node id item data deleted retracted purged')
    
    74 74
     PubSubEventData.__new__.__defaults__ = (None, None, None, False, False, False)
    
    75 75
     
    
    76
    -PubSubConfigResult = namedtuple('PubSubConfigResult', 'jid node form')
    
    77
    -
    
    78
    -PubSubPublishResult = namedtuple('PubSubPublishResult', 'jid node id')
    
    79
    -
    
    80 76
     MoodData = namedtuple('MoodData', 'mood text')
    
    81 77
     
    
    82 78
     ActivityData = namedtuple('ActivityData', 'activity subactivity text')
    
    ... ... @@ -84,12 +80,6 @@ ActivityData = namedtuple('ActivityData', 'activity subactivity text')
    84 80
     LocationData = namedtuple('LocationData', LOCATION_DATA)
    
    85 81
     LocationData.__new__.__defaults__ = (None,) * len(LocationData._fields)
    
    86 82
     
    
    87
    -AvatarMetaData = namedtuple('AvatarMetaData', 'bytes height width id type url')
    
    88
    -AvatarMetaData.__new__.__defaults__ = (None,) * len(AvatarMetaData._fields)
    
    89
    -
    
    90
    -AvatarData = namedtuple('AvatarData', 'jid sha data')
    
    91
    -AvatarData.__new__.__defaults__ = (None,) * len(AvatarData._fields)
    
    92
    -
    
    93 83
     BookmarkData = namedtuple('BookmarkData', 'jid name nick autojoin password')
    
    94 84
     BookmarkData.__new__.__defaults__ = (None, None, None, None)
    
    95 85
     
    

  • nbxmpp/task.py
    1
    +# Copyright (C) 2020 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 weakref
    
    19
    +import inspect
    
    20
    +import logging
    
    21
    +from enum import IntEnum
    
    22
    +from functools import wraps
    
    23
    +
    
    24
    +from nbxmpp.errors import is_error
    
    25
    +from nbxmpp.errors import CancelledError
    
    26
    +from nbxmpp.simplexml import Node
    
    27
    +
    
    28
    +
    
    29
    +log = logging.getLogger('nbxmpp.task')
    
    30
    +
    
    31
    +
    
    32
    +class _ResultSet:
    
    33
    +    pass
    
    34
    +
    
    35
    +ResultSet = _ResultSet()
    
    36
    +
    
    37
    +
    
    38
    +class NoType:
    
    39
    +    pass
    
    40
    +
    
    41
    +
    
    42
    +class TaskState(IntEnum):
    
    43
    +    INIT = 0
    
    44
    +    RUNNING = 1
    
    45
    +    FINISHED = 2
    
    46
    +    CANCELLED = 3
    
    47
    +
    
    48
    +    @property
    
    49
    +    def is_init(self):
    
    50
    +        return self == TaskState.INIT
    
    51
    +
    
    52
    +    @property
    
    53
    +    def is_running(self):
    
    54
    +        return self == TaskState.RUNNING
    
    55
    +
    
    56
    +    @property
    
    57
    +    def is_finished(self):
    
    58
    +        return self == TaskState.FINISHED
    
    59
    +
    
    60
    +    @property
    
    61
    +    def is_cancelled(self):
    
    62
    +        return self == TaskState.CANCELLED
    
    63
    +
    
    64
    +
    
    65
    +def iq_request_task(func):
    
    66
    +    @wraps(func)
    
    67
    +    def func_wrapper(self, *args,
    
    68
    +                     callback=None, user_data=None, **kwargs):
    
    69
    +        task = IqRequestTask(func(self, *args, **kwargs),
    
    70
    +                             self._log,
    
    71
    +                             self._client)
    
    72
    +        self._client.add_task(task)
    
    73
    +        task.set_finalize_func(self._client.remove_task)
    
    74
    +        task.set_user_data(user_data)
    
    75
    +        if callback is not None:
    
    76
    +            task.add_done_callback(callback)
    
    77
    +        task.start()
    
    78
    +        return task
    
    79
    +    return func_wrapper
    
    80
    +
    
    81
    +
    
    82
    +def is_fatal_error(error):
    
    83
    +    if is_error(error):
    
    84
    +        return error.is_fatal
    
    85
    +    return isinstance(error, Exception)
    
    86
    +
    
    87
    +
    
    88
    +class Task:
    
    89
    +
    
    90
    +    '''
    
    91
    +    Base class for wrapping a generator method.
    
    92
    +
    
    93
    +    It runs the generator depending on what the generator yields. If the
    
    94
    +    generator yields another generator a sub task is created. If it yields
    
    95
    +    a type defined in _process_types, _run_async() is called which needs to
    
    96
    +    be implemented by classes.
    
    97
    +
    
    98
    +    the implementation of _run_async() must be really async, means it should
    
    99
    +    not call _async_finished() in the same mainloop cycle. Otherwise sub tasks
    
    100
    +    may break. _async_finished() needs to call _next_step(result).
    
    101
    +    '''
    
    102
    +
    
    103
    +    _process_types = (NoType,)
    
    104
    +
    
    105
    +    def __init__(self, gen, logger=log):
    
    106
    +        self._logger = logger
    
    107
    +        self._gen = gen
    
    108
    +        self._done_callbacks = []
    
    109
    +        self._sub_task = None
    
    110
    +        self._result = None
    
    111
    +        self._error = None
    
    112
    +        self._user_data = None
    
    113
    +        self._finalize_func = None
    
    114
    +        self._finalize_context = None
    
    115
    +        self._state = TaskState.INIT
    
    116
    +
    
    117
    +    @property
    
    118
    +    def state(self):
    
    119
    +        return self._state
    
    120
    +
    
    121
    +    def add_done_callback(self, callback, weak=True):
    
    122
    +        if self._state in (TaskState.FINISHED, TaskState.CANCELLED):
    
    123
    +            raise RuntimeError('Task is finished')
    
    124
    +
    
    125
    +        if weak:
    
    126
    +            if inspect.ismethod(callback):
    
    127
    +                callback = weakref.WeakMethod(callback)
    
    128
    +            elif inspect.isfunction(callback):
    
    129
    +                callback = weakref.ref(callback)
    
    130
    +            else:
    
    131
    +                ValueError('Unknown callback object')
    
    132
    +
    
    133
    +        self._done_callbacks.append(callback)
    
    134
    +
    
    135
    +    def start(self):
    
    136
    +        if not self._state.is_init:
    
    137
    +            raise RuntimeError('Task already started')
    
    138
    +
    
    139
    +        self._state = TaskState.RUNNING
    
    140
    +        next(self._gen)
    
    141
    +        self._next_step(self)
    
    142
    +
    
    143
    +    def _run_async(self, data):
    
    144
    +        raise NotImplementedError
    
    145
    +
    
    146
    +    def _async_finished(self, *args, **kwargs):
    
    147
    +        raise NotImplementedError
    
    148
    +
    
    149
    +    def _sub_task_completed(self, task):
    
    150
    +        self._sub_task = None
    
    151
    +        if self._state.is_cancelled:
    
    152
    +            return
    
    153
    +
    
    154
    +        result = task.get_result()
    
    155
    +        if is_fatal_error(result):
    
    156
    +            self._error = result
    
    157
    +            self._set_finished()
    
    158
    +        else:
    
    159
    +            self._next_step(result)
    
    160
    +
    
    161
    +    def _next_step(self, result):
    
    162
    +        try:
    
    163
    +            res = self._gen.send(result)
    
    164
    +        except StopIteration:
    
    165
    +            self._set_finished()
    
    166
    +            return
    
    167
    +
    
    168
    +        except Exception as error:
    
    169
    +            self._log_if_fatal(error)
    
    170
    +            self._error = error
    
    171
    +            self._set_finished()
    
    172
    +            return
    
    173
    +
    
    174
    +        if isinstance(res, self._process_types):
    
    175
    +            self._run_async(res)
    
    176
    +
    
    177
    +        elif isinstance(res, Task):
    
    178
    +            if self._sub_task is not None:
    
    179
    +                RuntimeError('Only one sub task can be active')
    
    180
    +
    
    181
    +            self._sub_task = res
    
    182
    +            self._sub_task.add_done_callback(self._sub_task_completed,
    
    183
    +                                             weak=False)
    
    184
    +
    
    185
    +        else:
    
    186
    +            if res is not ResultSet:
    
    187
    +                self._result = res
    
    188
    +            self._set_finished()
    
    189
    +
    
    190
    +    def _set_finished(self):
    
    191
    +        self._state = TaskState.FINISHED
    
    192
    +        self._invoke_callbacks()
    
    193
    +        self._finalize()
    
    194
    +
    
    195
    +    def _log_if_fatal(self, error):
    
    196
    +        if is_error(error):
    
    197
    +            if error.is_fatal:
    
    198
    +                self._logger.log(error.log_level, error)
    
    199
    +
    
    200
    +        elif isinstance(error, Exception):
    
    201
    +            self._logger.exception('Fatal Exception')
    
    202
    +
    
    203
    +    def _invoke_callbacks(self):
    
    204
    +        for callback in self._done_callbacks:
    
    205
    +            if isinstance(callback, weakref.WeakMethod):
    
    206
    +                callback = callback()
    
    207
    +                if callback is None:
    
    208
    +                    return
    
    209
    +
    
    210
    +            callback(self)
    
    211
    +
    
    212
    +    def set_result(self, result):
    
    213
    +        self._result = result
    
    214
    +        return ResultSet
    
    215
    +
    
    216
    +    def get_result(self):
    
    217
    +        # if self._error is None, there was no error
    
    218
    +        # but None is a valid value for self._result
    
    219
    +        if self._error is not None:
    
    220
    +            return self._error
    
    221
    +        return self._result
    
    222
    +
    
    223
    +    def finish(self):
    
    224
    +        if self._error is not None:
    
    225
    +            raise self._error  # pylint: disable=raising-bad-type
    
    226
    +        return self._result
    
    227
    +
    
    228
    +    def set_user_data(self, user_data):
    
    229
    +        self._user_data = user_data
    
    230
    +
    
    231
    +    def get_user_data(self):
    
    232
    +        return self._user_data
    
    233
    +
    
    234
    +    def set_finalize_func(self, func, context=None):
    
    235
    +        self._finalize_func = func
    
    236
    +        self._finalize_context = context
    
    237
    +
    
    238
    +    def cancel(self):
    
    239
    +        if self._state.is_cancelled:
    
    240
    +            return
    
    241
    +
    
    242
    +        self._state = TaskState.CANCELLED
    
    243
    +        if self._sub_task is not None:
    
    244
    +            self._sub_task.cancel()
    
    245
    +
    
    246
    +        self._error = CancelledError()
    
    247
    +        self._invoke_callbacks()
    
    248
    +        self._finalize()
    
    249
    +
    
    250
    +    def _finalize(self):
    
    251
    +        self._done_callbacks.clear()
    
    252
    +        self._sub_task = None
    
    253
    +        self._error = None
    
    254
    +        self._result = None
    
    255
    +        self._user_data = None
    
    256
    +        self._gen.close()
    
    257
    +        if self._finalize_func is not None:
    
    258
    +            self._finalize_func(self, self._finalize_context)
    
    259
    +
    
    260
    +
    
    261
    +class IqRequestTask(Task):
    
    262
    +
    
    263
    +    '''
    
    264
    +    A Task for running IQ requests
    
    265
    +
    
    266
    +    '''
    
    267
    +
    
    268
    +    _process_types = (Node,)
    
    269
    +
    
    270
    +    def __init__(self, gen, logger, client):
    
    271
    +        super().__init__(gen, logger)
    
    272
    +        self._client = client
    
    273
    +
    
    274
    +    def _run_async(self, stanza):
    
    275
    +        self._client.send_stanza(stanza, callback=self._async_finished)
    
    276
    +
    
    277
    +    def _async_finished(self, _client, result, *args, **kwargs):
    
    278
    +        if self._state != TaskState.CANCELLED:
    
    279
    +            self._next_step(result)
    
    280
    +
    
    281
    +    def _finalize(self):
    
    282
    +        self._client = None
    
    283
    +        super()._finalize()

  • test/unit/test_avatar.py
    ... ... @@ -2,27 +2,27 @@ from test.lib.util import StanzaHandlerTest
    2 2
     
    
    3 3
     from nbxmpp.namespaces import Namespace
    
    4 4
     from nbxmpp.structs import StanzaHandler
    
    5
    -from nbxmpp.structs import AvatarMetaData
    
    6 5
     from nbxmpp.structs import PubSubEventData
    
    7
    -
    
    6
    +from nbxmpp.modules.user_avatar import AvatarMetaData
    
    7
    +from nbxmpp.modules.user_avatar import AvatarInfo
    
    8 8
     
    
    9 9
     class AvatarTest(StanzaHandlerTest):
    
    10 10
     
    
    11 11
         def test_avatar_parsing(self):
    
    12 12
             def _on_message(_con, _stanza, properties):
    
    13
    -
    
    14
    -            data = AvatarMetaData(bytes='12345',
    
    15
    -                                  height='64',
    
    16
    -                                  width='64',
    
    17
    -                                  id='111f4b3c50d7b0df729d299bc6f8e9ef9066971f',
    
    18
    -                                  type='image/png',
    
    19
    -                                  url='http://avatars.example.org/happy.gif')
    
    20
    -
    
    13
    +            info = AvatarInfo(bytes='12345',
    
    14
    +                              height='64',
    
    15
    +                              width='64',
    
    16
    +                              id='111f4b3c50d7b0df729d299bc6f8e9ef9066971f',
    
    17
    +                              type='image/png',
    
    18
    +                              url='http://avatars.example.org/happy.gif')
    
    19
    +
    
    20
    +            metadata = AvatarMetaData(infos=[info])
    
    21 21
                 pubsub_event = PubSubEventData(
    
    22 22
                     node='urn:xmpp:avatar:metadata',
    
    23 23
                     id='111f4b3c50d7b0df729d299bc6f8e9ef9066971f',
    
    24 24
                     item=None,
    
    25
    -                data=data,
    
    25
    +                data=metadata,
    
    26 26
                     deleted=False,
    
    27 27
                     retracted=False,
    
    28 28
                     purged=False)