diff 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
line wrap: on
line diff
--- a/libervia/backend/plugins/plugin_xep_0353.py	Tue Aug 08 23:59:24 2023 +0200
+++ b/libervia/backend/plugins/plugin_xep_0353.py	Wed Aug 09 00:07:37 2023 +0200
@@ -49,6 +49,17 @@
 }
 
 
+class RejectException(exceptions.CancelError):
+
+    def __init__(self, reason: str, text: str|None = None):
+        super().__init__(text)
+        self.reason = reason
+
+
+class RetractException(exceptions.CancelError):
+    pass
+
+
 class XEP_0353:
     def __init__(self, host):
         log.info(_("plugin {name} initialization").format(name=PLUGIN_INFO[C.PI_NAME]))
@@ -63,6 +74,11 @@
             # can get the full peer JID
             priority=host.trigger.MAX_PRIORITY,
         )
+        host.trigger.add(
+            "XEP-0166_terminate",
+            self._terminate_trigger,
+            priority=host.trigger.MAX_PRIORITY,
+        )
         host.trigger.add("message_received", self._on_message_received)
 
     def get_handler(self, client):
@@ -136,8 +152,8 @@
         # FIXME: let's application decide timeout?
         response_d.addTimeout(2 * 60, reactor)
         client._xep_0353_pending_sessions[session["id"]] = response_d
-        await client.send_message_data(mess_data)
         try:
+            await client.send_message_data(mess_data)
             accepting_jid = await response_d
         except defer.TimeoutError:
             log.warning(
@@ -145,6 +161,14 @@
                     peer_jid=peer_jid
                 )
             )
+        except exceptions.CancelError as e:
+            for content in session["contents"].values():
+                await content["application"].handler.jingle_preflight_cancel(
+                    client, session, e
+                )
+
+            self._j.delete_session(client, session["id"])
+            return False
         else:
             if iq_elt["to"] != accepting_jid.userhost():
                 raise exceptions.InternalError(
@@ -154,9 +178,30 @@
                 )
             iq_elt["to"] = accepting_jid.full()
             session["peer_jid"] = accepting_jid
-        del client._xep_0353_pending_sessions[session["id"]]
+        finally:
+            del client._xep_0353_pending_sessions[session["id"]]
         return True
 
+    def _terminate_trigger(
+        self,
+        client: SatXMPPEntity,
+        session: dict,
+        reason_elt: domish.Element
+    ) -> bool:
+        session_id = session["id"]
+        try:
+            response_d = client._xep_0353_pending_sessions[session_id]
+        except KeyError:
+            return True
+        # we have a XEP-0353 session, that means that we are retracting a proposed session
+        mess_data = self.build_message_data(
+            client, session["peer_jid"], "retract", session_id
+        )
+        defer.ensureDeferred(client.send_message_data(mess_data))
+        response_d.errback(RetractException())
+
+        return False
+
     async def _on_message_received(self, client, message_elt, post_treat):
         for elt in message_elt.elements():
             if elt.uri == NS_JINGLE_MESSAGE:
@@ -169,79 +214,138 @@
                 elif elt.name == "accept":
                     return self._handle_accept(client, message_elt, elt)
                 elif elt.name == "reject":
-                    return self._handle_accept(client, message_elt, elt)
+                    return self._handle_reject(client, message_elt, elt)
                 else:
                     log.warning(f"invalid element: {elt.toXml}")
                     return True
         return True
 
-    async def _handle_propose(self, client, message_elt, elt):
-        peer_jid = jid.JID(message_elt["from"])
-        session_id = elt["id"]
-        if peer_jid.userhostJID() not in client.roster:
-            app_ns = elt.description.uri
-            try:
-                application = self._j.get_application(app_ns)
-                human_name = getattr(application.handler, "human_name", application.name)
-            except (exceptions.NotFound, AttributeError):
-                if app_ns.startswith("urn:xmpp:jingle:apps:"):
-                    human_name = app_ns[21:].split(":", 1)[0].replace("-", " ").title()
-                else:
-                    splitted_ns = app_ns.split(":")
-                    if len(splitted_ns) > 1:
-                        human_name = splitted_ns[-2].replace("- ", " ").title()
-                    else:
-                        human_name = app_ns
-
-            confirm_msg = D_(
-                "Somebody not in your contact list ({peer_jid}) wants to do a "
-                '"{human_name}" session with you, this would leak your presence and '
-                "possibly you IP (internet localisation), do you accept?"
-            ).format(peer_jid=peer_jid, human_name=human_name)
-            confirm_title = D_("Invitation from an unknown contact")
-            accept = await xml_tools.defer_confirm(
-                self.host,
-                confirm_msg,
-                confirm_title,
-                profile=client.profile,
-                action_extra={
-                    "type": C.META_TYPE_NOT_IN_ROSTER_LEAK,
-                    "session_id": session_id,
-                    "from_jid": peer_jid.full(),
-                },
-            )
-            if not accept:
-                mess_data = self.build_message_data(
-                    client, client.jid.userhostJID(), "reject", session_id
-                )
-                await client.send_message_data(mess_data)
-                # we don't sent anything to sender, to avoid leaking presence
-                return False
-            else:
-                await client.presence.available(peer_jid)
-        session_id = elt["id"]
-        mess_data = self.build_message_data(client, peer_jid, "proceed", session_id)
-        await client.send_message_data(mess_data)
-        return False
-
-    def _handle_retract(self, client, message_elt, proceed_elt):
-        log.warning("retract is not implemented yet")
-        return False
-
-    def _handle_proceed(self, client, message_elt, proceed_elt):
+    def _get_sid_and_response_d(
+        self,
+        client: SatXMPPEntity,
+        elt: domish.Element
+    ) -> tuple[str, defer.Deferred]:
+        """Retrieve session ID and response_d from response element"""
         try:
