diff options
author | Philipp Hörist <philipp@hoerist.com> | 2019-09-28 01:37:26 +0300 |
---|---|---|
committer | Philipp Hörist <philipp@hoerist.com> | 2019-09-29 02:12:58 +0300 |
commit | 550decfd858f91fa5ada54ea0473da4831d85d05 (patch) | |
tree | 139fcd8f3cd219c5ae07ccab39c9a08b1d90b422 /nbxmpp/modules | |
parent | 589c0417455010c0ba1a41aae203b3e3ded942d5 (diff) |
Add Bookmarks 2 (XEP-0402) support
Diffstat (limited to 'nbxmpp/modules')
-rw-r--r-- | nbxmpp/modules/bookmarks.py | 249 | ||||
-rw-r--r-- | nbxmpp/modules/pubsub.py | 134 |
2 files changed, 350 insertions, 33 deletions
diff --git a/nbxmpp/modules/bookmarks.py b/nbxmpp/modules/bookmarks.py index d19e9f7..30fb9a2 100644 --- a/nbxmpp/modules/bookmarks.py +++ b/nbxmpp/modules/bookmarks.py @@ -18,6 +18,7 @@ import logging from nbxmpp.protocol import NS_BOOKMARKS +from nbxmpp.protocol import NS_BOOKMARKS_2 from nbxmpp.protocol import NS_PUBSUB_EVENT from nbxmpp.protocol import NS_PRIVATE from nbxmpp.protocol import isResultNode @@ -32,13 +33,31 @@ from nbxmpp.util import to_xs_boolean from nbxmpp.util import call_on_response from nbxmpp.util import callback from nbxmpp.util import raise_error +from nbxmpp.util import is_error_result from nbxmpp.modules.pubsub import get_pubsub_item +from nbxmpp.modules.pubsub import get_pubsub_items from nbxmpp.modules.pubsub import get_pubsub_request -from nbxmpp.modules.pubsub import get_bookmark_publish_options +from nbxmpp.modules.pubsub import get_publish_options + log = logging.getLogger('nbxmpp.m.bookmarks') +BOOKMARK_1_OPTIONS = { + 'pubsub#persist_items': 'true', + 'pubsub#access_model': 'whitelist', +} + +BOOKMARK_2_OPTIONS = { + 'pubsub#notify_delete': 'true', + 'pubsub#notify_retract': 'true', + 'pubsub#persist_items': 'true', + 'pubsub#max_items': '128', + 'pubsub#access_model': 'whitelist', + 'pubsub#send_last_published_item': 'never', +} + + class Bookmarks: def __init__(self, client): self._client = client @@ -47,8 +66,17 @@ class Bookmarks: callback=self._process_pubsub_bookmarks, ns=NS_PUBSUB_EVENT, priority=16), + StanzaHandler(name='message', + callback=self._process_pubsub_bookmarks2, + ns=NS_PUBSUB_EVENT, + priority=16), ] + self._bookmark_2_queue = {} + self._bookmark_1_queue = [] + self._node_configuration_in_progress = False + self._node_configuration_not_possible = False + def _process_pubsub_bookmarks(self, _con, _stanza, properties): if not properties.is_pubsub_event: return @@ -78,6 +106,30 @@ class Bookmarks: properties.pubsub_event = pubsub_event + def _process_pubsub_bookmarks2(self, _con, _stanza, properties): + if not properties.is_pubsub_event: + return + + if properties.pubsub_event.node != NS_BOOKMARKS_2: + return + + if properties.pubsub_event.deleted or properties.pubsub_event.retracted: + return + + item = properties.pubsub_event.item + if item is None: + return + + bookmark_item = self._parse_bookmarks2(item) + if bookmark_item is None: + return + + pubsub_event = properties.pubsub_event._replace(data=bookmark_item) + log.info('Received bookmark item from: %s', properties.jid) + log.info(bookmark_item) + + properties.pubsub_event = pubsub_event + @staticmethod def _parse_bookmarks(storage): bookmarks = [] @@ -112,6 +164,35 @@ class Bookmarks: return bookmarks @staticmethod + def _parse_bookmarks2(item): + jid = item.getAttr('id') + if jid is None: + log.warning('No id attr found') + return + + try: + jid = JID(jid) + except Exception as error: + log.warning('Invalid JID: %s', error) + log.warning(item) + return + + conference = item.getTag('conference', namespace=NS_BOOKMARKS_2) + if conference is None: + log.warning('No conference node found') + log.warning(item) + return + + autojoin = conference.getAttr('autojoin') == 'true' + name = conference.getAttr('name') + nick = conference.getTagData('nick') + + return BookmarkData(jid=jid, + name=name or None, + autojoin=autojoin, + nick=nick or None) + + @staticmethod def get_private_request(): iq = Iq(typ='get') query = iq.addChild(name='query', namespace=NS_PRIVATE) @@ -121,10 +202,16 @@ class Bookmarks: @call_on_response('_bookmarks_received') def request_bookmarks(self, type_): jid = self._client.get_bound_jid().getBare() - if type_ == BookmarkStoreType.PUBSUB: + if type_ == BookmarkStoreType.PUBSUB_BOOKMARK_2: + log.info('Request bookmarks 2 (PubSub)') + request = get_pubsub_request(jid, NS_BOOKMARKS_2) + return request, {'type_': type_} + + if type_ == BookmarkStoreType.PUBSUB_BOOKMARK_1: log.info('Request bookmarks (PubSub)') request = get_pubsub_request(jid, NS_BOOKMARKS, max_items=1) return request, {'type_': type_} + if type_ == BookmarkStoreType.PRIVATE: log.info('Request bookmarks (Private Storage)') return self.get_private_request(), {'type_': type_} @@ -135,16 +222,27 @@ class Bookmarks: return raise_error(log.info, stanza) bookmarks = [] - if type_ == BookmarkStoreType.PUBSUB: + if type_ == BookmarkStoreType.PUBSUB_BOOKMARK_2: + items = get_pubsub_items(stanza, NS_BOOKMARKS_2) + if items is None: + return bookmarks + for item in items: + bookmark_item = self._parse_bookmarks2(item) + if bookmark_item is not None: + bookmarks.append(bookmark_item) + + elif type_ == BookmarkStoreType.PUBSUB_BOOKMARK_1: item = get_pubsub_item(stanza) - storage_node = item.getTag('storage', namespace=NS_BOOKMARKS) + if item is not None: + storage_node = item.getTag('storage', namespace=NS_BOOKMARKS) + if storage_node.getChildren(): + bookmarks = self._parse_bookmarks(storage_node) - if type_ == BookmarkStoreType.PRIVATE: + elif type_ == BookmarkStoreType.PRIVATE: query = stanza.getQuery() storage_node = query.getTag('storage', namespace=NS_BOOKMARKS) - - if storage_node.getChildren(): - bookmarks = self._parse_bookmarks(storage_node) + if storage_node.getChildren(): + bookmarks = self._parse_bookmarks(storage_node) from_ = stanza.getFrom() if from_ is None: @@ -169,22 +267,137 @@ class Bookmarks: conf_node.setTagData('password', bookmark.password) return storage_node + @staticmethod + def _build_conference_node(bookmark): + attrs = {'xmlns': NS_BOOKMARKS_2} + if bookmark.autojoin: + attrs['autojoin'] = 'true' + if bookmark.name: + attrs['name'] = bookmark.name + conference = Node(tag='conference', attrs=attrs) + if bookmark.nick: + conference.setTagData('nick', bookmark.nick) + return conference + def store_bookmarks(self, bookmarks, type_): - if type_ == BookmarkStoreType.PUBSUB: - self._store_with_pubsub(bookmarks) + if type_ == BookmarkStoreType.PUBSUB_BOOKMARK_2: + self._store_bookmark_2(bookmarks) + elif type_ == BookmarkStoreType.PUBSUB_BOOKMARK_1: + self._store_bookmark_1(bookmarks) elif type_ == BookmarkStoreType.PRIVATE: self._store_with_private(bookmarks) - def _store_with_pubsub(self, bookmarks): - log.info('Store Bookmarks (PubSub)') + def retract_bookmark(self, bookmark_jid): + log.info('Retract Bookmark: %s', bookmark_jid) + jid = self._client.get_bound_jid().getBare() + self._client.get_module('PubSub').retract(jid, + NS_BOOKMARKS_2, + str(bookmark_jid)) + + def _store_bookmark_1(self, bookmarks): + log.info('Store Bookmarks 1 (PubSub)') jid = self._client.get_bound_jid().getBare() + self._bookmark_1_queue = bookmarks item = self._build_storage_node(bookmarks) - options = get_bookmark_publish_options() - self._client.get_module('PubSub').publish(jid, - NS_BOOKMARKS, - item, - id_='current', - options=options) + options = get_publish_options(BOOKMARK_1_OPTIONS) + self._client.get_module('PubSub').publish( + jid, + NS_BOOKMARKS, + item, + id_='current', + options=options, + callback=self._on_store_bookmark_result, + user_data=NS_BOOKMARKS) + + def _store_bookmark_2(self, bookmarks): + if self._node_configuration_not_possible: + log.warning('Node configuration not possible') + return + + log.info('Store Bookmarks 2 (PubSub)') + jid = self._client.get_bound_jid().getBare() + for bookmark in bookmarks: + self._bookmark_2_queue[bookmark.jid] = bookmark + item = self._build_conference_node(bookmark) + options = get_publish_options(BOOKMARK_2_OPTIONS) + self._client.get_module('PubSub').publish( + jid, + NS_BOOKMARKS_2, + item, + id_=str(bookmark.jid), + options=options, + callback=self._on_store_bookmark_result, + user_data=NS_BOOKMARKS_2) + + def _on_store_bookmark_result(self, result, node): + if not is_error_result(result): + self._bookmark_1_queue = [] + self._bookmark_2_queue.pop(result.id, None) + return + + if (result.condition == 'conflict' and + result.app_condition == 'precondition-not-met'): + if self._node_configuration_in_progress: + return + + self._node_configuration_in_progress = True + jid = self._client.get_bound_jid().getBare() + self._client.get_module('PubSub').get_node_configuration( + jid, + node, + callback=self._on_node_configuration_received) + + else: + self._bookmark_1_queue = [] + self._bookmark_2_queue.pop(result.id, None) + log.warning(result) + + def _on_node_configuration_received(self, result): + if is_error_result(result): + log.warning(result) + self._bookmark_1_queue = [] + self._bookmark_2_queue.clear() + return + + if result.node == NS_BOOKMARKS: + config = BOOKMARK_1_OPTIONS + else: + config = BOOKMARK_2_OPTIONS + self._apply_config(result.form, config) + self._client.get_module('PubSub').set_node_configuration( + result.jid, + result.node, + result.form, + callback=self._on_node_configuration_finished) + + def _on_node_configuration_finished(self, result): + self._node_configuration_in_progress = False + if is_error_result(result): + log.warning(result) + self._bookmark_2_queue.clear() + self._bookmark_1_queue = [] + self._node_configuration_not_possible = True + return + + log.info('Republish bookmarks') + if self._bookmark_2_queue: + bookmarks = self._bookmark_2_queue.copy() + self._bookmark_2_queue.clear() + self._store_bookmark_2(bookmarks.values()) + else: + bookmarks = self._bookmark_1_queue.copy() + self._bookmark_1_queue.clear() + self._store_bookmark_1(bookmarks) + + @staticmethod + def _apply_config(form, config): + for var, value in config.items(): + try: + field = form[var] + except KeyError: + pass + else: + field.value = value @call_on_response('_on_private_store_result') def _store_with_private(self, bookmarks): diff --git a/nbxmpp/modules/pubsub.py b/nbxmpp/modules/pubsub.py index 8a5a3e2..b1f9edd 100644 --- a/nbxmpp/modules/pubsub.py +++ b/nbxmpp/modules/pubsub.py @@ -20,6 +20,8 @@ import logging from nbxmpp.protocol import NS_PUBSUB from nbxmpp.protocol import NS_PUBSUB_EVENT from nbxmpp.protocol import NS_PUBSUB_PUBLISH_OPTIONS +from nbxmpp.protocol import NS_PUBSUB_OWNER +from nbxmpp.protocol import NS_PUBSUB_CONFIG from nbxmpp.protocol import NS_DATA from nbxmpp.protocol import Node from nbxmpp.protocol import Iq @@ -27,6 +29,9 @@ from nbxmpp.protocol import isResultNode from nbxmpp.structs import StanzaHandler from nbxmpp.structs import PubSubEventData from nbxmpp.structs import CommonResult +from nbxmpp.structs import PubSubConfigResult +from nbxmpp.structs import PubSubPublishResult +from nbxmpp.modules.dataforms import extend_form from nbxmpp.util import call_on_response from nbxmpp.util import callback from nbxmpp.util import raise_error @@ -56,28 +61,36 @@ class PubSub: node, empty=True, deleted=True) return - retract = event.getTag('retract') - if retract is not None: - node = retract.getAttr('node') - item = retract.getTag('item') - id_ = item.getAttr('id') - properties.pubsub_event = PubSubEventData( - node, id_, item, retracted=True) + purge = event.getTag('purge') + if purge is not None: + node = purge.getAttr('node') + item = purge.getTag('item') + properties.pubsub_event = PubSubEventData(node, purged=True) return items = event.getTag('items') if items is not None: + node = items.getAttr('node') + + retract = items.getTag('retract') + if retract is not None: + id_ = retract.getAttr('id') + properties.pubsub_event = PubSubEventData( + node, id_, retracted=True) + return + if len(items.getChildren()) != 1: log.warning('PubSub event with more than one item') log.warning(stanza) - node = items.getAttr('node') + return + item = items.getTag('item') if item is None: return id_ = item.getAttr('id') properties.pubsub_event = PubSubEventData(node, id_, item) - @call_on_response('_default_response') + @call_on_response('_publish_result_received') def publish(self, jid, node, item, id_=None, options=None): query = Iq('set', to=jid) pubsub = query.addChild('pubsub', namespace=NS_PUBSUB) @@ -92,6 +105,87 @@ class PubSub: return query @callback + def _publish_result_received(self, stanza): + if not isResultNode(stanza): + return raise_error(log.warning, stanza) + + jid = stanza.getFrom() + pubsub = stanza.getTag('pubsub', namespace=NS_PUBSUB) + if pubsub is None: + return raise_error(log.warning, stanza, 'stanza-malformed') + + publish = pubsub.getTag('publish') + if publish is None: + return raise_error(log.warning, stanza, 'stanza-malformed') + + node = publish.getAttr('node') + item = publish.getTag('item') + if item is None: + return raise_error(log.warning, stanza, 'stanza-malformed') + + id_ = item.getAttr('id') + return PubSubPublishResult(jid, node, id_) + + @call_on_response('_default_response') + def retract(self, jid, node, id_, notify=True): + query = Iq('set', to=jid) + pubsub = query.addChild('pubsub', namespace=NS_PUBSUB) + attrs = {'node': node} + if notify: + attrs['notify'] = 'true' + retract = pubsub.addChild('retract', attrs=attrs) + retract.addChild('item', {'id': id_}) + return query + + @call_on_response('_default_response') + def set_node_configuration(self, jid, node, form): + log.info('Set configuration for %s %s', node, jid) + query = Iq('set', to=jid) + pubsub = query.addChild('pubsub', namespace=NS_PUBSUB_OWNER) + configure = pubsub.addChild('configure', {'node': node}) + form.setAttr('type', 'submit') + configure.addChild(node=form) + return query + + @call_on_response('_node_configuration_received') + def get_node_configuration(self, jid, node): + log.info('Request node configuration') + query = Iq('get', to=jid) + pubsub = query.addChild('pubsub', namespace=NS_PUBSUB_OWNER) + pubsub.addChild('configure', {'node': node}) + return query + + @callback + def _node_configuration_received(self, stanza): + if not isResultNode(stanza): + return raise_error(log.warning, stanza) + + jid = stanza.getFrom() + pubsub = stanza.getTag('pubsub', namespace=NS_PUBSUB_OWNER) + if pubsub is None: + return raise_error(log.warning, stanza, 'stanza-malformed', + 'No pubsub node found') + + configure = pubsub.getTag('configure') + if configure is None: + return raise_error(log.warning, stanza, 'stanza-malformed', + 'No configure node found') + + node = configure.getAttr('node') + + forms = configure.getTags('x', namespace=NS_DATA) + for form in forms: + dataform = extend_form(node=form) + form_type = dataform.vars.get('FORM_TYPE') + if form_type is None or form_type.value != NS_PUBSUB_CONFIG: + continue + log.info('Node configuration received from: %s', jid) + return PubSubConfigResult(jid=jid, node=node, form=dataform) + + return raise_error(log.warning, stanza, 'stanza-malformed', + 'No valid form type found') + + @callback def _default_response(self, stanza): if not isResultNode(stanza): return raise_error(log.info, stanza) @@ -115,13 +209,23 @@ def get_pubsub_item(stanza): return items_node.getTag('item') -def get_bookmark_publish_options(): - # TODO: Make generic - options = Node(NS_DATA + ' x', - attrs={'type': 'submit'}) +def get_pubsub_items(stanza, node=None): + pubsub_node = stanza.getTag('pubsub') + items_node = pubsub_node.getTag('items') + if node is not None and items_node.getAttr('node') != node: + return + + if items_node is not None: + return items_node.getTags('item') + + +def get_publish_options(config): + options = Node(NS_DATA + ' x', attrs={'type': 'submit'}) field = options.addChild('field', attrs={'var': 'FORM_TYPE', 'type': 'hidden'}) field.setTagData('value', NS_PUBSUB_PUBLISH_OPTIONS) - field = options.addChild('field', attrs={'var': 'pubsub#access_model'}) - field.setTagData('value', 'whitelist') + + for var, value in config.items(): + field = options.addChild('field', attrs={'var': var}) + field.setTagData('value', value) return options |