comparison sat/plugins/plugin_xep_0424.py @ 3801:6952a002abc7

plugin XEP-424: Message Retractation implementation: rel 367
author Goffi <goffi@goffi.org>
date Fri, 17 Jun 2022 14:15:23 +0200
parents
children 6090141b1b70
comparison
equal deleted inserted replaced
3800:2033fa3c5b85 3801:6952a002abc7
1 #!/usr/bin/env python3
2
3 # Copyright (C) 2009-2022 Jérôme Poisson (goffi@goffi.org)
4
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License as published by
7 # the Free Software Foundation, either version 3 of the License, or
8 # (at your option) any later version.
9
10 # This program is distributed in the hope that it will be useful,
11 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 # GNU Affero General Public License for more details.
14
15 # You should have received a copy of the GNU Affero General Public License
16 # along with this program. If not, see <http://www.gnu.org/licenses/>.
17
18 from typing import Dict, Any
19 import time
20 from copy import deepcopy
21
22 from twisted.words.protocols.jabber import xmlstream, jid
23 from twisted.words.xish import domish
24 from twisted.internet import defer
25 from wokkel import disco
26 from zope.interface import implementer
27
28 from sat.core.constants import Const as C
29 from sat.core.i18n import _, D_
30 from sat.core import exceptions
31 from sat.core.core_types import SatXMPPEntity
32 from sat.core.log import getLogger
33 from sat.memory.sqla_mapping import History
34
35 log = getLogger(__name__)
36
37
38 PLUGIN_INFO = {
39 C.PI_NAME: "Message Retraction",
40 C.PI_IMPORT_NAME: "XEP-0424",
41 C.PI_TYPE: "XEP",
42 C.PI_MODES: C.PLUG_MODE_BOTH,
43 C.PI_PROTOCOLS: ["XEP-0334", "XEP-0424", "XEP-0428"],
44 C.PI_DEPENDENCIES: ["XEP-0422"],
45 C.PI_MAIN: "XEP_0424",
46 C.PI_HANDLER: "yes",
47 C.PI_DESCRIPTION: _("""Implementation Message Retraction"""),
48 }
49
50 NS_MESSAGE_RETRACT = "urn:xmpp:message-retract:0"
51
52 CATEGORY = "Privacy"
53 NAME = "retract_history"
54 LABEL = D_("Keep History of Retracted Messages")
55 PARAMS = """
56 <params>
57 <individual>
58 <category name="{category_name}">
59 <param name="{name}" label="{label}" type="bool" value="false" />
60 </category>
61 </individual>
62 </params>
63 """.format(
64 category_name=CATEGORY, name=NAME, label=_(LABEL)
65 )
66
67
68 class XEP_0424(object):
69
70 def __init__(self, host):
71 log.info(_("XEP-0424 (Message Retraction) plugin initialization"))
72 self.host = host
73 host.memory.updateParams(PARAMS)
74 self._h = host.plugins["XEP-0334"]
75 self._f = host.plugins["XEP-0422"]
76 host.registerNamespace("message-retract", NS_MESSAGE_RETRACT)
77 host.trigger.add("messageReceived", self._messageReceivedTrigger, 100)
78 host.bridge.addMethod(
79 "messRetract",
80 ".plugin",
81 in_sign="ss",
82 out_sign="",
83 method=self._retract,
84 async_=True,
85 )
86
87 def getHandler(self, __):
88 return XEP_0424_handler()
89
90 def _retract(self, message_id: str, profile: str) -> None:
91 client = self.host.getClient(profile)
92 return defer.ensureDeferred(
93 self.retract(client, message_id)
94 )
95
96 def retractByOriginId(
97 self,
98 client: SatXMPPEntity,
99 dest_jid: jid.JID,
100 origin_id: str
101 ) -> None:
102 """Send a message retraction using origin-id
103
104 [retract] should be prefered: internal ID should be used as it is independant of
105 XEPs changes. However, in some case messages may not be stored in database
106 (notably for some components), and then this method can be used
107 @param origin_id: origin-id as specified in XEP-0359
108 """
109 message_elt = domish.Element((None, "message"))
110 message_elt["from"] = client.jid.full()
111 message_elt["to"] = dest_jid.full()
112 apply_to_elt = self._f.applyToElt(message_elt, origin_id)
113 apply_to_elt.addElement((NS_MESSAGE_RETRACT, "retract"))
114 self.host.plugins["XEP-0428"].addFallbackElt(
115 message_elt,
116 "[A message retraction has been requested, but your client doesn't support "
117 "it]"
118 )
119 self._h.addHintElements(message_elt, [self._h.HINT_STORE])
120 client.send(message_elt)
121
122 async def retractByHistory(
123 self,
124 client: SatXMPPEntity,
125 history: History
126 ) -> None:
127 """Send a message retraction using History instance
128
129 This method is to use instead of [retract] when the history instance is already
130 retrieved. Note that the instance must have messages and subjets loaded
131 @param history: history instance of the message to retract
132 """
133 try:
134 origin_id = history.origin_id
135 except KeyError:
136 raise exceptions.FeatureNotFound(
137 f"message to retract doesn't have the necessary origin-id, the sending "
138 "client is probably not supporting message retraction."
139 )
140 else:
141 self.retractByOriginId(client, history.dest_jid, origin_id)
142 await self.retractDBHistory(client, history)
143
144 async def retract(
145 self,
146 client: SatXMPPEntity,
147 message_id: str,
148 ) -> None:
149 """Send a message retraction request
150
151 @param message_id: ID of the message
152 This ID is the Libervia internal ID of the message. It will be retrieve from
153 database to find the ID used by XMPP (i.e. XEP-0359's "origin ID"). If the
154 message is not found in database, an exception will be raised
155 """
156 if not message_id:
157 raise ValueError("message_id can't be empty")
158 history = await self.host.memory.storage.get(
159 client, History, History.uid, message_id,
160 joined_loads=[History.messages, History.subjects]
161 )
162 if history is None:
163 raise exceptions.NotFound(
164 f"message to retract not found in database ({message_id})"
165 )
166 await self.retractByHistory(client, history)
167
168 async def retractDBHistory(self, client, history: History) -> None:
169 """Mark an history instance in database as retracted
170
171 @param history: history instance
172 "messages" and "subjects" must be loaded too
173 """
174 # FIXME: should be keep history? This is useful to check why a message has been
175 # retracted, but if may be bad if the user think it's really deleted
176 # we assign a new object to be sure to trigger an update
177 history.extra = deepcopy(history.extra) if history.extra else {}
178 history.extra["retracted"] = True
179 keep_history = self.host.memory.getParamA(
180 NAME, CATEGORY, profile_key=client.profile
181 )
182 old_version: Dict[str, Any] = {
183 "timestamp": time.time()
184 }
185 if keep_history:
186 old_version.update({
187 "messages": [m.serialise() for m in history.messages],
188 "subjects": [s.serialise() for s in history.subjects]
189 })
190
191 history.extra.setdefault("old_versions", []).append(old_version)
192 await self.host.memory.storage.delete(
193 history.messages + history.subjects,
194 session_add=[history]
195 )
196
197 async def _messageReceivedTrigger(
198 self,
199 client: SatXMPPEntity,
200 message_elt: domish.Element,
201 post_treat: defer.Deferred
202 ) -> bool:
203 fastened_elts = await self._f.getFastenedElts(client, message_elt)
204 if fastened_elts is None:
205 return True
206 for elt in fastened_elts.elements:
207 if elt.name == "retract" and elt.uri == NS_MESSAGE_RETRACT:
208 if fastened_elts.history is not None:
209 source_jid = fastened_elts.history.source_jid
210 from_jid = jid.JID(message_elt["from"])
211 if source_jid.userhostJID() != from_jid.userhostJID():
212 log.warning(
213 f"Received message retraction from {from_jid.full()}, but "
214 f"the message to retract is from {source_jid.full()}. This "
215 f"maybe a hack attempt.\n{message_elt.toXml()}"
216 )
217 return False
218 break
219 else:
220 return True
221 if not await self.host.trigger.asyncPoint(
222 "XEP-0424_retractReceived", client, message_elt, elt, fastened_elts
223 ):
224 return False
225 if fastened_elts.history is None:
226 # we check history after the trigger because we may be in a component which
227 # doesn't store messages in database.
228 log.warning(
229 f"No message found with given origin-id: {message_elt.toXml()}"
230 )
231 return False
232 log.info(f"[{client.profile}] retracting message {fastened_elts.id!r}")
233 await self.retractDBHistory(client, fastened_elts.history)
234 # TODO: send bridge signal
235
236 return False
237
238
239 @implementer(disco.IDisco)
240 class XEP_0424_handler(xmlstream.XMPPHandler):
241
242 def getDiscoInfo(self, __, target, nodeIdentifier=""):
243 return [disco.DiscoFeature(NS_MESSAGE_RETRACT)]
244
245 def getDiscoItems(self, requestor, target, nodeIdentifier=""):
246 return []