Philipp Hörist pushed to branch master at gajim / python-nbxmpp
Commits: d142c6f0 by Philipp Hörist at 2019-02-04T20:57:54Z Add Bookmarks (XEP-0048) module
- - - - -
6 changed files:
- nbxmpp/const.py - nbxmpp/dispatcher_nb.py - + nbxmpp/modules/bookmarks.py - nbxmpp/modules/pubsub.py - nbxmpp/structs.py - nbxmpp/util.py
Changes:
===================================== nbxmpp/const.py ===================================== @@ -285,6 +285,11 @@ class Error(Enum): return self.value
+class BookmarkStoreType(Enum): + PUBSUB = 'pubsub' + PRIVATE = 'private' + + MOODS = [ 'afraid', 'amazed',
===================================== nbxmpp/dispatcher_nb.py ===================================== @@ -62,6 +62,7 @@ from nbxmpp.modules.tune import Tune from nbxmpp.modules.mood import Mood from nbxmpp.modules.location import Location from nbxmpp.modules.user_avatar import UserAvatar +from nbxmpp.modules.bookmarks import Bookmarks from nbxmpp.modules.misc import unwrap_carbon from nbxmpp.modules.misc import unwrap_mam from nbxmpp.util import get_properties_struct @@ -191,6 +192,7 @@ class XMPPDispatcher(PlugIn): self._modules['Tune'] = Tune(self._owner) self._modules['Location'] = Location(self._owner) self._modules['UserAvatar'] = UserAvatar(self._owner) + self._modules['Bookmarks'] = Bookmarks(self._owner)
for instance in self._modules.values(): for handler in instance.handlers:
===================================== nbxmpp/modules/bookmarks.py ===================================== @@ -0,0 +1,190 @@ +# Copyright (C) 2018 Philipp Hörist <philipp AT hoerist.com> +# +# This file is part of nbxmpp. +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 3 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; If not, see http://www.gnu.org/licenses/. + +import logging + +from nbxmpp.protocol import NS_BOOKMARKS +from nbxmpp.protocol import NS_PUBSUB_EVENT +from nbxmpp.protocol import NS_PRIVATE +from nbxmpp.protocol import isResultNode +from nbxmpp.protocol import Node +from nbxmpp.protocol import Iq +from nbxmpp.structs import StanzaHandler +from nbxmpp.structs import BookmarkData +from nbxmpp.const import BookmarkStoreType +from nbxmpp.util import from_xs_boolean +from nbxmpp.util import to_xs_boolean +from nbxmpp.util import validate_jid +from nbxmpp.util import call_on_response +from nbxmpp.util import callback +from nbxmpp.modules.pubsub import get_pubsub_item +from nbxmpp.modules.pubsub import get_pubsub_request +from nbxmpp.modules.pubsub import get_bookmark_publish_options + +log = logging.getLogger('nbxmpp.m.bookmarks') + + +class Bookmarks: + def __init__(self, client): + self._client = client + self.handlers = [ + StanzaHandler(name='message', + callback=self._process_pubsub_bookmarks, + ns=NS_PUBSUB_EVENT, + priority=16), + ] + + def _process_pubsub_bookmarks(self, _con, _stanza, properties): + if not properties.is_pubsub_event: + return + + if properties.pubsub_event.node != NS_BOOKMARKS: + return + + item = properties.pubsub_event.item + + bookmarks = [] + storage_node = item.getTag('storage', namespace=NS_BOOKMARKS) + if storage_node.getChildren(): + bookmarks = self._parse_bookmarks(storage_node) + + pubsub_event = properties.pubsub_event._replace(data=bookmarks) + log.info('Received bookmarks from: %s', properties.jid) + for bookmark in bookmarks: + log.info(bookmark) + + properties.pubsub_event = pubsub_event + + @staticmethod + def _parse_bookmarks(storage): + bookmarks = [] + confs = storage.getTags('conference') + for conf in confs: + autojoin = conf.getAttr('autojoin') + if autojoin is None: + autojoin = False + else: + try: + autojoin = from_xs_boolean(autojoin) + except ValueError as error: + log.warning(error) + log.warning(storage) + autojoin = False + + try: + jid = validate_jid(conf.getAttr('jid')) + except Exception: + log.warning('Invalid JID: %s, ignoring it', + conf.getAttr('jid')) + continue + + bookmark = BookmarkData( + jid=jid, + name=conf.getAttr('name'), + autojoin=autojoin, + password=conf.getTagData('password'), + nick=conf.getTagData('nick')) + bookmarks.append(bookmark) + + return bookmarks + + @staticmethod + def get_private_request(): + iq = Iq(typ='get') + query = iq.addChild(name='query', namespace=NS_PRIVATE) + query.addChild(name='storage', namespace=NS_BOOKMARKS) + return iq + + @call_on_response('_bookmarks_received') + def request_bookmarks(self, type_): + jid = self._client.get_bound_jid().getBare() + if type_ == BookmarkStoreType.PUBSUB: + 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_} + + @callback + def _bookmarks_received(self, stanza, type_): + if not isResultNode(stanza): + log.info('Error: %s', stanza.getError()) + return [] + + if type_ == BookmarkStoreType.PUBSUB: + item = get_pubsub_item(stanza) + bookmarks = [] + storage_node = item.getTag('storage', namespace=NS_BOOKMARKS) + + if type_ == BookmarkStoreType.PRIVATE: + query = stanza.getQuery() + storage_node = query.getTag('storage', namespace=NS_BOOKMARKS) + + if storage_node.getChildren(): + bookmarks = self._parse_bookmarks(storage_node) + + from_ = stanza.getFrom() + if from_ is None: + from_ = self._client.get_bound_jid().getBare() + log.info('Received bookmarks from: %s', from_) + for bookmark in bookmarks: + log.info(bookmark) + return bookmarks + + @staticmethod + def _build_storage_node(bookmarks): + storage_node = Node(tag='storage', attrs={'xmlns': NS_BOOKMARKS}) + for bookmark in bookmarks: + conf_node = storage_node.addChild(name="conference") + conf_node.setAttr('jid', bookmark.jid) + conf_node.setAttr('autojoin', to_xs_boolean(bookmark.autojoin)) + if bookmark.name: + conf_node.setAttr('name', bookmark.name) + if bookmark.nick: + conf_node.setTagData('nick', bookmark.nick) + if bookmark.password: + conf_node.setTagData('password', bookmark.password) + return storage_node + + def store_bookmarks(self, bookmarks, type_): + if type_ == BookmarkStoreType.PUBSUB: + self._store_with_pubsub(bookmarks) + elif type_ == BookmarkStoreType.PRIVATE: + self._store_with_private(bookmarks) + + def _store_with_pubsub(self, bookmarks): + log.info('Store Bookmarks (PubSub)') + jid = self._client.get_bound_jid().getBare() + 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) + + @call_on_response('_on_private_store_result') + def _store_with_private(self, bookmarks): + log.info('Store Bookmarks (Private Storage)') + storage_node = self._build_storage_node(bookmarks) + return Iq('set', NS_PRIVATE, payload=storage_node) + + @staticmethod + def _on_private_store_result(_con, stanza): + if not isResultNode(stanza): + log.info('Error: %s', stanza.getError())
===================================== nbxmpp/modules/pubsub.py ===================================== @@ -19,6 +19,9 @@ 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_DATA +from nbxmpp.protocol import Node from nbxmpp.protocol import Iq from nbxmpp.protocol import isResultNode from nbxmpp.structs import StanzaHandler @@ -87,3 +90,21 @@ def get_pubsub_request(jid, node, id_=None, max_items=None): if id_ is not None: items.addChild('item', {'id': id_}) return query + + +def get_pubsub_item(stanza): + pubsub_node = stanza.getTag('pubsub') + items_node = pubsub_node.getTag('items') + return items_node.getTag('item') + + +def get_bookmark_publish_options(): + # TODO: Make generic + 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') + return options
===================================== nbxmpp/structs.py ===================================== @@ -74,6 +74,9 @@ AvatarMetaData.__new__.__defaults__ = (None,) * len(AvatarMetaData._fields) AvatarData = namedtuple('AvatarData', 'jid sha data') AvatarData.__new__.__defaults__ = (None,) * len(AvatarData._fields)
+BookmarkData = namedtuple('BookmarkData', 'jid name nick autojoin password') +BookmarkData.__new__.__defaults__ = (None, None, None, None) +
class TuneData(namedtuple('TuneData', 'artist length rating source title track uri')):
===================================== nbxmpp/util.py ===================================== @@ -159,11 +159,35 @@ def callback(func): @wraps(func) def func_wrapper(self, _con, stanza, **kwargs): cb = kwargs.pop('callback', None) - if cb is None: - return
- if cb() is not None: - result = func(self, stanza, **kwargs) + result = func(self, stanza, **kwargs) + if cb is not None and cb() is not None: cb()(result)
return func_wrapper + + +def from_xs_boolean(value): + if value in ('1', 'true', 'True'): + return True + + if value in ('0', 'false', 'False', ''): + return False + + raise ValueError('Cant convert %s to python boolean' % value) + + +def to_xs_boolean(value): + # Convert to xs:boolean ('true', 'false') + # from a python boolean (True, False) or None + if value is True: + return 'true' + + if value is False: + return 'false' + + if value is None: + return 'false' + + raise ValueError( + 'Cant convert %s to xs:boolean' % value)
View it on GitLab: https://dev.gajim.org/gajim/python-nbxmpp/commit/d142c6f0901f5173c2f5037cb39...