diff --git a/libervia/backend/plugins/plugin_xep_0166/__init__.py b/libervia/backend/plugins/plugin_xep_0166/__init__.py
index 3377bcb2..1ee1e9d2 100644
--- a/libervia/backend/plugins/plugin_xep_0166/__init__.py
+++ b/libervia/backend/plugins/plugin_xep_0166/__init__.py
@@ -130,6 +130,14 @@ class XEP_0166:
XEP_0166.TRANSPORT_DATAGRAM: [],
XEP_0166.TRANSPORT_STREAMING: [],
}
+ host.bridge.add_method(
+ "jingle_terminate",
+ ".plugin",
+ in_sign="ssss",
+ out_sign="",
+ method=self._terminate,
+ async_=True,
+ )
def profile_connected(self, client):
client.jingle_sessions = {} # key = sid, value = session_data
@@ -152,8 +160,56 @@ class XEP_0166:
f"No session with SID {session_id} found"
)
+ def create_session(
+ self,
+ client: SatXMPPEntity,
+ sid: str,
+ role: str,
+ peer_jid: jid.JID,
+ local_jid: jid.JID|None = None,
+ **kwargs
+ ) -> dict:
+ """Create a new jingle session.
- def _del_session(self, client, sid):
+ @param client: The client entity.
+ @param sid: Session ID.
+ @param role: Session role (initiator or responder).
+ @param peer_jid: JID of the peer.
+ @param local_jid: JID of the local entity.
+ If None, defaults to client.jid.
+ @param extra_data: Additional data to be added to the session. Defaults to None.
+
+ @return: The created session.
+
+ @raise ValueError: If the provided role is neither initiator nor responder.
+ """
+ # TODO: session cleaning after timeout ?
+ if role not in [XEP_0166.ROLE_INITIATOR, XEP_0166.ROLE_RESPONDER]:
+ raise ValueError(f"Invalid role {role}. Expected initiator or responder.")
+
+
+ session_data = {
+ "id": sid,
+ "state": STATE_PENDING,
+ "initiator": client.jid if role == XEP_0166.ROLE_INITIATOR else peer_jid,
+ "role": role,
+ "local_jid": local_jid or client.jid,
+ "peer_jid": peer_jid,
+ "started": time.time(),
+ "contents": {}
+ }
+
+ # If extra kw args are provided, merge them into the session_data
+ if kwargs:
+ session_data.update(kwargs)
+
+ # Add the session to the client's jingle sessions
+ client.jingle_sessions[sid] = session_data
+
+ return session_data
+
+
+ def delete_session(self, client, sid):
try:
del client.jingle_sessions[sid]
except KeyError:
@@ -191,7 +247,7 @@ class XEP_0166:
if jingle_condition is not None:
iq_elt.error.addElement((NS_JINGLE_ERROR, jingle_condition))
if error.STANZA_CONDITIONS[error_condition]["type"] == "cancel" and sid:
- self._del_session(client, sid)
+ self.delete_session(client, sid)
log.warning(
"Error while managing jingle session, cancelling: {condition}".format(
condition=error_condition
@@ -202,26 +258,64 @@ class XEP_0166:
def _terminate_eb(self, failure_):
log.warning(_("Error while terminating session: {msg}").format(msg=failure_))
- def terminate(self, client, reason, session, text=None):
+ def _terminate(
+ self,
+ session_id: str,
+ reason: str,
+ reason_txt: str,
+ profile: str
+ ) -> defer.Deferred:
+ client = self.host.get_client(profile)
+ session = self.get_session(client, session_id)
+ if reason not in ("", "cancel", "decline", "busy"):
+ raise ValueError(
+ 'only "cancel", "decline" and "busy" and empty value are allowed'
+ )
+ return self.terminate(
+ client,
+ reason or None,
+ session,
+ text=reason_txt or None
+ )
+
+ def terminate(
+ self,
+ client: SatXMPPEntity,
+ reason: str|list[domish.Element]|None,
+ session: dict,
+ text: str|None = None
+ ) -> defer.Deferred:
"""Terminate the session
send the session-terminate action, and delete the session data
- @param reason(unicode, list[domish.Element]): if unicode, will be transformed to an element
+ @param reason: if unicode, will be transformed to an element
if a list of element, add them as children of the element
- @param session(dict): data of the session
+ @param session: data of the session
"""
iq_elt, jingle_elt = self._build_jingle_elt(
client, session, XEP_0166.A_SESSION_TERMINATE
)
- reason_elt = jingle_elt.addElement("reason")
- if isinstance(reason, str):
- reason_elt.addElement(reason)
+ if reason is not None:
+ reason_elt = jingle_elt.addElement("reason")
+ if isinstance(reason, str):
+ reason_elt.addElement(reason)
+ else:
+ for elt in reason:
+ reason_elt.addChild(elt)
else:
- for elt in reason:
- reason_elt.addChild(elt)
+ reason_elt = None
if text is not None:
+ if reason_elt is None:
+ raise ValueError(
+ "You have to specify a reason if text is specified"
+ )
reason_elt.addElement("text", content=text)
- self._del_session(client, session["id"])
+ if not self.host.trigger.point(
+ "XEP-0166_terminate",
+ client, session, reason_elt
+ ):
+ return defer.succeed(None)
+ self.delete_session(client, session["id"])
d = iq_elt.send()
d.addErrback(self._terminate_eb)
return d
@@ -239,7 +333,7 @@ class XEP_0166:
failure_=failure_.value
)
)
- self._del_session(client, sid)
+ self.delete_session(client, sid)
def _jingle_error_cb(self, failure_, session, request, client):
"""Called when something is going wrong while parsing jingle request
@@ -462,6 +556,7 @@ class XEP_0166:
peer_jid: jid.JID,
contents: List[dict],
encrypted: bool = False,
+ sid: str|None = None,
**extra_data: Any
) -> str:
"""Send a session initiation request
@@ -474,32 +569,27 @@ class XEP_0166:
- transport_type(str): type of transport to use (see XEP-0166 §8)
default to TRANSPORT_STREAMING
- name(str): name of the content
- - senders(str): One of XEP_0166.ROLE_INITIATOR, XEP_0166.ROLE_RESPONDER, both or none
- default to BOTH (see XEP-0166 §7.3)
+ - senders(str): One of XEP_0166.ROLE_INITIATOR, XEP_0166.ROLE_RESPONDER,
+ both or none
+ Defaults to BOTH (see XEP-0166 §7.3)
- app_args(list): args to pass to the application plugin
- app_kwargs(dict): keyword args to pass to the application plugin
@param encrypted: if True, session must be encrypted and "encryption" must be set
to all content data of session
- @return: jingle session id
+ @param sid: Session ID.
+ If None, one will be generated (and used as return value)
+ @return: Sesson ID
"""
assert contents # there must be at least one content
if (peer_jid == client.jid
or client.is_component and peer_jid.host == client.jid.host):
raise ValueError(_("You can't do a jingle session with yourself"))
- initiator = client.jid
- sid = str(uuid.uuid4())
- # TODO: session cleaning after timeout ?
- session = client.jingle_sessions[sid] = {
- "id": sid,
- "state": STATE_PENDING,
- "initiator": initiator,
- "role": XEP_0166.ROLE_INITIATOR,
- "local_jid": client.jid,
- "peer_jid": peer_jid,
- "started": time.time(),
- "contents": {},
- **extra_data,
- }
+ if sid is None:
+ sid = str(uuid.uuid4())
+ session = self.create_session(
+ client, sid, XEP_0166.ROLE_INITIATOR, peer_jid, **extra_data
+ )
+ initiator = session["initiator"]
if not await self.host.trigger.async_point(
"XEP-0166_initiate",
@@ -679,7 +769,8 @@ class XEP_0166:
pass
elif action == XEP_0166.A_SESSION_TERMINATE:
log.debug(
- "ignoring session terminate action (inexisting session id): {request_id} [{profile}]".format(
+ "ignoring session terminate action (inexisting session id): "
+ "{request_id} [{profile}]".format(
request_id=sid, profile=client.profile
)
)
@@ -693,21 +784,19 @@ class XEP_0166:
self.sendError(client, "item-not-found", None, request, "unknown-session")
return
- session = client.jingle_sessions[sid] = {
- "id": sid,
- "state": STATE_PENDING,
- "initiator": peer_jid,
- "role": XEP_0166.ROLE_RESPONDER,
- # we store local_jid using request['to'] because for a component the jid
- # used may not be client.jid (if a local part is used).
- "local_jid": jid.JID(request['to']),
- "peer_jid": peer_jid,
- "started": time.time(),
- }
+ try:
+ session = self.get_session(client, sid)
+ except exceptions.NotFound:
+ # XXX: we store local_jid using request['to'] because for a component the
+ # jid used may not be client.jid (if a local part is used).
+ session = self.create_session(
+ client, sid, XEP_0166.ROLE_RESPONDER, peer_jid, jid.JID(request['to'])
+ )
else:
if session["peer_jid"] != peer_jid:
log.warning(
- "sid conflict ({}), the jid doesn't match. Can be a collision, a hack attempt, or a bad sid generation".format(
+ "sid conflict ({}), the jid doesn't match. Can be a collision, a "
+ "hack attempt, or a bad sid generation".format(
sid
)
)
@@ -721,7 +810,7 @@ class XEP_0166:
if action == XEP_0166.A_SESSION_INITIATE:
await self.on_session_initiate(client, request, jingle_elt, session)
elif action == XEP_0166.A_SESSION_TERMINATE:
- self.on_session_terminate(client, request, jingle_elt, session)
+ await self.on_session_terminate(client, request, jingle_elt, session)
elif action == XEP_0166.A_SESSION_ACCEPT:
await self.on_session_accept(client, request, jingle_elt, session)
elif action == XEP_0166.A_SESSION_INFO:
@@ -969,11 +1058,11 @@ class XEP_0166:
@param jingle_elt(domish.Element): element
@param session(dict): session data
"""
- if "contents" in session:
+ contents_dict = session["contents"]
+ if contents_dict:
raise exceptions.InternalError(
- "Contents dict should not already exist at this point"
+ "Contents dict should not already be set at this point"
)
- session["contents"] = contents_dict = {}
try:
self._parse_elements(
@@ -1107,14 +1196,47 @@ class XEP_0166:
d_list.addErrback(self._iq_error, session["id"], client)
return d_list
- def on_session_terminate(self, client, request, jingle_elt, session):
- # TODO: check reason, display a message to user if needed
- log.debug(f"Jingle Session {session['id']} terminated")
+ def get_reason_elt(self, parent_elt: domish.Element) -> domish.Element:
+ """Find a element in parent_elt
+
+ if none is found, add an empty one to the element
+ @return: the element
+ """
try:
- reason_elt = next(jingle_elt.elements(NS_JINGLE, "reason"))
+ return next(parent_elt.elements(NS_JINGLE, "reason"))
except StopIteration:
log.warning("No reason given for session termination")
- reason_elt = jingle_elt.addElement("reason")
+ reason_elt = parent_elt.addElement("reason")
+ return reason_elt
+
+ def parse_reason_elt(self, reason_elt: domish.Element) -> tuple[str|None, str|None]:
+ """Parse a element
+
+ @return: reason found, and text if any
+ """
+ reason, text = None, None
+ for elt in reason_elt.elements():
+ if elt.uri == NS_JINGLE:
+ if elt.name == "text":
+ text = str(elt)
+ else:
+ reason = elt.name
+
+ if reason is None:
+ log.debug("no reason specified,")
+
+ return reason, text
+
+ async def on_session_terminate(
+ self,
+ client: SatXMPPEntity,
+ request: domish.Element,
+ jingle_elt: domish.Element,
+ session: dict
+ ) -> None:
+ # TODO: check reason, display a message to user if needed
+ log.debug(f"Jingle Session {session['id']} terminated")
+ reason_elt = self.get_reason_elt(jingle_elt)
terminate_defers = self._call_plugins(
client,
@@ -1129,7 +1251,7 @@ class XEP_0166:
)
terminate_dlist = defer.DeferredList(terminate_defers)
- terminate_dlist.addCallback(lambda __: self._del_session(client, session["id"]))
+ terminate_dlist.addCallback(lambda __: self.delete_session(client, session["id"]))
client.send(xmlstream.toResponse(request, "result"))
async def on_session_accept(self, client, request, jingle_elt, session):