Mercurial > libervia-backend
comparison libervia/backend/plugins/plugin_xep_0353.py @ 4112:bc60875cb3b8
plugin XEP-0166, XEP-0167, XEP-0234, XEP-0353: call events management to prepare for UI:
- XEP-0166: add `jingle_preflight` and `jingle_preflight_cancel` methods to prepare a
jingle session, principally used by XEP-0353 to create and cancel a session
- XEP-0167: preflight methods implementation, workflow split in more methods/signals to
handle UI and call events (e.g.: retract or reject a call)
- XEP-0234: implementation of preflight methods as they are now mandatory
- XEP-0353: handle various events using the new preflight methods
rel 423
author | Goffi <goffi@goffi.org> |
---|---|
date | Wed, 09 Aug 2023 00:07:37 +0200 |
parents | 4b842c1fb686 |
children | 0da563780ffc |
comparison
equal
deleted
inserted
replaced
4111:a8ac5e1e5848 | 4112:bc60875cb3b8 |
---|---|
45 C.PI_DEPENDENCIES: ["XEP-0166", "XEP-0334"], | 45 C.PI_DEPENDENCIES: ["XEP-0166", "XEP-0334"], |
46 C.PI_MAIN: "XEP_0353", | 46 C.PI_MAIN: "XEP_0353", |
47 C.PI_HANDLER: "yes", | 47 C.PI_HANDLER: "yes", |
48 C.PI_DESCRIPTION: _("""Implementation of Jingle Message Initiation"""), | 48 C.PI_DESCRIPTION: _("""Implementation of Jingle Message Initiation"""), |
49 } | 49 } |
50 | |
51 | |
52 class RejectException(exceptions.CancelError): | |
53 | |
54 def __init__(self, reason: str, text: str|None = None): | |
55 super().__init__(text) | |
56 self.reason = reason | |
57 | |
58 | |
59 class RetractException(exceptions.CancelError): | |
60 pass | |
50 | 61 |
51 | 62 |
52 class XEP_0353: | 63 class XEP_0353: |
53 def __init__(self, host): | 64 def __init__(self, host): |
54 log.info(_("plugin {name} initialization").format(name=PLUGIN_INFO[C.PI_NAME])) | 65 log.info(_("plugin {name} initialization").format(name=PLUGIN_INFO[C.PI_NAME])) |
59 host.trigger.add( | 70 host.trigger.add( |
60 "XEP-0166_initiate_elt_built", | 71 "XEP-0166_initiate_elt_built", |
61 self._on_initiate_trigger, | 72 self._on_initiate_trigger, |
62 # this plugin set the resource, we want it to happen first to other trigger | 73 # this plugin set the resource, we want it to happen first to other trigger |
63 # can get the full peer JID | 74 # can get the full peer JID |
75 priority=host.trigger.MAX_PRIORITY, | |
76 ) | |
77 host.trigger.add( | |
78 "XEP-0166_terminate", | |
79 self._terminate_trigger, | |
64 priority=host.trigger.MAX_PRIORITY, | 80 priority=host.trigger.MAX_PRIORITY, |
65 ) | 81 ) |
66 host.trigger.add("message_received", self._on_message_received) | 82 host.trigger.add("message_received", self._on_message_received) |
67 | 83 |
68 def get_handler(self, client): | 84 def get_handler(self, client): |
134 response_d = defer.Deferred() | 150 response_d = defer.Deferred() |
135 # we wait for 2 min before cancelling the session init | 151 # we wait for 2 min before cancelling the session init |
136 # FIXME: let's application decide timeout? | 152 # FIXME: let's application decide timeout? |
137 response_d.addTimeout(2 * 60, reactor) | 153 response_d.addTimeout(2 * 60, reactor) |
138 client._xep_0353_pending_sessions[session["id"]] = response_d | 154 client._xep_0353_pending_sessions[session["id"]] = response_d |
139 await client.send_message_data(mess_data) | 155 try: |
140 try: | 156 await client.send_message_data(mess_data) |
141 accepting_jid = await response_d | 157 accepting_jid = await response_d |
142 except defer.TimeoutError: | 158 except defer.TimeoutError: |
143 log.warning( | 159 log.warning( |
144 _("Message initiation with {peer_jid} timed out").format( | 160 _("Message initiation with {peer_jid} timed out").format( |
145 peer_jid=peer_jid | 161 peer_jid=peer_jid |
146 ) | 162 ) |
147 ) | 163 ) |
164 except exceptions.CancelError as e: | |
165 for content in session["contents"].values(): | |
166 await content["application"].handler.jingle_preflight_cancel( | |
167 client, session, e | |
168 ) | |
169 | |
170 self._j.delete_session(client, session["id"]) | |
171 return False | |
148 else: | 172 else: |
149 if iq_elt["to"] != accepting_jid.userhost(): | 173 if iq_elt["to"] != accepting_jid.userhost(): |
150 raise exceptions.InternalError( | 174 raise exceptions.InternalError( |
151 f"<jingle> 'to' attribute ({iq_elt['to']!r}) must not differ " | 175 f"<jingle> 'to' attribute ({iq_elt['to']!r}) must not differ " |
152 f"from bare JID of the accepting entity ({accepting_jid!r}), this " | 176 f"from bare JID of the accepting entity ({accepting_jid!r}), this " |
153 "may be a sign of an internal bug, a hack attempt, or a MITM attack!" | 177 "may be a sign of an internal bug, a hack attempt, or a MITM attack!" |
154 ) | 178 ) |
155 iq_elt["to"] = accepting_jid.full() | 179 iq_elt["to"] = accepting_jid.full() |
156 session["peer_jid"] = accepting_jid | 180 session["peer_jid"] = accepting_jid |
157 del client._xep_0353_pending_sessions[session["id"]] | 181 finally: |
182 del client._xep_0353_pending_sessions[session["id"]] | |
158 return True | 183 return True |
184 | |
185 def _terminate_trigger( | |
186 self, | |
187 client: SatXMPPEntity, | |
188 session: dict, | |
189 reason_elt: domish.Element | |
190 ) -> bool: | |
191 session_id = session["id"] | |
192 try: | |
193 response_d = client._xep_0353_pending_sessions[session_id] | |
194 except KeyError: | |
195 return True | |
196 # we have a XEP-0353 session, that means that we are retracting a proposed session | |
197 mess_data = self.build_message_data( | |
198 client, session["peer_jid"], "retract", session_id | |
199 ) | |
200 defer.ensureDeferred(client.send_message_data(mess_data)) | |
201 response_d.errback(RetractException()) | |
202 | |
203 return False | |
159 | 204 |
160 async def _on_message_received(self, client, message_elt, post_treat): | 205 async def _on_message_received(self, client, message_elt, post_treat): |
161 for elt in message_elt.elements(): | 206 for elt in message_elt.elements(): |
162 if elt.uri == NS_JINGLE_MESSAGE: | 207 if elt.uri == NS_JINGLE_MESSAGE: |
163 if elt.name == "propose": | 208 if elt.name == "propose": |
167 elif elt.name == "proceed": | 212 elif elt.name == "proceed": |
168 return self._handle_proceed(client, message_elt, elt) | 213 return self._handle_proceed(client, message_elt, elt) |
169 elif elt.name == "accept": | 214 elif elt.name == "accept": |
170 return self._handle_accept(client, message_elt, elt) | 215 return self._handle_accept(client, message_elt, elt) |
171 elif elt.name == "reject": | 216 elif elt.name == "reject": |
172 return self._handle_accept(client, message_elt, elt) | 217 return self._handle_reject(client, message_elt, elt) |
173 else: | 218 else: |
174 log.warning(f"invalid element: {elt.toXml}") | 219 log.warning(f"invalid element: {elt.toXml}") |
175 return True | 220 return True |
176 return True | 221 return True |
177 | 222 |
178 async def _handle_propose(self, client, message_elt, elt): | 223 def _get_sid_and_response_d( |
179 peer_jid = jid.JID(message_elt["from"]) | 224 self, |
180 session_id = elt["id"] | 225 client: SatXMPPEntity, |
181 if peer_jid.userhostJID() not in client.roster: | 226 elt: domish.Element |
182 app_ns = elt.description.uri | 227 ) -> tuple[str, defer.Deferred]: |
183 try: | 228 """Retrieve session ID and response_d from response element""" |
184 application = self._j.get_application(app_ns) | 229 try: |
185 human_name = getattr(application.handler, "human_name", application.name) | 230 session_id = elt["id"] |
186 except (exceptions.NotFound, AttributeError): | 231 except KeyError as e: |
187 if app_ns.startswith("urn:xmpp:jingle:apps:"): | 232 assert elt.parent is not None |
188 human_name = app_ns[21:].split(":", 1)[0].replace("-", " ").title() | 233 log.warning(f"invalid proceed element in message_elt: {elt.parent.toXml()}") |
189 else: | 234 raise e |
190 splitted_ns = app_ns.split(":") | |
191 if len(splitted_ns) > 1: | |
192 human_name = splitted_ns[-2].replace("- ", " ").title() | |
193 else: | |
194 human_name = app_ns | |
195 | |
196 confirm_msg = D_( | |
197 "Somebody not in your contact list ({peer_jid}) wants to do a " | |
198 '"{human_name}" session with you, this would leak your presence and ' | |
199 "possibly you IP (internet localisation), do you accept?" | |
200 ).format(peer_jid=peer_jid, human_name=human_name) | |
201 confirm_title = D_("Invitation from an unknown contact") | |
202 accept = await xml_tools.defer_confirm( | |
203 self.host, | |
204 confirm_msg, | |
205 confirm_title, | |
206 profile=client.profile, | |
207 action_extra={ | |
208 "type": C.META_TYPE_NOT_IN_ROSTER_LEAK, | |
209 "session_id": session_id, | |
210 "from_jid": peer_jid.full(), | |
211 }, | |
212 ) | |
213 if not accept: | |
214 mess_data = self.build_message_data( | |
215 client, client.jid.userhostJID(), "reject", session_id | |
216 ) | |
217 await client.send_message_data(mess_data) | |
218 # we don't sent anything to sender, to avoid leaking presence | |
219 return False | |
220 else: | |
221 await client.presence.available(peer_jid) | |
222 session_id = elt["id"] | |
223 mess_data = self.build_message_data(client, peer_jid, "proceed", session_id) | |
224 await client.send_message_data(mess_data) | |
225 return False | |
226 | |
227 def _handle_retract(self, client, message_elt, proceed_elt): | |
228 log.warning("retract is not implemented yet") | |
229 return False | |
230 | |
231 def _handle_proceed(self, client, message_elt, proceed_elt): | |
232 try: | |
233 session_id = proceed_elt["id"] | |
234 except KeyError: | |
235 log.warning(f"invalid proceed element in message_elt: {message_elt}") | |
236 return True | |
237 try: | 235 try: |
238 response_d = client._xep_0353_pending_sessions[session_id] | 236 response_d = client._xep_0353_pending_sessions[session_id] |
239 except KeyError: | 237 except KeyError as e: |
240 log.warning( | 238 log.warning( |
241 _( | 239 _( |
242 "no pending session found with id {session_id}, did it timed out?" | 240 "no pending session found with id {session_id}, did it timed out?" |
243 ).format(session_id=session_id) | 241 ).format(session_id=session_id) |
244 ) | 242 ) |
243 raise e | |
244 return session_id, response_d | |
245 | |
246 async def _handle_propose(self, client, message_elt, elt): | |
247 peer_jid = jid.JID(message_elt["from"]) | |
248 local_jid = jid.JID(message_elt["to"]) | |
249 session_id = elt["id"] | |
250 try: | |
251 desc_and_apps = [ | |
252 (description_elt, self._j.get_application(description_elt.uri)) | |
253 for description_elt in elt.elements() | |
254 if description_elt.name == "description" | |
255 ] | |
256 if not desc_and_apps: | |
257 raise AttributeError | |
258 except AttributeError: | |
259 log.warning(f"Invalid propose element: {message_elt.toXml()}") | |
260 return False | |
261 except exceptions.NotFound: | |
262 log.warning( | |
263 f"There is not registered application to handle this " | |
264 f"proposal: {elt.toXml()}" | |
265 ) | |
266 return False | |
267 | |
268 if not desc_and_apps: | |
269 log.warning("No application specified: {message_elt.toXml()}") | |
270 return False | |
271 | |
272 session = self._j.create_session( | |
273 client, session_id, self._j.ROLE_RESPONDER, peer_jid, local_jid | |
274 ) | |
275 | |
276 is_in_roster = peer_jid.userhostJID() in client.roster | |
277 if is_in_roster: | |
278 # we indicate that device is ringing as explained in | |
279 # https://xmpp.org/extensions/xep-0353.html#ring , but we only do that if user | |
280 # is in roster to avoid presence leak of all our devices. | |
281 mess_data = self.build_message_data(client, peer_jid, "ringing", session_id) | |
282 await client.send_message_data(mess_data) | |
283 | |
284 for description_elt, application in desc_and_apps: | |
285 try: | |
286 await application.handler.jingle_preflight( | |
287 client, session, description_elt | |
288 ) | |
289 except exceptions.CancelError as e: | |
290 log.info(f"{client.profile} refused the session: {e}") | |
291 | |
292 if is_in_roster: | |
293 # peer is in our roster, we send reject to them, ou other devices will | |
294 # get carbon copies | |
295 reject_dest_jid = peer_jid | |
296 else: | |
297 # peer is not in our roster, we send the "reject" only to our own | |
298 # devices to make them stop ringing/doing notification, and we don't | |
299 # send anything to peer to avoid presence leak. | |
300 reject_dest_jid = client.jid.userhostJID() | |
301 | |
302 mess_data = self.build_message_data( | |
303 client, reject_dest_jid, "reject", session_id | |
304 ) | |
305 await client.send_message_data(mess_data) | |
306 self._j.delete_session(client, session_id) | |
307 | |
308 return False | |
309 except defer.CancelledError: | |
310 # raised when call is retracted before user can reply | |
311 self._j.delete_session(client, session_id) | |
312 return False | |
313 | |
314 if peer_jid.userhostJID() not in client.roster: | |
315 await client.presence.available(peer_jid) | |
316 | |
317 mess_data = self.build_message_data(client, peer_jid, "proceed", session_id) | |
318 await client.send_message_data(mess_data) | |
319 | |
320 return False | |
321 | |
322 def _handle_retract(self, client, message_elt, retract_elt): | |
323 try: | |
324 session = self._j.get_session(client, retract_elt["id"]) | |
325 except KeyError: | |
326 log.warning(f"invalid retract element: {message_elt.toXml()}") | |
327 return False | |
328 except exceptions.NotFound: | |
329 log.warning(f"no session found with ID {retract_elt['id']}") | |
330 return False | |
331 log.debug( | |
332 f"{message_elt['from']} are retracting their proposal {retract_elt['id']}" | |
333 ) | |
334 try: | |
335 cancellable_deferred = session["cancellable_deferred"] | |
336 if not cancellable_deferred: | |
337 raise KeyError | |
338 except KeyError: | |
339 self._j.delete_session(client, session["id"]) | |
340 else: | |
341 for d in cancellable_deferred: | |
342 d.cancel() | |
343 return False | |
344 | |
345 def _handle_proceed(self, client, message_elt, proceed_elt): | |
346 try: | |
347 __, response_d = self._get_sid_and_response_d(client, proceed_elt) | |
348 except KeyError: | |
245 return True | 349 return True |
246 | 350 |
247 response_d.callback(jid.JID(message_elt["from"])) | 351 response_d.callback(jid.JID(message_elt["from"])) |
248 return False | 352 return False |
249 | 353 |
250 def _handle_accept(self, client, message_elt, accept_elt): | 354 def _handle_accept(self, client, message_elt, accept_elt): |
251 pass | 355 pass |
252 | 356 |
253 def _handle_reject(self, client, message_elt, accept_elt): | 357 def _handle_reject(self, client, message_elt, reject_elt): |
254 pass | 358 try: |
359 __, response_d = self._get_sid_and_response_d(client, reject_elt) | |
360 except KeyError: | |
361 return True | |
362 reason_elt = self._j.get_reason_elt(reject_elt) | |
363 reason, text = self._j.parse_reason_elt(reason_elt) | |
364 if reason is None: | |
365 reason = "busy" | |
366 | |
367 response_d.errback(RejectException(reason, text)) | |
368 return False | |
255 | 369 |
256 | 370 |
257 @implementer(iwokkel.IDisco) | 371 @implementer(iwokkel.IDisco) |
258 class Handler(xmlstream.XMPPHandler): | 372 class Handler(xmlstream.XMPPHandler): |
259 def getDiscoInfo(self, requestor, target, nodeIdentifier=""): | 373 def getDiscoInfo(self, requestor, target, nodeIdentifier=""): |