Mercurial > libervia-backend
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 [] |