# HG changeset patch # User Goffi # Date 1396880555 -7200 # Node ID 0e80ee1fe9afabb8071b70450e9ab4b6c57fec4a # Parent 58a57ce5932a93577749d6a3321188f897f06049 plugin XEP-0048: bookmarks (first draft) diff -r 58a57ce5932a -r 0e80ee1fe9af src/plugins/plugin_xep_0048.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/plugins/plugin_xep_0048.py Mon Apr 07 16:22:35 2014 +0200 @@ -0,0 +1,280 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# SAT plugin for Bookmarks (xep-0048) +# Copyright (C) 2009, 2010, 2011, 2012, 2013, 2014 Jérôme Poisson (goffi@goffi.org) + +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero 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 Affero General Public License for more details. + +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +from sat.core.i18n import _, D_ +from sat.core import exceptions +from sat.memory.persistent import PersistentBinaryDict +from sat.tools import xml_tools +from logging import debug, info, warning, error +from twisted.words.xish import domish +from twisted.words.protocols.jabber import jid + +from twisted.internet import defer + +NS_BOOKMARKS = 'storage:bookmarks' + +PLUGIN_INFO = { + "name": "Bookmarks", + "import_name": "XEP-0048", + "type": "XEP", + "protocols": ["XEP-0048"], + "dependencies": ["XEP-0045"], + "recommendations": ["XEP-0049"], + "main": "XEP_0048", + "handler": "no", + "description": _("""Implementation of bookmarks""") +} + + +class XEP_0048(object): + MUC_TYPE = 'muc' + URL_TYPE = 'url' + MUC_KEY = 'jid' + URL_KEY = 'url' + MUC_ATTRS = ('autojoin', 'name') + URL_ATTRS = ('name',) + + def __init__(self, host): + info(_("Bookmarks plugin initialization")) + self.host = host + # self.__menu_id = host.registerCallback(self._bookmarksMenu, with_data=True) + self.__bm_save_id = host.registerCallback(self._bookmarksSaveCb, with_data=True) + host.importMenu((D_("Communication"), D_("bookmarks")), self._bookmarksMenu, security_limit=0, help_string=D_("Use and manage bookmarks")) + try: + self.private_plg = self.host.plugins["XEP-0049"] + except KeyError: + self.private_plg = None + + @defer.inlineCallbacks + def profileConnected(self, profile): + client = self.host.getClient(profile) + local = client.bookmarks_local = PersistentBinaryDict(NS_BOOKMARKS, profile) + yield local.load() + if not local: + local[XEP_0048.MUC_TYPE] = dict() + local[XEP_0048.URL_TYPE] = dict() + private = yield self._getServerBookmarks('private', profile) + pubsub = client.bookmarks_pubsub = None + + for bookmarks in (local, private, pubsub): + if bookmarks is not None: + for (room_jid, data) in bookmarks[XEP_0048.MUC_TYPE].items(): + if data.get('autojoin', 'false') == 'true': + nick = data.get('nick', client.jid.user) + self.host.plugins['XEP-0045'].join(room_jid, nick, {}, profile_key=client.profile) + + @defer.inlineCallbacks + def _getServerBookmarks(self, storage_type, profile): + """Get distants bookmarks + + update also the client.bookmarks_[type] key, with None if service is not available + @param storage_type: storage type, can be: + - 'private': XEP-0049 storage + - 'pubsub': XEP-0223 storage + @param profile: %(doc_profile)s + @return: data dictionary, or None if feature is not available + + @raise: exception.FeatureNotFound + """ + client = self.host.getClient(profile) + if storage_type == 'private': + try: + bookmarks_private_xml = yield self.private_plg.privateXMLGet('storage', NS_BOOKMARKS, profile) + data = client.bookmarks_private = self._bookmarkElt2Dict(bookmarks_private_xml) + except (exceptions.FeatureNotFound, AttributeError): + info(_("Private XML storage not available")) + data = client.bookmarks_private = None + elif storage_type == 'pubsub': + raise NotImplementedError + else: + raise ValueError("storage_type must be 'private' or 'pubsub'") + defer.returnValue(data) + + @defer.inlineCallbacks + def _setServerBookmarks(self, storage_type, bookmarks_elt, profile): + """Save bookmarks on server + + @param storage_type: storage type, can be: + - 'private': XEP-0049 storage + - 'pubsub': XEP-0223 storage + @param bookmarks_elt (domish.Element): bookmarks XML + @param profile: %(doc_profile)s + """ + if storage_type == 'private': + yield self.private_plg.privateXMLStore(bookmarks_elt, profile) + elif storage_type == 'pubsub': + raise NotImplementedError + else: + raise ValueError("storage_type must be 'private' or 'pubsub'") + + def _bookmarkElt2Dict(self, storage_elt): + """Parse bookmarks to get dictionary + @param storage_elt (domish.Element): bookmarks storage + @return (dict): bookmark data (key: bookmark type, value: list) where key can be: + - XEP_0048.MUC_TYPE + - XEP_0048.URL_TYPE + - value (dict): data as for addBookmark + """ + conf_data = {} + url_data = {} + + conference_elts = storage_elt.elements(NS_BOOKMARKS, 'conference') + for conference_elt in conference_elts: + try: + room_jid = jid.JID(conference_elt[XEP_0048.MUC_KEY]) + except KeyError: + warning ("invalid bookmark found, igoring it:\n%s" % conference_elt.toXml()) + continue + + data = conf_data[room_jid] = {} + + for attr in XEP_0048.MUC_ATTRS: + if conference_elt.hasAttribute(attr): + data[attr] = conference_elt[attr] + try: + data['nick'] = unicode(conference_elt.elements(NS_BOOKMARKS, 'nick').next()) + except StopIteration: + pass + # TODO: manage password (need to be secured, see XEP-0049 §4) + + url_elts = storage_elt.elements(NS_BOOKMARKS, 'url') + for url_elt in url_elts: + try: + url = url_elt[XEP_0048.URL_KEY] + except KeyError: + warning ("invalid bookmark found, igoring it:\n%s" % url_elt.toXml()) + continue + data = url_data[url] = {} + for attr in XEP_0048.URL_ATTRS: + if url_elt.hasAttribute(attr): + data[attr] = url_elt[attr] + + return {XEP_0048.MUC_TYPE: conf_data, XEP_0048.URL_TYPE: url_data} + + def _dict2BookmarkElt(self, type_, data): + """Construct a bookmark element from a data dict + @param data (dict): bookmark data (key: bookmark type, value: list) where key can be: + - XEP_0048.MUC_TYPE + - XEP_0048.URL_TYPE + - value (dict): data as for addBookmark + @return (domish.Element): bookmark element + """ + rooms_data = data.get(XEP_0048.MUC_TYPE, {}) + urls_data = data.get(XEP_0048.URL_TYPE, {}) + storage_elt = domish.Element((NS_BOOKMARKS, 'storage')) + for room_jid in rooms_data: + conference_elt = storage_elt.addElement('conference') + conference_elt[XEP_0048.MUC_KEY] = room_jid.full() + for attr in XEP_0048.MUC_ATTRS: + try: + conference_elt[attr] = rooms_data[room_jid][attr] + except KeyError: + pass + try: + conference_elt.addElement('nick', content=rooms_data[room_jid]['nick']) + except KeyError: + pass + + for url in urls_data: + url_elt = storage_elt.addElement('url') + url_elt[XEP_0048.URL_KEY] = url + for attr in XEP_0048.URL_ATTRS: + try: + url_elt[attr] = url[attr] + except KeyError: + pass + + return storage_elt + + def _bookmarksMenu(self, data, profile): + """ XMLUI activated by menu: return Gateways UI + @param profile: %(doc_profile)s + + """ + client = self.host.getClient(profile) + xmlui = xml_tools.XMLUI(title=_('Bookmarks manager')) + xmlui.addText(_("add a bookmark")) + xmlui.changeContainer("pairs") + xmlui.addLabel(_('Name')) + xmlui.addString('name') + xmlui.addLabel(_('jid')) + xmlui.addString('jid') + xmlui.addLabel(_('Nickname')) + xmlui.addString('nick', client.jid.user) + xmlui.addLabel(_('Autojoin')) + xmlui.addBool('autojoin') + xmlui.changeContainer("vertical") + xmlui.addButton(self.__bm_save_id, _("Save"), ('name', 'jid', 'nick', 'autojoin')) + return {'xmlui': xmlui.toXml()} + + def _bookmarksSaveCb(self, data, profile): + bm_data = xml_tools.XMLUIResult2DataFormResult(data) + try: + location = jid.JID(bm_data.pop('jid')) + except KeyError: + raise exceptions.InternalError("Can't find mandatory key") + d = self.addBookmark(XEP_0048.MUC_TYPE, location, bm_data, profile_key=profile) + d.addCallback(lambda dummy: {}) + return d + + @defer.inlineCallbacks + def addBookmark(self, type_, location, data, storage_type="auto", profile_key="@NONE@"): + """ Store a new bookmark + @param type_: bookmark type, one of: + - XEP_0048.MUC_TYPE: Multi-User chat room + - XEP_0048.URL_TYPE: web page URL + @param location: dependeding on type_, can be a MUC room jid or an url + @param data (dict): depending on type_, can contains the following keys: + - name: human readable name of the bookmark + - nick: user preferred room nick (default to user part of profile's jid) + - autojoin: "true" if room must be automatically joined on connection + - password: unused yet TODO + @param storage_type: where the bookmark will be stored, can be: + - "auto": find best available option: pubsub, private, local in that order + - "pubsub": PubSub private storage (XEP-0223) + - "private": Private XML storage (XEP-0049) + - "local": Store in SàT database + @param profile_key: %(doc_profile_key)s + + """ + assert storage_type in ('auto', 'pubsub', 'private', 'local') + client = self.host.getClient(profile_key) + if storage_type == 'auto': + if client.bookmarks_pubsub is not None: + storage_type = 'pubsub' + elif client.bookmarks_private is not None: + storage_type = 'private' + else: + storage_type = 'local' + warning(_("Bookmarks will be local only")) + info(_('Type selected for "auto" storage: %s') % storage_type) + + if storage_type == 'local': + client.bookmarks_local[type_][location] = data + yield client.bookmarks_local.force(type_) + else: + bookmarks = yield self._getServerBookmarks(storage_type, client.profile) + bookmarks[type_][location] = data + bookmark_elt = self._dict2BookmarkElt(type_, bookmarks) + yield self._setServerBookmarks(storage_type, bookmark_elt, client.profile) + + + + +