246 lines
8.9 KiB
Python
246 lines
8.9 KiB
Python
#!/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 []
|