Mercurial > libervia-backend
diff libervia/backend/plugins/plugin_xep_0424.py @ 4166:a1f7040b5a15
plugin XEP-0424: message retraction update:
- follow specification update (with namespace bump)
- retract from history on message reception for group chat
- send bridge message
author | Goffi <goffi@goffi.org> |
---|---|
date | Thu, 30 Nov 2023 13:23:53 +0100 |
parents | 4b842c1fb686 |
children | 7eda7cb8a15c |
line wrap: on
line diff
--- a/libervia/backend/plugins/plugin_xep_0424.py Tue Nov 28 17:41:49 2023 +0100 +++ b/libervia/backend/plugins/plugin_xep_0424.py Thu Nov 30 13:23:53 2023 +0100 @@ -15,22 +15,23 @@ # 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 Dict, Any import time -from copy import deepcopy +from typing import Any, Dict -from twisted.words.protocols.jabber import xmlstream, jid +from sqlalchemy.orm.attributes import flag_modified +from twisted.internet import defer +from twisted.words.protocols.jabber import jid, xmlstream from twisted.words.xish import domish -from twisted.internet import defer from wokkel import disco from zope.interface import implementer +from libervia.backend.core import exceptions from libervia.backend.core.constants import Const as C -from libervia.backend.core.i18n import _, D_ -from libervia.backend.core import exceptions -from libervia.backend.core.core_types import SatXMPPEntity +from libervia.backend.core.core_types import MessageData, SatXMPPEntity +from libervia.backend.core.i18n import D_, _ from libervia.backend.core.log import getLogger from libervia.backend.memory.sqla_mapping import History +from libervia.backend.tools.common import data_format log = getLogger(__name__) @@ -40,14 +41,14 @@ C.PI_IMPORT_NAME: "XEP-0424", C.PI_TYPE: "XEP", C.PI_MODES: C.PLUG_MODE_BOTH, - C.PI_PROTOCOLS: ["XEP-0334", "XEP-0424", "XEP-0428"], - C.PI_DEPENDENCIES: ["XEP-0422"], + C.PI_PROTOCOLS: ["XEP-0424"], + C.PI_DEPENDENCIES: ["XEP-0334", "XEP-0428"], C.PI_MAIN: "XEP_0424", C.PI_HANDLER: "yes", C.PI_DESCRIPTION: _("""Implementation Message Retraction"""), } -NS_MESSAGE_RETRACT = "urn:xmpp:message-retract:0" +NS_MESSAGE_RETRACT = "urn:xmpp:message-retract:1" CATEGORY = "Privacy" NAME = "retract_history" @@ -65,14 +66,13 @@ ) -class XEP_0424(object): +class XEP_0424: def __init__(self, host): - log.info(_("XEP-0424 (Message Retraction) plugin initialization")) + log.info(f"plugin {PLUGIN_INFO[C.PI_NAME]!r} initialization") self.host = host host.memory.update_params(PARAMS) self._h = host.plugins["XEP-0334"] - self._f = host.plugins["XEP-0422"] host.register_namespace("message-retract", NS_MESSAGE_RETRACT) host.trigger.add("message_received", self._message_received_trigger, 100) host.bridge.add_method( @@ -96,7 +96,7 @@ def retract_by_origin_id( self, client: SatXMPPEntity, - dest_jid: jid.JID, + peer_jid: jid.JID, origin_id: str ) -> None: """Send a message retraction using origin-id @@ -108,9 +108,9 @@ """ message_elt = domish.Element((None, "message")) message_elt["from"] = client.jid.full() - message_elt["to"] = dest_jid.full() - apply_to_elt = self._f.apply_to_elt(message_elt, origin_id) - apply_to_elt.addElement((NS_MESSAGE_RETRACT, "retract")) + message_elt["to"] = peer_jid.full() + retract_elt = message_elt.addElement((NS_MESSAGE_RETRACT, "retract")) + retract_elt["id"] = origin_id self.host.plugins["XEP-0428"].add_fallback_elt( message_elt, "[A message retraction has been requested, but your client doesn't support " @@ -138,8 +138,17 @@ "client is probably not supporting message retraction." ) else: - self.retract_by_origin_id(client, history.dest_jid, origin_id) - await self.retract_db_history(client, history) + if history.type == C.MESS_TYPE_GROUPCHAT: + is_group_chat = True + peer_jid = history.dest_jid + else: + is_group_chat = False + peer_jid = jid.JID(history.dest) + self.retract_by_origin_id(client, peer_jid, origin_id) + if not is_group_chat: + # retraction will happen when <retract> message will be received in the + # chat. + await self.retract_db_history(client, history) async def retract( self, @@ -157,7 +166,7 @@ raise ValueError("message_id can't be empty") history = await self.host.memory.storage.get( client, History, History.uid, message_id, - joined_loads=[History.messages, History.subjects] + joined_loads=[History.messages, History.subjects, History.thread] ) if history is None: raise exceptions.NotFound( @@ -171,12 +180,10 @@ @param history: history instance "messages" and "subjects" must be loaded too """ - # FIXME: should be keep history? This is useful to check why a message has been + # FIXME: should we keep history? This is useful to check why a message has been # retracted, but if may be bad if the user think it's really deleted - # we assign a new object to be sure to trigger an update - history.extra = deepcopy(history.extra) if history.extra else {} - history.extra["retracted"] = True - keep_history = self.host.memory.param_get_a( + flag_modified(history, "extra") + keep_history = await self.host.memory.param_get_a_async( NAME, CATEGORY, profile_key=client.profile ) old_version: Dict[str, Any] = { @@ -185,13 +192,27 @@ if keep_history: old_version.update({ "messages": [m.serialise() for m in history.messages], - "subjects": [s.serialise() for s in history.subjects] + "subjects": [s.serialise() for s in history.subjects], }) history.extra.setdefault("old_versions", []).append(old_version) - await self.host.memory.storage.delete( - history.messages + history.subjects, - session_add=[history] + + history.messages.clear() + history.subjects.clear() + history.extra["retracted"] = True + # we remove editions history to keep no trace of old messages + if "editions" in history.extra: + del history.extra["editions"] + if "attachments" in history.extra: + del history.extra["attachments"] + await self.host.memory.storage.add(history) + + retract_data = MessageData(history.serialise()) + self.host.bridge.message_update( + history.uid, + C.MESS_UPDATE_RETRACT, + data_format.serialise(retract_data), + client.profile, ) async def _message_received_trigger( @@ -200,39 +221,48 @@ message_elt: domish.Element, post_treat: defer.Deferred ) -> bool: - fastened_elts = await self._f.get_fastened_elts(client, message_elt) - if fastened_elts is None: + retract_elt = next(message_elt.elements(NS_MESSAGE_RETRACT, "retract"), None) + if not retract_elt: return True - for elt in fastened_elts.elements: - if elt.name == "retract" and elt.uri == NS_MESSAGE_RETRACT: - if fastened_elts.history is not None: - source_jid = fastened_elts.history.source_jid - from_jid = jid.JID(message_elt["from"]) - if source_jid.userhostJID() != from_jid.userhostJID(): - log.warning( - f"Received message retraction from {from_jid.full()}, but " - f"the message to retract is from {source_jid.full()}. This " - f"maybe a hack attempt.\n{message_elt.toXml()}" - ) - return False - break - else: - return True + try: + if message_elt.getAttribute("type") == C.MESS_TYPE_GROUPCHAT: + col_id = History.stanza_id + else: + col_id = History.origin_id + history = await self.host.memory.storage.get( + client, History, col_id, retract_elt["id"], + joined_loads=[History.messages, History.subjects, History.thread] + ) + except KeyError: + log.warning(f"invalid retract element, missing id: {retract_elt.toXml()}") + return False + from_jid = jid.JID(message_elt["from"]) + + if ( + history is not None + and history.source_jid.userhostJID() != from_jid.userhostJID() + ): + log.warning( + f"Received message retraction from {from_jid.full()}, but the message to " + f"retract is from {history.source_jid.full()}. This maybe a hack " + f"attempt.\n{message_elt.toXml()}" + ) + return False + if not await self.host.trigger.async_point( - "XEP-0424_retractReceived", client, message_elt, elt, fastened_elts + "XEP-0424_retract_received", client, message_elt, retract_elt, history ): return False - if fastened_elts.history is None: + + if history is None: # we check history after the trigger because we may be in a component which # doesn't store messages in database. log.warning( f"No message found with given origin-id: {message_elt.toXml()}" ) return False - log.info(f"[{client.profile}] retracting message {fastened_elts.id!r}") - await self.retract_db_history(client, fastened_elts.history) - # TODO: send bridge signal - + log.info(f"[{client.profile}] retracting message {history.uid!r}") + await self.retract_db_history(client, history) return False