comparison libervia/backend/plugins/plugin_xep_0424.py @ 4199:7eda7cb8a15c

plugin XEP-0424: fix ID used and recipients
author Goffi <goffi@goffi.org>
date Wed, 13 Dec 2023 22:00:25 +0100
parents a1f7040b5a15
children
comparison
equal deleted inserted replaced
4198:b1207332cea2 4199:7eda7cb8a15c
41 C.PI_IMPORT_NAME: "XEP-0424", 41 C.PI_IMPORT_NAME: "XEP-0424",
42 C.PI_TYPE: "XEP", 42 C.PI_TYPE: "XEP",
43 C.PI_MODES: C.PLUG_MODE_BOTH, 43 C.PI_MODES: C.PLUG_MODE_BOTH,
44 C.PI_PROTOCOLS: ["XEP-0424"], 44 C.PI_PROTOCOLS: ["XEP-0424"],
45 C.PI_DEPENDENCIES: ["XEP-0334", "XEP-0428"], 45 C.PI_DEPENDENCIES: ["XEP-0334", "XEP-0428"],
46 C.PI_RECOMMENDATIONS: ["XEP-0045"],
46 C.PI_MAIN: "XEP_0424", 47 C.PI_MAIN: "XEP_0424",
47 C.PI_HANDLER: "yes", 48 C.PI_HANDLER: "yes",
48 C.PI_DESCRIPTION: _("""Implementation Message Retraction"""), 49 C.PI_DESCRIPTION: _("""Implementation Message Retraction"""),
49 } 50 }
50 51
65 category_name=CATEGORY, name=NAME, label=_(LABEL) 66 category_name=CATEGORY, name=NAME, label=_(LABEL)
66 ) 67 )
67 68
68 69
69 class XEP_0424: 70 class XEP_0424:
70
71 def __init__(self, host): 71 def __init__(self, host):
72 log.info(f"plugin {PLUGIN_INFO[C.PI_NAME]!r} initialization") 72 log.info(f"plugin {PLUGIN_INFO[C.PI_NAME]!r} initialization")
73 self.host = host 73 self.host = host
74 host.memory.update_params(PARAMS) 74 host.memory.update_params(PARAMS)
75 self._h = host.plugins["XEP-0334"] 75 self._h = host.plugins["XEP-0334"]
76 self._m = host.plugins.get("XEP-0045")
76 host.register_namespace("message-retract", NS_MESSAGE_RETRACT) 77 host.register_namespace("message-retract", NS_MESSAGE_RETRACT)
77 host.trigger.add("message_received", self._message_received_trigger, 100) 78 host.trigger.add("message_received", self._message_received_trigger, 100)
78 host.bridge.add_method( 79 host.bridge.add_method(
79 "message_retract", 80 "message_retract",
80 ".plugin", 81 ".plugin",
87 def get_handler(self, __): 88 def get_handler(self, __):
88 return XEP_0424_handler() 89 return XEP_0424_handler()
89 90
90 def _retract(self, message_id: str, profile: str) -> None: 91 def _retract(self, message_id: str, profile: str) -> None:
91 client = self.host.get_client(profile) 92 client = self.host.get_client(profile)
92 return defer.ensureDeferred( 93 return defer.ensureDeferred(self.retract(client, message_id))
93 self.retract(client, message_id) 94
94 ) 95 def send_retract(
95 96 self, client: SatXMPPEntity, peer_jid: jid.JID, retract_id: str, history: History
96 def retract_by_origin_id(
97 self,
98 client: SatXMPPEntity,
99 peer_jid: jid.JID,
100 origin_id: str
101 ) -> None: 97 ) -> None:
102 """Send a message retraction using origin-id 98 """Send a message retraction using origin-id
103 99
104 [retract] should be prefered: internal ID should be used as it is independant of 100 [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 101 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 102 (notably for some components), and then this method can be used
107 @param origin_id: origin-id as specified in XEP-0359 103
104 @param retract_id: ID (origin or stanza according to XEP-0359) of message to
105 retract
106 @param history: history instance of the message to retract
108 """ 107 """
109 message_elt = domish.Element((None, "message")) 108 message_elt = domish.Element((None, "message"))
110 message_elt["from"] = client.jid.full() 109 message_elt["from"] = client.jid.full()
111 message_elt["to"] = peer_jid.full() 110 message_elt["to"] = peer_jid.full()
111 message_elt["type"] = history.type
112 retract_elt = message_elt.addElement((NS_MESSAGE_RETRACT, "retract")) 112 retract_elt = message_elt.addElement((NS_MESSAGE_RETRACT, "retract"))
113 retract_elt["id"] = origin_id 113 retract_elt["id"] = retract_id
114 self.host.plugins["XEP-0428"].add_fallback_elt( 114 self.host.plugins["XEP-0428"].add_fallback_elt(
115 message_elt, 115 message_elt,
116 "[A message retraction has been requested, but your client doesn't support " 116 "[A message retraction has been requested, but your client doesn't support "
117 "it]" 117 "it]",
118 ) 118 )
119 self._h.add_hint_elements(message_elt, [self._h.HINT_STORE]) 119 self._h.add_hint_elements(message_elt, [self._h.HINT_STORE])
120 client.send(message_elt) 120 client.send(message_elt)
121 121
122 async def retract_by_history( 122 async def retract_by_history(self, client: SatXMPPEntity, history: History) -> None:
123 self,
124 client: SatXMPPEntity,
125 history: History
126 ) -> None:
127 """Send a message retraction using History instance 123 """Send a message retraction using History instance
128 124
129 This method is to use instead of [retract] when the history instance is already 125 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 126 retrieved. Note that the instance must have messages and subjets loaded
131 @param history: history instance of the message to retract 127 @param history: history instance of the message to retract
132 """ 128 """
133 try: 129 if history.type == C.MESS_TYPE_GROUPCHAT:
134 origin_id = history.origin_id 130 is_group_chat = True
135 except KeyError: 131 peer_jid = jid.JID(history.source)
132 retract_id = history.stanza_id
133 else:
134 is_group_chat = False
135 peer_jid = jid.JID(history.dest)
136 if self._m is not None and self._m.is_joined_room(client, peer_jid):
137 # it's actually a private MUC message, we need the full JID.
138 peer_jid = history.dest_jid
139 retract_id = history.origin_id
140
141 if not retract_id:
136 raise exceptions.FeatureNotFound( 142 raise exceptions.FeatureNotFound(
137 f"message to retract doesn't have the necessary origin-id, the sending " 143 "Message to retract doesn't have the necessary ID, the sending client is "
138 "client is probably not supporting message retraction." 144 "probably not supporting message retraction."
139 ) 145 )
140 else: 146
141 if history.type == C.MESS_TYPE_GROUPCHAT: 147 self.send_retract(client, peer_jid, retract_id, history)
142 is_group_chat = True 148 if not is_group_chat:
143 peer_jid = history.dest_jid 149 # retraction will happen when <retract> message will be received in the
144 else: 150 # chat.
145 is_group_chat = False 151 await self.retract_db_history(client, history)
146 peer_jid = jid.JID(history.dest)
147 self.retract_by_origin_id(client, peer_jid, origin_id)
148 if not is_group_chat:
149 # retraction will happen when <retract> message will be received in the
150 # chat.
151 await self.retract_db_history(client, history)
152 152
153 async def retract( 153 async def retract(
154 self, 154 self,
155 client: SatXMPPEntity, 155 client: SatXMPPEntity,
156 message_id: str, 156 message_id: str,
163 message is not found in database, an exception will be raised 163 message is not found in database, an exception will be raised
164 """ 164 """
165 if not message_id: 165 if not message_id:
166 raise ValueError("message_id can't be empty") 166 raise ValueError("message_id can't be empty")
167 history = await self.host.memory.storage.get( 167 history = await self.host.memory.storage.get(
168 client, History, History.uid, message_id, 168 client,
169 joined_loads=[History.messages, History.subjects, History.thread] 169 History,
170 History.uid,
171 message_id,
172 joined_loads=[History.messages, History.subjects, History.thread],
170 ) 173 )
171 if history is None: 174 if history is None:
172 raise exceptions.NotFound( 175 raise exceptions.NotFound(
173 f"message to retract not found in database ({message_id})" 176 f"message to retract not found in database ({message_id})"
174 ) 177 )
184 # retracted, but if may be bad if the user think it's really deleted 187 # retracted, but if may be bad if the user think it's really deleted
185 flag_modified(history, "extra") 188 flag_modified(history, "extra")
186 keep_history = await self.host.memory.param_get_a_async( 189 keep_history = await self.host.memory.param_get_a_async(
187 NAME, CATEGORY, profile_key=client.profile 190 NAME, CATEGORY, profile_key=client.profile
188 ) 191 )
189 old_version: Dict[str, Any] = { 192 old_version: Dict[str, Any] = {"timestamp": time.time()}
190 "timestamp": time.time()
191 }
192 if keep_history: 193 if keep_history:
193 old_version.update({ 194 old_version.update(
194 "messages": [m.serialise() for m in history.messages], 195 {
195 "subjects": [s.serialise() for s in history.subjects], 196 "messages": [m.serialise() for m in history.messages],
196 }) 197 "subjects": [s.serialise() for s in history.subjects],
198 }
199 )
197 200
198 history.extra.setdefault("old_versions", []).append(old_version) 201 history.extra.setdefault("old_versions", []).append(old_version)
199 202
200 history.messages.clear() 203 history.messages.clear()
201 history.subjects.clear() 204 history.subjects.clear()
217 220
218 async def _message_received_trigger( 221 async def _message_received_trigger(
219 self, 222 self,
220 client: SatXMPPEntity, 223 client: SatXMPPEntity,
221 message_elt: domish.Element, 224 message_elt: domish.Element,
222 post_treat: defer.Deferred 225 post_treat: defer.Deferred,
223 ) -> bool: 226 ) -> bool:
224 retract_elt = next(message_elt.elements(NS_MESSAGE_RETRACT, "retract"), None) 227 retract_elt = next(message_elt.elements(NS_MESSAGE_RETRACT, "retract"), None)
225 if not retract_elt: 228 if not retract_elt:
226 return True 229 return True
227 try: 230 try:
228 if message_elt.getAttribute("type") == C.MESS_TYPE_GROUPCHAT: 231 if message_elt.getAttribute("type") == C.MESS_TYPE_GROUPCHAT:
229 col_id = History.stanza_id 232 col_id = History.stanza_id
230 else: 233 else:
231 col_id = History.origin_id 234 col_id = History.origin_id
232 history = await self.host.memory.storage.get( 235 history = await self.host.memory.storage.get(
233 client, History, col_id, retract_elt["id"], 236 client,
234 joined_loads=[History.messages, History.subjects, History.thread] 237 History,
238 col_id,
239 retract_elt["id"],
240 joined_loads=[History.messages, History.subjects, History.thread],
235 ) 241 )
236 except KeyError: 242 except KeyError:
237 log.warning(f"invalid retract element, missing id: {retract_elt.toXml()}") 243 log.warning(f"invalid retract element, missing id: {retract_elt.toXml()}")
238 return False 244 return False
239 from_jid = jid.JID(message_elt["from"]) 245 from_jid = jid.JID(message_elt["from"])
240 246
241 if ( 247 if history is not None and (
242 history is not None 248 (history.type == C.MESS_TYPE_GROUPCHAT and history.source_jid != from_jid)
243 and history.source_jid.userhostJID() != from_jid.userhostJID() 249 or (
250 history.type != C.MESS_TYPE_GROUPCHAT
251 and history.source_jid.userhostJID() != from_jid.userhostJID()
252 )
244 ): 253 ):
245 log.warning( 254 log.warning(
246 f"Received message retraction from {from_jid.full()}, but the message to " 255 f"Received message retraction from {from_jid.full()}, but the message to "
247 f"retract is from {history.source_jid.full()}. This maybe a hack " 256 f"retract is from {history.source_jid.full()}. This maybe a hack "
248 f"attempt.\n{message_elt.toXml()}" 257 f"attempt.\n{message_elt.toXml()}"
255 return False 264 return False
256 265
257 if history is None: 266 if history is None:
258 # we check history after the trigger because we may be in a component which 267 # we check history after the trigger because we may be in a component which
259 # doesn't store messages in database. 268 # doesn't store messages in database.
260 log.warning( 269 log.warning(f"No message found with given id: {message_elt.toXml()}")
261 f"No message found with given origin-id: {message_elt.toXml()}"
262 )
263 return False 270 return False
264 log.info(f"[{client.profile}] retracting message {history.uid!r}") 271 log.info(f"[{client.profile}] retracting message {history.uid!r}")
265 await self.retract_db_history(client, history) 272 await self.retract_db_history(client, history)
266 return False 273 return False
267 274
268 275
269 @implementer(disco.IDisco) 276 @implementer(disco.IDisco)
270 class XEP_0424_handler(xmlstream.XMPPHandler): 277 class XEP_0424_handler(xmlstream.XMPPHandler):
271
272 def getDiscoInfo(self, __, target, nodeIdentifier=""): 278 def getDiscoInfo(self, __, target, nodeIdentifier=""):
273 return [disco.DiscoFeature(NS_MESSAGE_RETRACT)] 279 return [disco.DiscoFeature(NS_MESSAGE_RETRACT)]
274 280
275 def getDiscoItems(self, requestor, target, nodeIdentifier=""): 281 def getDiscoItems(self, requestor, target, nodeIdentifier=""):
276 return [] 282 return []