Mercurial > libervia-backend
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)