plugin XEP-424: Message Retractation implementation:
rel 367
This commit is contained in:
parent
46132a523e
commit
bcdd1356bb
1 changed files with 246 additions and 0 deletions
246
sat/plugins/plugin_xep_0424.py
Normal file
246
sat/plugins/plugin_xep_0424.py
Normal file
|
|
@ -0,0 +1,246 @@
|
|||
#!/usr/bin/env python3
|
||||
|
||||
# Copyright (C) 2009-2022 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 Dict, Any
|
||||
import time
|
||||
from copy import deepcopy
|
||||
|
||||
from twisted.words.protocols.jabber import xmlstream, jid
|
||||
from twisted.words.xish import domish
|
||||
from twisted.internet import defer
|
||||
from wokkel import disco
|
||||
from zope.interface import implementer
|
||||
|
||||
from sat.core.constants import Const as C
|
||||
from sat.core.i18n import _, D_
|
||||
from sat.core import exceptions
|
||||
from sat.core.core_types import SatXMPPEntity
|
||||
from sat.core.log import getLogger
|
||||
from sat.memory.sqla_mapping import History
|
||||
|
||||
log = getLogger(__name__)
|
||||
|
||||
|
||||
PLUGIN_INFO = {
|
||||
C.PI_NAME: "Message Retraction",
|
||||
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_MAIN: "XEP_0424",
|
||||
C.PI_HANDLER: "yes",
|
||||
C.PI_DESCRIPTION: _("""Implementation Message Retraction"""),
|
||||
}
|
||||
|
||||
NS_MESSAGE_RETRACT = "urn:xmpp:message-retract:0"
|
||||
|
||||
CATEGORY = "Privacy"
|
||||
NAME = "retract_history"
|
||||
LABEL = D_("Keep History of Retracted Messages")
|
||||
PARAMS = """
|
||||
<params>
|
||||
<individual>
|
||||
<category name="{category_name}">
|
||||
<param name="{name}" label="{label}" type="bool" value="false" />
|
||||
</category>
|
||||
</individual>
|
||||
</params>
|
||||
""".format(
|
||||
category_name=CATEGORY, name=NAME, label=_(LABEL)
|
||||
)
|
||||
|
||||
|
||||
class XEP_0424(object):
|
||||
|
||||
def __init__(self, host):
|
||||
log.info(_("XEP-0424 (Message Retraction) plugin initialization"))
|
||||
self.host = host
|
||||
host.memory.updateParams(PARAMS)
|
||||
self._h = host.plugins["XEP-0334"]
|
||||
self._f = host.plugins["XEP-0422"]
|
||||
host.registerNamespace("message-retract", NS_MESSAGE_RETRACT)
|
||||
host.trigger.add("messageReceived", self._messageReceivedTrigger, 100)
|
||||
host.bridge.addMethod(
|
||||
"messRetract",
|
||||
".plugin",
|
||||
in_sign="ss",
|
||||
out_sign="",
|
||||
method=self._retract,
|
||||
async_=True,
|
||||
)
|
||||
|
||||
def getHandler(self, __):
|
||||
return XEP_0424_handler()
|
||||
|
||||
def _retract(self, message_id: str, profile: str) -> None:
|
||||
client = self.host.getClient(profile)
|
||||
return defer.ensureDeferred(
|
||||
self.retract(client, message_id)
|
||||
)
|
||||
|
||||
def retractByOriginId(
|
||||
self,
|
||||
client: SatXMPPEntity,
|
||||
dest_jid: jid.JID,
|
||||
origin_id: str
|
||||
) -> None:
|
||||
"""Send a message retraction using origin-id
|
||||
|
||||
[retract] should be prefered: internal ID should be used as it is independant of
|
||||
XEPs changes. However, in some case messages may not be stored in database
|
||||
(notably for some components), and then this method can be used
|
||||
@param origin_id: origin-id as specified in XEP-0359
|
||||
"""
|
||||
message_elt = domish.Element((None, "message"))
|
||||
message_elt["from"] = client.jid.full()
|
||||
message_elt["to"] = dest_jid.full()
|
||||
apply_to_elt = self._f.applyToElt(message_elt, origin_id)
|
||||
apply_to_elt.addElement((NS_MESSAGE_RETRACT, "retract"))
|
||||
self.host.plugins["XEP-0428"].addFallbackElt(
|
||||
message_elt,
|
||||
"[A message retraction has been requested, but your client doesn't support "
|
||||
"it]"
|
||||
)
|
||||
self._h.addHintElements(message_elt, [self._h.HINT_STORE])
|
||||
client.send(message_elt)
|
||||
|
||||
async def retractByHistory(
|
||||
self,
|
||||
client: SatXMPPEntity,
|
||||
history: History
|
||||
) -> None:
|
||||
"""Send a message retraction using History instance
|
||||
|
||||
This method is to use instead of [retract] when the history instance is already
|
||||
retrieved. Note that the instance must have messages and subjets loaded
|
||||
@param history: history instance of the message to retract
|
||||
"""
|
||||
try:
|
||||
origin_id = history.origin_id
|
||||
except KeyError:
|
||||
raise exceptions.FeatureNotFound(
|
||||
f"message to retract doesn't have the necessary origin-id, the sending "
|
||||
"client is probably not supporting message retraction."
|
||||
)
|
||||
else:
|
||||
self.retractByOriginId(client, history.dest_jid, origin_id)
|
||||
await self.retractDBHistory(client, history)
|
||||
|
||||
async def retract(
|
||||
self,
|
||||
client: SatXMPPEntity,
|
||||
message_id: str,
|
||||
) -> None:
|
||||
"""Send a message retraction request
|
||||
|
||||
@param message_id: ID of the message
|
||||
This ID is the Libervia internal ID of the message. It will be retrieve from
|
||||
database to find the ID used by XMPP (i.e. XEP-0359's "origin ID"). If the
|
||||
message is not found in database, an exception will be raised
|
||||
"""
|
||||
if not message_id:
|
||||
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]
|
||||
)
|
||||
if history is None:
|
||||
raise exceptions.NotFound(
|
||||
f"message to retract not found in database ({message_id})"
|
||||
)
|
||||
await self.retractByHistory(client, history)
|
||||
|
||||
async def retractDBHistory(self, client, history: History) -> None:
|
||||
"""Mark an history instance in database as retracted
|
||||
|
||||
@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
|
||||
# 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.getParamA(
|
||||
NAME, CATEGORY, profile_key=client.profile
|
||||
)
|
||||
old_version: Dict[str, Any] = {
|
||||
"timestamp": time.time()
|
||||
}
|
||||
if keep_history:
|
||||
old_version.update({
|
||||
"messages": [m.serialise() for m in history.messages],
|
||||
"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]
|
||||
)
|
||||
|
||||
async def _messageReceivedTrigger(
|
||||
self,
|
||||
client: SatXMPPEntity,
|
||||
message_elt: domish.Element,
|
||||
post_treat: defer.Deferred
|
||||
) -> bool:
|
||||
fastened_elts = await self._f.getFastenedElts(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:
|
||||
return True
|
||||
if not await self.host.trigger.asyncPoint(
|
||||
"XEP-0424_retractReceived", client, message_elt, elt, fastened_elts
|
||||
):
|
||||
return False
|
||||
if fastened_elts.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.retractDBHistory(client, fastened_elts.history)
|
||||
# TODO: send bridge signal
|
||||
|
||||
return False
|
||||
|
||||
|
||||
@implementer(disco.IDisco)
|
||||
class XEP_0424_handler(xmlstream.XMPPHandler):
|
||||
|
||||
def getDiscoInfo(self, __, target, nodeIdentifier=""):
|
||||
return [disco.DiscoFeature(NS_MESSAGE_RETRACT)]
|
||||
|
||||
def getDiscoItems(self, requestor, target, nodeIdentifier=""):
|
||||
return []
|
||||
Loading…
Reference in a new issue