Philipp Hörist pushed to branch master at gajim / python-nbxmpp
Commits: 550decfd by Philipp Hörist at 2019-09-28T23:12:58Z Add Bookmarks 2 (XEP-0402) support
- - - - -
5 changed files:
- nbxmpp/const.py - nbxmpp/modules/bookmarks.py - nbxmpp/modules/pubsub.py - nbxmpp/protocol.py - nbxmpp/structs.py
Changes:
===================================== nbxmpp/const.py ===================================== @@ -310,7 +310,8 @@ Role._WEIGHTS = {
class BookmarkStoreType(Enum): - PUBSUB = 'pubsub' + PUBSUB_BOOKMARK_2 = 'pubsub bookmark 2' + PUBSUB_BOOKMARK_1 = 'pubsub bookmark 1' PRIVATE = 'private'
===================================== 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 = [] @@ -111,6 +163,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') @@ -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):
===================================== 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) @@ -91,6 +104,87 @@ class PubSub: publish.addChild(node=options) 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): @@ -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
===================================== nbxmpp/protocol.py ===================================== @@ -51,6 +51,8 @@ NS_BIND = 'urn:ietf:params:xml:ns:xmpp-bind' NS_BLOCKING = 'urn:xmpp:blocking' # XEP-0191 NS_BOB = 'urn:xmpp:bob' # XEP-0231 NS_BOOKMARKS = 'storage:bookmarks' # XEP-0048 +NS_BOOKMARKS_2 = 'urn:xmpp:bookmarks:0' # XEP-0402 +NS_BOOKMARKS_COMPAT = 'urn:xmpp:bookmarks:0#compat' # XEP-0402 NS_BROWSE = 'jabber:iq:browse' NS_BROWSING = 'http://jabber.org/protocol/browsing' # XEP-0195 NS_BYTESTREAM = 'http://jabber.org/protocol/bytestreams' # XEP-0065 @@ -136,6 +138,7 @@ NS_PUBSUB = 'http://jabber.org/protocol/pubsub' # XEP-0060 NS_PUBSUB_EVENT = 'http://jabber.org/protocol/pubsub#event' NS_PUBSUB_PUBLISH_OPTIONS = NS_PUBSUB + '#publish-options' # XEP-0060 NS_PUBSUB_OWNER = 'http://jabber.org/protocol/pubsub#owner' # XEP-0060 +NS_PUBSUB_CONFIG = 'http://jabber.org/protocol/pubsub#node_config' # XEP-0060 NS_REGISTER = 'jabber:iq:register' NS_ROSTER = 'jabber:iq:roster' NS_ROSTERNOTES = 'storage:rosternotes'
===================================== nbxmpp/structs.py ===================================== @@ -70,8 +70,12 @@ HTTPAuthData.__new__.__defaults__ = (None, None, None, None) StanzaIDData = namedtuple('StanzaIDData', 'id by') StanzaIDData.__new__.__defaults__ = (None, None)
-PubSubEventData = namedtuple('PubSubEventData', 'node id item data empty deleted retracted') -PubSubEventData.__new__.__defaults__ = (None, None, None, False, False, False) +PubSubEventData = namedtuple('PubSubEventData', 'node id item data empty deleted retracted purged') +PubSubEventData.__new__.__defaults__ = (None, None, None, False, False, False, False) + +PubSubConfigResult = namedtuple('PubSubConfigResult', 'jid node form') + +PubSubPublishResult = namedtuple('PubSubPublishResult', 'jid node id')
MoodData = namedtuple('MoodData', 'mood text')
View it on GitLab: https://dev.gajim.org/gajim/python-nbxmpp/commit/550decfd858f91fa5ada54ea047...