Mercurial > libervia-backend
diff libervia/backend/plugins/plugin_xep_0048.py @ 4071:4b842c1fb686
refactoring: renamed `sat` package to `libervia.backend`
author | Goffi <goffi@goffi.org> |
---|---|
date | Fri, 02 Jun 2023 11:49:51 +0200 |
parents | sat/plugins/plugin_xep_0048.py@524856bd7b19 |
children | 5f2d496c633f |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libervia/backend/plugins/plugin_xep_0048.py Fri Jun 02 11:49:51 2023 +0200 @@ -0,0 +1,523 @@ +#!/usr/bin/env python3 + + +# SAT plugin for Bookmarks (xep-0048) +# Copyright (C) 2009-2021 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 <http://www.gnu.org/licenses/>. + +from libervia.backend.core.i18n import _, D_ +from libervia.backend.core import exceptions +from libervia.backend.core.constants import Const as C +from libervia.backend.memory.persistent import PersistentBinaryDict +from libervia.backend.tools import xml_tools +from libervia.backend.core.log import getLogger + +log = getLogger(__name__) +from twisted.words.xish import domish +from twisted.words.protocols.jabber import jid +from twisted.words.protocols.jabber.error import StanzaError + +from twisted.internet import defer + +NS_BOOKMARKS = "storage:bookmarks" + +PLUGIN_INFO = { + C.PI_NAME: "Bookmarks", + C.PI_IMPORT_NAME: "XEP-0048", + C.PI_TYPE: "XEP", + C.PI_PROTOCOLS: ["XEP-0048"], + C.PI_DEPENDENCIES: ["XEP-0045"], + C.PI_RECOMMENDATIONS: ["XEP-0049"], + C.PI_MAIN: "XEP_0048", + C.PI_HANDLER: "no", + C.PI_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): + log.info(_("Bookmarks plugin initialization")) + self.host = host + # self.__menu_id = host.register_callback(self._bookmarks_menu, with_data=True) + self.__bm_save_id = host.register_callback(self._bookmarks_save_cb, with_data=True) + host.import_menu( + (D_("Groups"), D_("Bookmarks")), + self._bookmarks_menu, + security_limit=0, + help_string=D_("Use and manage bookmarks"), + ) + self.__selected_id = host.register_callback( + self._bookmark_selected_cb, with_data=True + ) + host.bridge.add_method( + "bookmarks_list", + ".plugin", + in_sign="sss", + out_sign="a{sa{sa{ss}}}", + method=self._bookmarks_list, + async_=True, + ) + host.bridge.add_method( + "bookmarks_remove", + ".plugin", + in_sign="ssss", + out_sign="", + method=self._bookmarks_remove, + async_=True, + ) + host.bridge.add_method( + "bookmarks_add", + ".plugin", + in_sign="ssa{ss}ss", + out_sign="", + method=self._bookmarks_add, + async_=True, + ) + try: + self.private_plg = self.host.plugins["XEP-0049"] + except KeyError: + self.private_plg = None + try: + self.host.plugins[C.TEXT_CMDS].register_text_commands(self) + except KeyError: + log.info(_("Text commands not available")) + + async def profile_connected(self, client): + local = client.bookmarks_local = PersistentBinaryDict( + NS_BOOKMARKS, client.profile + ) + await local.load() + if not local: + local[XEP_0048.MUC_TYPE] = dict() + local[XEP_0048.URL_TYPE] = dict() + private = await self._get_server_bookmarks("private", client.profile) + pubsub = client.bookmarks_pubsub = None + + for bookmarks in (local, private, pubsub): + if bookmarks is not None: + for (room_jid, data) in list(bookmarks[XEP_0048.MUC_TYPE].items()): + if data.get("autojoin", "false") == "true": + nick = data.get("nick", client.jid.user) + defer.ensureDeferred( + self.host.plugins["XEP-0045"].join(client, room_jid, nick, {}) + ) + + # we don't use a DeferredList to gather result here, as waiting for all room would + # slow down a lot the connection process, and result in a bad user experience. + + @defer.inlineCallbacks + def _get_server_bookmarks(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 + """ + client = self.host.get_client(profile) + if storage_type == "private": + try: + bookmarks_private_xml = yield self.private_plg.private_xml_get( + "storage", NS_BOOKMARKS, profile + ) + data = client.bookmarks_private = self._bookmark_elt_2_dict( + bookmarks_private_xml + ) + except (StanzaError, AttributeError): + log.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 _set_server_bookmarks(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.private_xml_store(bookmarks_elt, profile) + elif storage_type == "pubsub": + raise NotImplementedError + else: + raise ValueError("storage_type must be 'private' or 'pubsub'") + + def _bookmark_elt_2_dict(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 add_bookmark + """ + 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: + log.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"] = str( + next(conference_elt.elements(NS_BOOKMARKS, "nick")) + ) + 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: + log.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 _dict_2_bookmark_elt(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 add_bookmark + @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, url_data in urls_data.items(): + 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_data[attr] + except KeyError: + pass + + return storage_elt + + def _bookmark_selected_cb(self, data, profile): + try: + room_jid_s, nick = data["index"].split(" ", 1) + room_jid = jid.JID(room_jid_s) + except (KeyError, RuntimeError): + log.warning(_("No room jid selected")) + return {} + + client = self.host.get_client(profile) + d = self.host.plugins["XEP-0045"].join(client, room_jid, nick, {}) + + def join_eb(failure): + log.warning("Error while trying to join room: {}".format(failure)) + # FIXME: failure are badly managed in plugin XEP-0045. Plugin XEP-0045 need to be fixed before managing errors correctly here + return {} + + d.addCallbacks(lambda __: {}, join_eb) + return d + + def _bookmarks_menu(self, data, profile): + """ XMLUI activated by menu: return Gateways UI + @param profile: %(doc_profile)s + + """ + client = self.host.get_client(profile) + xmlui = xml_tools.XMLUI(title=_("Bookmarks manager")) + adv_list = xmlui.change_container( + "advanced_list", + columns=3, + selectable="single", + callback_id=self.__selected_id, + ) + for bookmarks in ( + client.bookmarks_local, + client.bookmarks_private, + client.bookmarks_pubsub, + ): + if bookmarks is None: + continue + for (room_jid, data) in sorted( + list(bookmarks[XEP_0048.MUC_TYPE].items()), + key=lambda item: item[1].get("name", item[0].user), + ): + room_jid_s = room_jid.full() + adv_list.set_row_index( + "%s %s" % (room_jid_s, data.get("nick") or client.jid.user) + ) + xmlui.addText(data.get("name", "")) + xmlui.addJid(room_jid) + if C.bool(data.get("autojoin", C.BOOL_FALSE)): + xmlui.addText("autojoin") + else: + xmlui.addEmpty() + adv_list.end() + xmlui.addDivider("dash") + xmlui.addText(_("add a bookmark")) + xmlui.change_container("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.change_container("vertical") + xmlui.addButton(self.__bm_save_id, _("Save"), ("name", "jid", "nick", "autojoin")) + return {"xmlui": xmlui.toXml()} + + def _bookmarks_save_cb(self, data, profile): + bm_data = xml_tools.xmlui_result_2_data_form_result(data) + try: + location = jid.JID(bm_data.pop("jid")) + except KeyError: + raise exceptions.InternalError("Can't find mandatory key") + d = self.add_bookmark(XEP_0048.MUC_TYPE, location, bm_data, profile_key=profile) + d.addCallback(lambda __: {}) + return d + + @defer.inlineCallbacks + def add_bookmark( + self, type_, location, data, storage_type="auto", profile_key=C.PROF_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") + if type_ == XEP_0048.URL_TYPE and {"autojoin", "nick"}.intersection(list(data.keys())): + raise ValueError("autojoin or nick can't be used with URLs") + client = self.host.get_client(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" + log.warning(_("Bookmarks will be local only")) + log.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._get_server_bookmarks(storage_type, client.profile) + bookmarks[type_][location] = data + bookmark_elt = self._dict_2_bookmark_elt(type_, bookmarks) + yield self._set_server_bookmarks(storage_type, bookmark_elt, client.profile) + + @defer.inlineCallbacks + def remove_bookmark( + self, type_, location, storage_type="all", profile_key=C.PROF_KEY_NONE + ): + """Remove a stored 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 storage_type: where the bookmark is stored, can be: + - "all": remove from everywhere + - "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 ("all", "pubsub", "private", "local") + client = self.host.get_client(profile_key) + + if storage_type in ("all", "local"): + try: + del client.bookmarks_local[type_][location] + yield client.bookmarks_local.force(type_) + except KeyError: + log.debug("Bookmark is not present in local storage") + + if storage_type in ("all", "private"): + bookmarks = yield self._get_server_bookmarks("private", client.profile) + try: + del bookmarks[type_][location] + bookmark_elt = self._dict_2_bookmark_elt(type_, bookmarks) + yield self._set_server_bookmarks("private", bookmark_elt, client.profile) + except KeyError: + log.debug("Bookmark is not present in private storage") + + if storage_type == "pubsub": + raise NotImplementedError + + def _bookmarks_list(self, type_, storage_location, profile_key=C.PROF_KEY_NONE): + """Return stored bookmarks + + @param type_: bookmark type, one of: + - XEP_0048.MUC_TYPE: Multi-User chat room + - XEP_0048.URL_TYPE: web page URL + @param storage_location: can be: + - 'all' + - 'local' + - 'private' + - 'pubsub' + @param profile_key: %(doc_profile_key)s + @param return (dict): (key: storage_location, value dict) with: + - value (dict): (key: bookmark_location, value: bookmark data) + """ + client = self.host.get_client(profile_key) + ret = {} + ret_d = defer.succeed(ret) + + def fill_bookmarks(__, _storage_location): + bookmarks_ori = getattr(client, "bookmarks_" + _storage_location) + if bookmarks_ori is None: + return ret + data = bookmarks_ori[type_] + for bookmark in data: + if type_ == XEP_0048.MUC_TYPE: + ret[_storage_location][bookmark.full()] = data[bookmark].copy() + else: + ret[_storage_location][bookmark] = data[bookmark].copy() + return ret + + for _storage_location in ("local", "private", "pubsub"): + if storage_location in ("all", _storage_location): + ret[_storage_location] = {} + if _storage_location in ("private",): + # we update distant bookmarks, just in case an other client added something + d = self._get_server_bookmarks(_storage_location, client.profile) + else: + d = defer.succeed(None) + d.addCallback(fill_bookmarks, _storage_location) + ret_d.addCallback(lambda __: d) + + return ret_d + + def _bookmarks_remove( + self, type_, location, storage_location, profile_key=C.PROF_KEY_NONE + ): + """Return stored bookmarks + + @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 storage_location: can be: + - "all": remove from everywhere + - "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 + """ + if type_ == XEP_0048.MUC_TYPE: + location = jid.JID(location) + return self.remove_bookmark(type_, location, storage_location, profile_key) + + def _bookmarks_add( + self, type_, location, data, storage_type="auto", profile_key=C.PROF_KEY_NONE + ): + if type_ == XEP_0048.MUC_TYPE: + location = jid.JID(location) + return self.add_bookmark(type_, location, data, storage_type, profile_key) + + def cmd_bookmark(self, client, mess_data): + """(Un)bookmark a MUC room + + @command (group): [autojoin | remove] + - autojoin: join room automatically on connection + - remove: remove bookmark(s) for this room + """ + txt_cmd = self.host.plugins[C.TEXT_CMDS] + + options = mess_data["unparsed"].strip().split() + if options and options[0] not in ("autojoin", "remove"): + txt_cmd.feed_back(client, _("Bad arguments"), mess_data) + return False + + room_jid = mess_data["to"].userhostJID() + + if "remove" in options: + self.remove_bookmark(XEP_0048.MUC_TYPE, room_jid, profile_key=client.profile) + txt_cmd.feed_back( + client, + _("All [%s] bookmarks are being removed") % room_jid.full(), + mess_data, + ) + return False + + data = { + "name": room_jid.user, + "nick": client.jid.user, + "autojoin": "true" if "autojoin" in options else "false", + } + self.add_bookmark(XEP_0048.MUC_TYPE, room_jid, data, profile_key=client.profile) + txt_cmd.feed_back(client, _("Bookmark added"), mess_data) + + return False