From 8d4d2e222a01f57634171a838acba4d8cfaa4c27 Mon Sep 17 00:00:00 2001 From: Goffi Date: Thu, 24 Sep 2026 11:12:52 +0200 Subject: [PATCH] plugin XEP-0482: Call Invites implementation --- libervia/backend/plugins/plugin_xep_0482.py | 134 ++++++++++++++++++++ 1 file changed, 134 insertions(+) create mode 100644 libervia/backend/plugins/plugin_xep_0482.py diff --git a/libervia/backend/plugins/plugin_xep_0482.py b/libervia/backend/plugins/plugin_xep_0482.py new file mode 100644 index 00000000..284d49f7 --- /dev/null +++ b/libervia/backend/plugins/plugin_xep_0482.py @@ -0,0 +1,134 @@ +#!/usr/bin/env python3 + +# Libervia plugin for call invites (XEP-0482) +# Copyright (C) 2009-2026 Jérôme Poisson (goffi@goffi.org) + +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. + +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +import uuid + +from twisted.words.protocols.jabber import jid +from twisted.words.xish import domish + +from libervia.backend import G +from libervia.backend.core.constants import Const as C +from libervia.backend.core.core_types import SatXMPPEntity +from libervia.backend.core.i18n import _ +from libervia.backend.core.log import getLogger + +log = getLogger(__name__) + +NS_CALL_INVITES = "urn:xmpp:call-invites:0" + +PLUGIN_INFO = { + C.PI_NAME: "Call Invites", + C.PI_IMPORT_NAME: "XEP-0482", + C.PI_TYPE: "XEP", + C.PI_PROTOCOLS: ["XEP-0482"], + C.PI_DEPENDENCIES: [], + C.PI_MAIN: "XEP_0482", + C.PI_HANDLER: "no", + C.PI_DESCRIPTION: _("Invitation handling for calls."), +} + + +class XEP_0482: + + def __init__(self, host): + log.info(f'Plugin "{PLUGIN_INFO[C.PI_NAME]}" initialization') + host.register_namespace("call-invites", NS_CALL_INVITES) + host.trigger.add("message_received", self._message_received_trigger) + + def _build_reply(self, name: str, id_s: str) -> tuple[domish.Element, domish.Element]: + """Build a or message + + @param name: name of the call-invites element to add + @param id_s: id of the invite message + @return: the message element and the answer element + """ + message_elt = domish.Element((None, "message")) + message_elt["type"] = "chat" + answer_elt = message_elt.addElement((NS_CALL_INVITES, name)) + answer_elt["id"] = id_s + return message_elt, answer_elt + + def invite( + self, + client: SatXMPPEntity, + to_jid: jid.JID, + method_elts: list[domish.Element], + ) -> None: + """Send a call invite to a given JID + + @param client: The client instance. + @param to_jid: JID of the user to invite + @param method_elts: elements describing the ways to join the call, they are added + as children of the element + """ + message_elt = domish.Element((None, "message")) + message_elt["to"] = to_jid.full() + message_elt["type"] = "chat" + message_id_s = str(uuid.uuid4()) + message_elt["id"] = message_id_s + invite_elt = message_elt.addElement((NS_CALL_INVITES, "invite")) + # FIXME: workaround for Dino bug (https://github.com/dino/dino/issues/1902) + invite_elt["id"] = message_id_s + for method_elt in method_elts: + invite_elt.addChild(method_elt) + client.send(message_elt) + + def accept( + self, + client: SatXMPPEntity, + to_jid: jid.JID, + invite_id_s: str, + method_elt: domish.Element, + ) -> None: + """Accept a call invite + + @param client: The client instance. + @param to_jid: JID of the user who sent the invite + @param invite_id_s: id of the invite message + @param method_elt: element describing the connection method used to join, as + defined by the specification of this method + """ + message_elt, answer_elt = self._build_reply("accept", invite_id_s) + message_elt["to"] = to_jid.full() + answer_elt.addChild(method_elt) + client.send(message_elt) + + def reject(self, client: SatXMPPEntity, to_jid: jid.JID, invite_id_s: str) -> None: + """Reject a call invite + + @param client: The client instance. + @param to_jid: JID of the user who sent the invite + @param invite_id_s: id of the invite message + """ + message_elt, _ = self._build_reply("reject", invite_id_s) + message_elt["to"] = to_jid.full() + client.send(message_elt) + + def _message_received_trigger(self, client, message_elt, post_treat) -> bool: + """Check if a call invite is in the message""" + invite_elt = next(message_elt.elements(NS_CALL_INVITES, "invite"), None) + if invite_elt is None: + return True + log.info(f"Call invite received from {message_elt['from']} [{client.profile}]") + if G.host.trigger.point( + "XEP-0482_invite_received", client, message_elt, invite_elt + ): + log.warning( + f"Call invite received, but no plugin handled it: {message_elt.toXml()}" + ) + return False