-            session_id = proceed_elt["id"]
-        except KeyError:
-            log.warning(f"invalid proceed element in message_elt: {message_elt}")
-            return True
+            session_id = elt["id"]
+        except KeyError as e:
+            assert elt.parent is not None
+            log.warning(f"invalid proceed element in message_elt: {elt.parent.toXml()}")
+            raise e
         try:
             response_d = client._xep_0353_pending_sessions[session_id]
-        except KeyError:
+        except KeyError as e:
             log.warning(
                 _(
                     "no pending session found with id {session_id}, did it timed out?"
                 ).format(session_id=session_id)
             )
+            raise e
+        return session_id, response_d
+
+    async def _handle_propose(self, client, message_elt, elt):
+        peer_jid = jid.JID(message_elt["from"])
+        local_jid = jid.JID(message_elt["to"])
+        session_id = elt["id"]
+        try:
+            desc_and_apps = [
+                (description_elt, self._j.get_application(description_elt.uri))
+                for description_elt in elt.elements()
+                if description_elt.name == "description"
+            ]
+            if not desc_and_apps:
+                raise AttributeError
+        except AttributeError:
+            log.warning(f"Invalid propose element: {message_elt.toXml()}")
+            return False
+        except exceptions.NotFound:
+            log.warning(
+                f"There is not registered application to handle this "
+                f"proposal: {elt.toXml()}"
+            )
+            return False
+
+        if not desc_and_apps:
+            log.warning("No application specified: {message_elt.toXml()}")
+            return False
+
+        session = self._j.create_session(
+            client, session_id, self._j.ROLE_RESPONDER, peer_jid, local_jid
+        )
+
+        is_in_roster = peer_jid.userhostJID() in client.roster
+        if is_in_roster:
+            # we indicate that device is ringing as explained in
+            # https://xmpp.org/extensions/xep-0353.html#ring , but we only do that if user
+            # is in roster to avoid presence leak of all our devices.
+            mess_data = self.build_message_data(client, peer_jid, "ringing", session_id)
+            await client.send_message_data(mess_data)
+
+        for description_elt, application in desc_and_apps:
+            try:
+                await application.handler.jingle_preflight(
+                    client, session, description_elt
+                )
+            except exceptions.CancelError as e:
+                log.info(f"{client.profile} refused the session: {e}")
+
+                if is_in_roster:
+                    # peer is in our roster, we send reject to them, ou other devices will
+                    # get carbon copies
+                    reject_dest_jid = peer_jid
+                else:
+                    # peer is not in our roster, we send the "reject" only to our own
+                    # devices to make them stop ringing/doing notification, and we don't
+                    # send anything to peer to avoid presence leak.
+                    reject_dest_jid = client.jid.userhostJID()
+
+                mess_data = self.build_message_data(
+                    client, reject_dest_jid, "reject", session_id
+                )
+                await client.send_message_data(mess_data)
+                self._j.delete_session(client, session_id)
+
+                return False
+            except defer.CancelledError:
+                # raised when call is retracted before user can reply
+                self._j.delete_session(client, session_id)
+                return False
+
+        if peer_jid.userhostJID() not in client.roster:
+            await client.presence.available(peer_jid)
+
+        mess_data = self.build_message_data(client, peer_jid, "proceed", session_id)
+        await client.send_message_data(mess_data)
+
+        return False
+
+    def _handle_retract(self, client, message_elt, retract_elt):
+        try:
+            session = self._j.get_session(client, retract_elt["id"])
+        except KeyError:
+            log.warning(f"invalid retract element: {message_elt.toXml()}")
+            return False
+        except exceptions.NotFound:
+            log.warning(f"no session found with ID {retract_elt['id']}")
+            return False
+        log.debug(
+            f"{message_elt['from']} are retracting their proposal {retract_elt['id']}"
+        )
+        try:
+            cancellable_deferred = session["cancellable_deferred"]
+            if not cancellable_deferred:
+                raise KeyError
+        except KeyError:
+            self._j.delete_session(client, session["id"])
+        else:
+            for d in cancellable_deferred:
+                d.cancel()
+        return False
+
+    def _handle_proceed(self, client, message_elt, proceed_elt):
+        try:
+            __, response_d = self._get_sid_and_response_d(client, proceed_elt)
+        except KeyError:
             return True
 
         response_d.callback(jid.JID(message_elt["from"]))
@@ -250,8 +354,18 @@
     def _handle_accept(self, client, message_elt, accept_elt):
         pass
 
-    def _handle_reject(self, client, message_elt, accept_elt):
-        pass
+    def _handle_reject(self, client, message_elt, reject_elt):
+        try:
+            __, response_d = self._get_sid_and_response_d(client, reject_elt)
+        except KeyError:
+            return True
+        reason_elt = self._j.get_reason_elt(reject_elt)
+        reason, text = self._j.parse_reason_elt(reason_elt)
+        if reason is None:
+            reason = "busy"
+
+        response_d.errback(RejectException(reason, text))
+        return False
 
 
 @implementer(iwokkel.IDisco)