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
This commit is contained in:
Goffi 2023-08-09 00:07:37 +02:00
commit ba6e59d2d1
5 changed files with 514 additions and 112 deletions

View file

@ -785,6 +785,8 @@ class XEP_0166:
return
try:
# session may have been already created in a jingle_preflight, in this
# case we re-use it.
session = self.get_session(client, sid)
except exceptions.NotFound:
# XXX: we store local_jid using request['to'] because for a component the

View file

@ -24,12 +24,38 @@ from typing import Awaitable, Callable, Union
from twisted.internet import defer
from twisted.words.xish import domish
from libervia.backend.core import exceptions
from libervia.backend.core.core_types import SatXMPPEntity
from libervia.backend.core.i18n import _
class BaseApplicationHandler(abc.ABC):
@abc.abstractmethod
async def jingle_preflight(
self,
client: SatXMPPEntity,
session: dict,
description_elt: domish.Element
) -> None:
"""Called when preparation steps are needed by a plugin
Notably used by XEP-0353 when a initiation message is received
"""
pass
@abc.abstractmethod
async def jingle_preflight_cancel(
self,
client: SatXMPPEntity,
session: dict,
cancel_error: exceptions.CancelError
) -> None:
"""Called when preflight initiation is cancelled
Notably used by XEP-0353 when an initiation message is cancelled
"""
pass
@abc.abstractmethod
def jingle_request_confirmation(
self,
@ -42,8 +68,7 @@ class BaseApplicationHandler(abc.ABC):
Callable[..., Union[bool, defer.Deferred]],
Callable[..., Awaitable[bool]]
]:
"""
If present, it is called on when session must be accepted.
"""If present, it is called on when session must be accepted.
If not present, a generic accept dialog will be used.
@param session: Jingle Session
@ -64,8 +89,7 @@ class BaseApplicationHandler(abc.ABC):
Callable[..., domish.Element],
Callable[..., Awaitable[domish.Element]]
]:
"""
Must return the domish.Element used for initial content.
"""Must return the domish.Element used for initial content.
@param client: SatXMPPEntity instance
@param session: Jingle Session
@ -86,8 +110,7 @@ class BaseApplicationHandler(abc.ABC):
Callable[..., None],
Callable[..., Awaitable[None]]
]:
"""
Called on several actions to negotiate the application or transport.
"""Called on several actions to negotiate the application or transport.
@param client: SatXMPPEntity instance
@param action: Jingle action
@ -109,8 +132,7 @@ class BaseApplicationHandler(abc.ABC):
Callable[..., None],
Callable[..., Awaitable[None]]
]:
"""
Called on session terminate, with reason_elt.
"""Called on session terminate, with reason_elt.
May be used to clean session.
@param reason_elt: Reason element
@ -131,8 +153,7 @@ class BaseTransportHandler(abc.ABC):
Callable[..., domish.Element],
Callable[..., Awaitable[domish.Element]]
]:
"""
Must return the domish.Element used for initial content.
"""Must return the domish.Element used for initial content.
@param client: SatXMPPEntity instance
@param session: Jingle Session
@ -153,8 +174,7 @@ class BaseTransportHandler(abc.ABC):
Callable[..., None],
Callable[..., Awaitable[None]]
]:
"""
Called on several actions to negotiate the application or transport.
"""Called on several actions to negotiate the application or transport.
@param client: SatXMPPEntity instance
@param action: Jingle action

View file

