plugin XEP-0424: message retraction update:

- follow specification update (with namespace bump)
- retract from history on message reception for group chat
- send bridge message
This commit is contained in:
Goffi 2023-11-30 13:23:53 +01:00
commit 3c35dea8b1
6 changed files with 89 additions and 62 deletions

View file

@ -126,6 +126,7 @@ class Const(object):
MESS_UPDATE_EDIT = "EDIT"
MESS_UPDATE_REACTION = "REACTION"
MESS_UPDATE_RETRACT = "RETRACT"
MESS_EXTRA_INFO = "info_type"
EXTRA_INFO_DECR_ERR = "DECRYPTION_ERROR"

View file

@ -32,11 +32,6 @@ class MessageUpdateData(BaseModel):
super().__init__(**kwargs)
class MessageEditData(MessageData):
"""Data used when a new message edition has been received"""
pass
class MessageReactionData(MessageUpdateData):
reactions: dict[str, list[str]] = Field(
description="Reaction to reacting entities mapping"

View file

@ -167,7 +167,7 @@ class APGateway:
self.ad_hoc = APAdHocService(self)
self.ap_events = APEvents(self)
host.trigger.add("message_received", self._message_received_trigger, priority=-1000)
host.trigger.add("XEP-0424_retractReceived", self._on_message_retract)
host.trigger.add("XEP-0424_retract_received", self._on_message_retract)
host.trigger.add("XEP-0372_ref_received", self._on_reference_received)
host.bridge.add_method(
@ -2321,7 +2321,7 @@ class APGateway:
client: SatXMPPEntity,
message_elt: domish.Element,
retract_elt: domish.Element,
fastened_elts
history: History
) -> bool:
if client != self.client:
return True
@ -2341,7 +2341,7 @@ class APGateway:
actor_id = await self.get_ap_actor_id_from_account(ap_account)
inbox = await self.get_ap_inbox_from_id(actor_id, use_shared=False)
url_actor, ap_item = await self.ap_delete_item(
from_jid.userhostJID(), None, fastened_elts.id, public=False
from_jid.userhostJID(), None, retract_elt["id"], public=False
)
resp = await self.sign_and_post(inbox, url_actor, ap_item)
return False

View file

@ -31,7 +31,7 @@ from libervia.backend.core.core_types import SatXMPPEntity
from libervia.backend.core.i18n import _
from libervia.backend.core.log import getLogger
from libervia.backend.memory.sqla import History, Message, Subject, joinedload, select
from libervia.backend.models.core import MessageEditData, MessageEdition
from libervia.backend.models.core import MessageData, MessageEdition
from libervia.backend.tools.common import data_format
from libervia.backend.tools.utils import aio
log = getLogger(__name__)
@ -140,7 +140,7 @@ class XEP_0308:
edited_history.extra.setdefault(C.MESS_EXTRA_EDITIONS, []).append(previous_version)
await self.host.memory.storage.add(edited_history)
edit_data = MessageEditData(edited_history.serialise())
edit_data = MessageData(edited_history.serialise())
self.host.bridge.message_update(
edited_history.uid,
C.MESS_UPDATE_EDIT,

View file

@ -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 twisted.words.xish import domish
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 wokkel import disco
from zope.interface import implementer
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.constants import Const as C
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 @@ PLUGIN_INFO = {
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 @@ PARAMS = """
)
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 @@ class XEP_0424(object):
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 @@ class XEP_0424(object):
"""
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 @@ class XEP_0424(object):
"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 @@ class XEP_0424(object):
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 @@ class XEP_0424(object):
@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 @@ class XEP_0424(object):
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 @@ class XEP_0424(object):
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:
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:
retract_elt = next(message_elt.elements(NS_MESSAGE_RETRACT, "retract"), None)
if not retract_elt:
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

View file

@ -33,6 +33,7 @@ PLUGIN_INFO = {
C.PI_NAME: "Fallback Indication",
C.PI_IMPORT_NAME: "XEP-0428",
C.PI_TYPE: "XEP",
C.PI_MODES: C.PLUG_MODE_BOTH,
C.PI_PROTOCOLS: ["XEP-0428"],
C.PI_MAIN: "XEP_0428",
C.PI_HANDLER: "yes",