libervia-backend/libervia/backend/plugins/plugin_xep_0048.py
Povilas Kanapickas 4326d01956 remove uses of twisted.internet.defer.returnValue
This function has been deprecated in Twisted 24.7.0.
2024-12-11 01:17:09 +02:00

535 lines
21 KiB
Python

#!/usr/bin/env python3
# Libervia plugin for Bookmarks (xep-0048)
# Copyright (C) 2009-2024 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 typing import cast
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
)
# XXX: We're transitionning to XEP-0402, so we mark bridge method as "legacy"
# here.
host.bridge.add_method(
"bookmarks_legacy_list",
".plugin",
in_sign="sss",
out_sign="a{sa{sa{ss}}}",
method=self.bookmarks_list,
async_=True,
)
host.bridge.add_method(
"bookmarks_legacy_remove",
".plugin",
in_sign="ssss",
out_sign="",
method=self.bookmarks_remove,
async_=True,
)
host.bridge.add_method(
"bookmarks_legacy_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()
local = cast(dict[str, dict | None] | None, local)
if not local:
local = {XEP_0048.MUC_TYPE: {}, XEP_0048.URL_TYPE: {}}
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'")
return 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_: str, storage_location: str, profile_key: str = C.PROF_KEY_NONE
) -> defer.Deferred[dict]:
"""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
try:
data = bookmarks_ori[type_]
except KeyError:
log.warning(f"{type_!r} missing in {storage_location} storage.")
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