@ -17,7 +17,9 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from typing import Optional
import uuid
from twisted.internet import reactor
from twisted.internet import defer
from twisted.words.protocols.jabber import jid
from twisted.words.protocols.jabber.xmlstream import XMPPHandler
@ -37,8 +39,8 @@ from . import mapping
from ..plugin_xep_0166 import BaseApplicationHandler
from .constants import (
NS_JINGLE_RTP,
NS_JINGLE_RTP_INFO,
NS_JINGLE_RTP_AUDIO,
NS_JINGLE_RTP_INFO,
NS_JINGLE_RTP_VIDEO,
)
@ -88,11 +90,11 @@ class XEP_0167(BaseApplicationHandler):
async_=True,
)
host.bridge.add_method(
"call_end",
"call_answer_sdp",
".plugin",
in_sign="sss",
out_sign="",
method=self._call_end,
method=self._call_answer_sdp,
async_=True,
)
host.bridge.add_method(
@ -100,17 +102,32 @@ class XEP_0167(BaseApplicationHandler):
".plugin",
in_sign="ssss",
out_sign="",
method=self._call_start,
method=self._call_info,
)
host.bridge.add_method(
"call_end",
".plugin",
in_sign="sss",
out_sign="",
method=self._call_end,
async_=True,
)
# args: session_id, serialised setup data (dict with keys "role" and "sdp"),
# profile
host.bridge.add_signal(
"call_accepted", ".plugin", signature="sss"
) # args: session_id, answer_sdp, profile
"call_setup", ".plugin", signature="sss"
)
# args: session_id, data, profile
host.bridge.add_signal(
"call_ended", ".plugin", signature="sss"
) # args: session_id, data, profile
)
# args: session_id, info_type, extra, profile
host.bridge.add_signal(
"call_info", ".plugin", signature="ssss"
) # args: session_id, info_type, extra, profile
)
def get_handler(self, client):
return XEP_0167_handler()
@ -135,8 +152,26 @@ class XEP_0167(BaseApplicationHandler):
client: SatXMPPEntity,
peer_jid: jid.JID,
call_data: dict,
) -> None:
"""Temporary method to test RTP session"""
) -> str:
"""Initiate a call session with the given peer.
@param peer_jid: JID of the peer to initiate a call session with.
@param call_data: Dictionary containing data for the call. Must include SDP information.
The dict can have the following keys:
- sdp (str): SDP data for the call.
- metadata (dict): Additional metadata for the call (optional).
Each media type ("audio" and "video") in the SDP should have:
- application_data (dict): Data about the media.
- fingerprint (str): Security fingerprint data (optional).
- id (str): Identifier for the media (optional).
- ice-candidates: ICE candidates for media transport.
- And other transport specific data.
@return: Session ID (SID) for the initiated call session.
@raises exceptions.DataError: If media data is invalid or duplicate content name
(mid) is found.
"""
contents = []
metadata = call_data.get("metadata") or {}
@ -201,14 +236,35 @@ class XEP_0167(BaseApplicationHandler):
contents.append(content)
if not contents:
raise exceptions.DataError("no valid media data found: {call_data}")
return await self._j.initiate(
client,
peer_jid,
contents,
call_type=call_type,
metadata=metadata,
peer_metadata={},
sid = str(uuid.uuid4())
defer.ensureDeferred(
self._j.initiate(
client,
peer_jid,
contents,
sid=sid,
call_type=call_type,
metadata=metadata,
peer_metadata={},
)
)
return sid
def _call_answer_sdp(
self,
session_id: str,
answer_sdp: str,
profile: str
) -> None:
client = self.host.get_client(profile)
session = self._j.get_session(client, session_id)
try:
answer_sdp_d = session.pop("answer_sdp_d")
except KeyError:
raise exceptions.NotFound(
f"No answer SDP expected for session {session_id!r}"
)
answer_sdp_d.callback(answer_sdp)
def _call_end(
self,
@ -240,6 +296,123 @@ class XEP_0167(BaseApplicationHandler):
# jingle callbacks
async def confirm_incoming_call(
self,
client: SatXMPPEntity,
session: dict,
call_type: str
) -> bool:
"""Prompt the user for a call confirmation.
@param client: The client entity.
@param session: The Jingle session.
@param media_type: Type of media (audio or video).
@return: True if the call has been accepted
"""
peer_jid = session["peer_jid"]
session["call_type"] = call_type
cancellable_deferred = session.setdefault("cancellable_deferred", [])
dialog_d = xml_tools.defer_dialog(
self.host,
_(CONFIRM).format(peer=peer_jid.userhost(), call_type=call_type),
_(CONFIRM_TITLE),
action_extra={
"session_id": session["id"],
"from_jid": peer_jid.full(),
"type": C.META_TYPE_CALL,
"sub_type": call_type,
},
security_limit=SECURITY_LIMIT,
profile=client.profile,
)
cancellable_deferred.append(dialog_d)
resp_data = await dialog_d
accepted = not resp_data.get("cancelled", False)
if accepted:
session["call_accepted"] = True
return accepted
async def jingle_preflight(
self,
client: SatXMPPEntity,
session: dict,
description_elt: domish.Element
) -> None:
"""Perform preflight checks for an incoming call session.
Check if the calls is audio only or audio/video, then, prompts the user for
confirmation.
@param client: The client instance.
@param session: Jingle session.
@param description_elt: The description element. It's parent attribute is used to
determine check siblings to see if it's an audio only or audio/video call.
@raises exceptions.CancelError: If the user doesn't accept the incoming call.
"""
if session.get("call_accepted", False):
# the call is already accepted, nothing to do
return
parent_elt = description_elt.parent
assert parent_elt is not None
assert description_elt.parent is not None
for desc_elt in parent_elt.elements(NS_JINGLE_RTP, "description"):
if desc_elt.getAttribute("media") == "video":
call_type = C.META_SUBTYPE_CALL_VIDEO
break
else:
call_type = C.META_SUBTYPE_CALL_AUDIO
try:
accepted = await self.confirm_incoming_call(client, session, call_type)
except defer.CancelledError as e:
# raised when call is retracted before user has answered or rejected
self.host.bridge.call_ended(
session["id"],
data_format.serialise({"reason": "retracted"}),
client.profile
)
raise e
if not accepted:
raise exceptions.CancelError("User declined the incoming call.")
async def jingle_preflight_cancel(
self,
client: SatXMPPEntity,
session: dict,
cancel_error: exceptions.CancelError
) -> None:
"""The call has been rejected"""
# call_ended is use to send the signal only once even if there are audio and video
# contents
call_ended = session.get("call_ended", False)
if call_ended:
return
data = {
"reason": getattr(cancel_error, "reason", "cancelled")
}
text = getattr(cancel_error, "text", None)
if text:
data["text"] = text
self.host.bridge.call_ended(
session["id"],
data_format.serialise(data),
client.profile
)
session["call_ended"] = True
def jingle_session_init(
self,
client: SatXMPPEntity,
@ -275,42 +448,56 @@ class XEP_0167(BaseApplicationHandler):
content_name: str,
desc_elt: domish.Element,
) -> bool:
"""Requests confirmation from the user for a Jingle session's incoming call.
This method checks the content type of the Jingle session (audio or video)
based on the session's contents. Confirmation is requested only for the first
content; subsequent contents are automatically accepted. This means, in practice,
that the call confirmation is prompted only once for both audio and video contents.
@param client: The client instance.
@param action: The action type associated with the Jingle session.
@param session: Jingle session.
@param content_name: Name of the content being checked.
@param desc_elt: The description element associated with the content.
@return: True if the call is accepted by the user, False otherwise.
"""
if content_name != next(iter(session["contents"])):
# we request confirmation only for the first content, all others are
# automatically accepted. In practice, that means that the call confirmation
# is requested only once for audio and video contents.
return True
peer_jid = session["peer_jid"]
if any(
c["desc_elt"].getAttribute("media") == "video"
for c in session["contents"].values()
):
call_type = session["call_type"] = C.META_SUBTYPE_CALL_VIDEO
else:
call_type = session["call_type"] = C.META_SUBTYPE_CALL_AUDIO
if not session.get("call_accepted", False):
if any(
c["desc_elt"].getAttribute("media") == "video"
for c in session["contents"].values()
):
call_type = session["call_type"] = C.META_SUBTYPE_CALL_VIDEO
else:
call_type = session["call_type"] = C.META_SUBTYPE_CALL_AUDIO
accepted = await self.confirm_incoming_call(client, session, call_type)
if not accepted:
return False
sdp = mapping.generate_sdp_from_session(session)
session["answer_sdp_d"] = answer_sdp_d = defer.Deferred()
# we should have the answer long before 2 min
answer_sdp_d.addTimeout(2 * 60, reactor)
resp_data = await xml_tools.defer_dialog(
self.host,
_(CONFIRM).format(peer=peer_jid.userhost(), call_type=call_type),
_(CONFIRM_TITLE),
action_extra={
"session_id": session["id"],
"from_jid": peer_jid.full(),
"type": C.META_TYPE_CALL,
"sub_type": call_type,
self.host.bridge.call_setup(
session["id"],
data_format.serialise({
"role": session["role"],
"sdp": sdp,
},
security_limit=SECURITY_LIMIT,
profile=client.profile,
}),
client.profile
)
if resp_data.get("cancelled", False):
return False
answer_sdp = await answer_sdp_d
answer_sdp = resp_data["sdp"]
parsed_answer = mapping.parse_sdp(answer_sdp)
session["peer_metadata"].update(parsed_answer["metadata"])
for media in ("audio", "video"):
@ -352,7 +539,14 @@ class XEP_0167(BaseApplicationHandler):
# we only send the signal for first content, as it means that the whole
# session is accepted
answer_sdp = mapping.generate_sdp_from_session(session)
self.host.bridge.call_accepted(session["id"], answer_sdp, client.profile)
self.host.bridge.call_setup(
session["id"],
data_format.serialise({
"role": session["role"],
"sdp": answer_sdp,
}),
client.profile
)
else:
log.warning(f"FIXME: unmanaged action {action}")
@ -397,7 +591,6 @@ class XEP_0167(BaseApplicationHandler):
extra = data_format.deserialise(extra_s)
return self.send_info(client, session_id, info_type, extra)
def send_info(
self,
client: SatXMPPEntity,
@ -423,7 +616,15 @@ class XEP_0167(BaseApplicationHandler):
content_name: str,
reason_elt: domish.Element,
) -> None:
self.host.bridge.call_ended(session["id"], "", client.profile)
reason, text = self._j.parse_reason_elt(reason_elt)
data = {
"reason": reason
}
if text:
data["text"] = text
self.host.bridge.call_ended(
session["id"], data_format.serialise(data), client.profile
)
@implementer(iwokkel.IDisco)

View file

@ -32,13 +32,17 @@ from zope.interface import implementer
from libervia.backend.core import exceptions
from libervia.backend.core.constants import Const as C
from libervia.backend.core.core_types import SatXMPPEntity
from libervia.backend.core.i18n import D_, _
from libervia.backend.core.log import getLogger
from libervia.backend.tools import xml_tools
from libervia.backend.tools import utils
from libervia.backend.tools import stream
from libervia.backend.tools.common import date_utils
from libervia.backend.tools.common import regex
from .plugin_xep_0166 import BaseApplicationHandler
log = getLogger(__name__)
@ -60,7 +64,7 @@ EXTRA_ALLOWED = {"path", "namespace", "file_desc", "file_hash", "hash_algo"}
Range = namedtuple("Range", ("offset", "length"))
class XEP_0234:
class XEP_0234(BaseApplicationHandler):
# TODO: assure everything is closed when file is sent or session terminate is received
# TODO: call self._f.unregister when unloading order will be managing (i.e. when
# dependencies will be unloaded at the end)
@ -407,6 +411,67 @@ class XEP_0234:
# jingle callbacks
async def jingle_preflight(
self,
client: SatXMPPEntity,
session: dict,
description_elt: domish.Element
) -> None:
"""Perform preflight checks for an incoming call session.
Check if the calls is audio only or audio/video, then, prompts the user for
confirmation.
@param client: The client instance.
@param session: Jingle session.
@param description_elt: The description element. It's parent attribute is used to
determine check siblings to see if it's an audio only or audio/video call.
@raises exceptions.CancelError: If the user doesn't accept the incoming call.
"""
session_id = session["id"]
peer_jid = session["peer_jid"]
# FIXME: has been moved from XEP-0353, but it doesn't handle correctly file
# transfer (metadata are not used). We must check with other clients what is
# actually send, and if XEP-0353 is used, and do a better integration.
if client.roster and peer_jid.userhostJID() not in client.roster:
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=self.human_name)
confirm_title = D_("File sent from an unknown contact")
action_type = C.META_TYPE_NOT_IN_ROSTER_LEAK
else:
confirm_msg = D_(
"{peer_jid} wants to send a file ro you, do you accept?"
).format(peer_jid=peer_jid)
confirm_title = D_("File Proposed")
action_type = C.META_TYPE_FILE
accepted = await xml_tools.defer_confirm(
self.host,
confirm_msg,
confirm_title,
profile=client.profile,
action_extra={
"type": action_type,
"session_id": session_id,
"from_jid": peer_jid.full(),
},
)
if accepted:
session["file_accepted"] = True
return accepted
async def jingle_preflight_cancel(
self,
client: SatXMPPEntity,
session: dict,
cancel_error: exceptions.CancelError
) -> None:
pass
def jingle_description_elt(
self, client, session, content_name, filepath, name, extra, progress_id_d
):

View file

@ -49,6 +49,17 @@ PLUGIN_INFO = {
}
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 @@ class XEP_0353:
# 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 @@ class XEP_0353:
# 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 @@ class XEP_0353:
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 @@ class XEP_0353:
)
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 @@ class XEP_0353:
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 @@ class XEP_0353:
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)