Welcome to mirror list, hosted at ThFree Co, Russian Federation.

dev.gajim.org/gajim/python-nbxmpp.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPhilipp Hörist <philipp@hoerist.com>2019-09-28 01:37:26 +0300
committerPhilipp Hörist <philipp@hoerist.com>2019-09-29 02:12:58 +0300
commit550decfd858f91fa5ada54ea0473da4831d85d05 (patch)
tree139fcd8f3cd219c5ae07ccab39c9a08b1d90b422 /nbxmpp/modules
parent589c0417455010c0ba1a41aae203b3e3ded942d5 (diff)
Add Bookmarks 2 (XEP-0402) support
Diffstat (limited to 'nbxmpp/modules')
-rw-r--r--nbxmpp/modules/bookmarks.py249
-rw-r--r--nbxmpp/modules/pubsub.py134
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