Philipp Hörist pushed to branch tasks at gajim / python-nbxmpp
Commits:
-
190ce1ec
by lovetox at 2020-09-22T23:21:15+02:00
-
8bd67f6f
by lovetox at 2020-09-22T23:21:15+02:00
-
192e8093
by lovetox at 2020-09-22T23:21:15+02:00
-
ddb5134f
by lovetox at 2020-09-22T23:21:15+02:00
-
397d7c5a
by lovetox at 2020-09-22T23:21:15+02:00
-
1b5bfca9
by lovetox at 2020-09-22T23:28:00+02:00
-
b1cb21dd
by lovetox at 2020-09-22T23:36:19+02:00
-
13022658
by lovetox at 2020-09-22T23:37:05+02:00
-
4d55d9a2
by lovetox at 2020-09-23T13:15:40+02:00
22 changed files:
- nbxmpp/client.py
- nbxmpp/connection.py
- nbxmpp/dispatcher.py
- + nbxmpp/errors.py
- nbxmpp/modules/activity.py
- nbxmpp/modules/base.py
- nbxmpp/modules/bookmarks.py
- nbxmpp/modules/location.py
- nbxmpp/modules/mood.py
- nbxmpp/modules/nickname.py
- nbxmpp/modules/omemo.py
- nbxmpp/modules/openpgp.py
- nbxmpp/modules/pubsub.py
- nbxmpp/modules/tune.py
- nbxmpp/modules/user_avatar.py
- + nbxmpp/modules/util.py
- + nbxmpp/modules/vcard_temp.py
- nbxmpp/namespaces.py
- nbxmpp/protocol.py
- nbxmpp/structs.py
- + nbxmpp/task.py
- test/unit/test_avatar.py
Changes:
| ... | ... | @@ -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
|
| ... | ... | @@ -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
|
| ... | ... | @@ -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:
|
| 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'
|
| ... | ... | @@ -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')
|
| ... | ... | @@ -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)
|
| ... | ... | @@ -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):
|
| ... | ... | @@ -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')
|
| ... | ... | @@ -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')
|
| ... | ... | @@ -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')
|
| ... | ... | @@ -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):
|
| ... | ... | @@ -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):
|
| 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')
|
| ... | ... | @@ -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')
|
| ... | ... | @@ -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
|
| 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
|
| 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
|
| ... | ... | @@ -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'
|
| ... | ... | @@ -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
|
| ... | ... | @@ -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 |
|
| 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()
|
| ... | ... | @@ -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)
|