plugin XEP-0272: update to v0.2.0 + fixes:
- use the proper `urn:xmpp:jingle:muji:0` namespace - replace XEP-0249 invitations with XEP-0482 - the dedicated `META_TYPE_GROUP_CALL` subtype is now used for the confirm action (instead of `META_TYPE_MUC_INVITATION`) - handle MUJI invitation: on accept, join the room, start preparing and answer with the connection method element - add the `<muji>` element to outgoing Jingle IQs - propagate the call type (audio/video) in the MUJI invites (the `type` attribute), so the invited clients set up the proper local stream - the joining participant initiates the sessions with the other participants, using their real JIDs, extracted from the presence (the room is set non-anonymous for that) - only trust the `<muji>` element of a `session-initiate` from a member of the room - add a `call_group_end` bridge method to clean the MUJI session - typo fix in `META_TYPE_MUC_INVIRATION` (`R` instead of `T`)
This commit is contained in:
parent
c4e3a2b015
commit
0c6f852ce9
4 changed files with 358 additions and 94 deletions
|
|
@ -442,7 +442,7 @@ class Const:
|
|||
META_TYPE_CALL = "call"
|
||||
META_TYPE_GROUP_CALL = "group-call"
|
||||
META_TYPE_REMOTE_CONTROL = "remote-control"
|
||||
META_TYPE_MUC_INVIRATION = "muc-invitation"
|
||||
META_TYPE_MUC_INVITATION = "muc-invitation"
|
||||
META_TYPE_OVERWRITE = "overwrite"
|
||||
META_TYPE_NOT_IN_ROSTER_LEAK = "not_in_roster_leak"
|
||||
META_SUBTYPE_CALL_AUDIO = "audio"
|
||||
|
|
|
|||
|
|
@ -198,7 +198,7 @@ class XEP_0249:
|
|||
else: # leave the default value here
|
||||
action_extra = {
|
||||
"type": C.META_TYPE_CONFIRM,
|
||||
"subtype": C.META_TYPE_MUC_INVIRATION,
|
||||
"subtype": C.META_TYPE_MUC_INVITATION,
|
||||
"from_jid": from_jid_s,
|
||||
"room_jid": room_jid_s,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@
|
|||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from typing import cast
|
||||
from twisted.internet import defer
|
||||
from twisted.words.protocols.jabber import jid
|
||||
from twisted.words.protocols.jabber.xmlstream import XMPPHandler
|
||||
|
|
@ -26,15 +27,15 @@ 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 _
|
||||
from libervia.backend.core.i18n import D_, _
|
||||
from libervia.backend.core.log import getLogger
|
||||
from libervia.backend.plugins import plugin_xep_0166
|
||||
from libervia.backend.plugins import plugin_xep_0167
|
||||
from libervia.backend.plugins.plugin_xep_0167 import mapping
|
||||
from libervia.backend.plugins.plugin_xep_0045 import XEP_0045
|
||||
from libervia.backend.plugins.plugin_xep_0166 import XEP_0166
|
||||
from libervia.backend.plugins.plugin_xep_0167 import XEP_0167, mapping
|
||||
from libervia.backend.plugins.plugin_xep_0482 import XEP_0482
|
||||
from libervia.backend.tools import xml_tools
|
||||
from libervia.backend.tools.common import data_format
|
||||
|
||||
from . import plugin_xep_0045, plugin_xep_0249
|
||||
|
||||
log = getLogger(__name__)
|
||||
|
||||
|
||||
|
|
@ -43,16 +44,16 @@ PLUGIN_INFO = {
|
|||
C.PI_IMPORT_NAME: "XEP-0272",
|
||||
C.PI_TYPE: "XEP",
|
||||
C.PI_PROTOCOLS: ["XEP-0272"],
|
||||
C.PI_DEPENDENCIES: ["XEP-0045", "XEP-0166", "XEP-0167", "XEP-0249"],
|
||||
C.PI_DEPENDENCIES: ["XEP-0045", "XEP-0166", "XEP-0167", "XEP-0482"],
|
||||
C.PI_MAIN: "XEP_0272",
|
||||
C.PI_HANDLER: "yes",
|
||||
C.PI_DESCRIPTION: _(
|
||||
"Allow to run A/V conference with several participant using P2P connections. "
|
||||
"The number of participant must not be to big to make it work correctly."
|
||||
"The number of participant must not be too big to make it work correctly."
|
||||
),
|
||||
}
|
||||
|
||||
NS_MUJI = "http://telepathy.freedesktop.org/muji"
|
||||
NS_MUJI = "urn:xmpp:jingle:muji:0"
|
||||
PRESENCE_MUJI = f'/presence/muji[@xmlns="{NS_MUJI}"]'
|
||||
|
||||
|
||||
|
|
@ -62,10 +63,13 @@ class XEP_0272:
|
|||
log.info(f'Plugin "{PLUGIN_INFO[C.PI_NAME]}" initialization')
|
||||
self.host = host
|
||||
host.register_namespace("muji", NS_MUJI)
|
||||
self._muc: plugin_xep_0045.XEP_0045 = host.plugins["XEP-0045"]
|
||||
self._muc_invite: plugin_xep_0249.XEP_0249 = host.plugins["XEP-0249"]
|
||||
self._j: plugin_xep_0166.XEP_0166 = host.plugins["XEP-0166"]
|
||||
self._rtp: plugin_xep_0167.XEP_0167 = host.plugins["XEP-0167"]
|
||||
self._muc = cast(XEP_0045, host.plugins["XEP-0045"])
|
||||
self._j = cast(XEP_0166, host.plugins["XEP-0166"])
|
||||
self._rtp = cast(XEP_0167, host.plugins["XEP-0167"])
|
||||
self._call_invites = cast(XEP_0482, host.plugins["XEP-0482"])
|
||||
host.trigger.add("XEP-0166_initiate_elt_built", self._on_jingle_elt_built)
|
||||
host.trigger.add("XEP-0166_on_session_initiate", self._on_session_initiate)
|
||||
host.trigger.add("XEP-0482_invite_received", self._on_call_invite_received)
|
||||
host.bridge.add_method(
|
||||
"call_group_start",
|
||||
".plugin",
|
||||
|
|
@ -82,6 +86,14 @@ class XEP_0272:
|
|||
method=self._call_group_data_set,
|
||||
async_=True,
|
||||
)
|
||||
host.bridge.add_method(
|
||||
"call_group_end",
|
||||
".plugin",
|
||||
in_sign="ss",
|
||||
out_sign="",
|
||||
method=self._call_group_end,
|
||||
async_=True,
|
||||
)
|
||||
host.bridge.add_signal("call_group_setup", ".plugin", signature="sss")
|
||||
|
||||
def get_handler(self, client):
|
||||
|
|
@ -115,15 +127,30 @@ class XEP_0272:
|
|||
)
|
||||
)
|
||||
|
||||
async def on_room_join(self, room: muc.Room, user: muc.User) -> None:
|
||||
pass
|
||||
def _call_group_end(self, room_jid_s: str, profile_key: str) -> None:
|
||||
client = self.host.get_client(profile_key)
|
||||
defer.ensureDeferred(self.call_group_end(client, jid.JID(room_jid_s)))
|
||||
|
||||
async def on_room_left(self, room: muc.Room, user: muc.User) -> None:
|
||||
pass
|
||||
async def call_group_end(self, client: SatXMPPEntity, room_jid: jid.JID) -> None:
|
||||
"""End and clean call."""
|
||||
try:
|
||||
room = self._muc.get_room(client, room_jid)
|
||||
except exceptions.NotFound:
|
||||
log.warning(f"Ignoring MUJI end for unknown room: {room_jid}")
|
||||
return
|
||||
# We remove MUJI information from presence, as required by spec.
|
||||
presence_elt = domish.Element((None, "presence"))
|
||||
presence_elt["from"] = client.jid.full()
|
||||
presence_elt["to"] = room.roomJID.full()
|
||||
await client.presence.send(presence_elt)
|
||||
muji_data = self.get_muji_data(room)
|
||||
muji_data.clear()
|
||||
muji_data["to_call"] = set()
|
||||
|
||||
def on_muji_request(
|
||||
self, presence_elt: domish.Element, client: SatXMPPEntity
|
||||
) -> None:
|
||||
"""Handle the MUJI element of a presence received in a MUC room"""
|
||||
from_jid = jid.JID(presence_elt["from"])
|
||||
room_jid = from_jid.userhostJID()
|
||||
try:
|
||||
|
|
@ -133,19 +160,11 @@ class XEP_0272:
|
|||
f"Ignoring MUJI element from an unknown room: {presence_elt.toXml()}"
|
||||
)
|
||||
return
|
||||
if from_jid == self._muc.get_room_user_jid(client, room_jid):
|
||||
own_jid = True
|
||||
else:
|
||||
own_jid = False
|
||||
own_jid = from_jid == self._muc.get_room_user_jid(client, room_jid)
|
||||
muji_data = self.get_muji_data(room)
|
||||
muji_elt = presence_elt.muji
|
||||
assert muji_elt is not None
|
||||
try:
|
||||
next(muji_elt.elements(NS_MUJI, "preparing"))
|
||||
except StopIteration:
|
||||
preparing_state = False
|
||||
else:
|
||||
preparing_state = True
|
||||
preparing_state = next(muji_elt.elements(NS_MUJI, "preparing"), None) is not None
|
||||
|
||||
if preparing_state:
|
||||
if own_jid:
|
||||
|
|
@ -154,24 +173,34 @@ class XEP_0272:
|
|||
self.try_to_finish_preparation(client, room, muji_data)
|
||||
elif not muji_data.get("done_collecting", False):
|
||||
# other entities currently doing preparation
|
||||
preparing_jids = muji_data["preparing_jids"]
|
||||
preparing_jids.add(from_jid)
|
||||
muji_data["preparing_jids"].add(from_jid)
|
||||
elif not own_jid:
|
||||
done_preparing = muji_data.get("done_preparing", False)
|
||||
# if we are still in preparation, we remove the JID from data data we are
|
||||
# still waiting for, and we check if we can finish the preparation.
|
||||
if not done_preparing:
|
||||
allowed_payloads = muji_data.setdefault("allowed_payloads")
|
||||
# TODO: check allowed_payloads
|
||||
preparing_jids = muji_data["preparing_jids"]
|
||||
preparing_jids.discard(from_jid)
|
||||
log.debug(
|
||||
f"[{client.profile}] received call data for {from_jid}.\n"
|
||||
f"{preparing_jids=}"
|
||||
)
|
||||
muji_data["to_call"].add(from_jid)
|
||||
# a peer has advertised its data
|
||||
if not muji_data.get("done_preparing", False):
|
||||
muji_data["preparing_jids"].discard(from_jid)
|
||||
log.debug(f"[{client.profile}] received call data for {from_jid}.")
|
||||
# per spec, the joining participant initiates the sessions with all
|
||||
# the other participants; the sender's real JID is in the presence
|
||||
# itself (the room roster is not up to date yet for a joiner)
|
||||
user_jid = self._get_presence_user_jid(presence_elt)
|
||||
if user_jid is None:
|
||||
raise exceptions.ExternalRequestError(
|
||||
f"can't get the real JID of {from_jid}: the MUJI room is "
|
||||
"anonymous"
|
||||
)
|
||||
muji_data["to_call"].add(user_jid)
|
||||
self.try_to_finish_preparation(client, room, muji_data)
|
||||
|
||||
def _get_presence_user_jid(self, presence_elt: domish.Element) -> jid.JID | None:
|
||||
"""Get the sender's real JID from the muc#user item of a room presence."""
|
||||
x_elt = next(presence_elt.elements(muc.NS_MUC_USER, "x"), None)
|
||||
if x_elt is None:
|
||||
return None
|
||||
item_elt = next(x_elt.elements(muc.NS_MUC_USER, "item"), None)
|
||||
if item_elt is None or not item_elt.hasAttribute("jid"):
|
||||
return None
|
||||
return jid.JID(item_elt["jid"])
|
||||
|
||||
def try_to_finish_preparation(
|
||||
self, client: SatXMPPEntity, room: muc.Room, muji_data: dict
|
||||
) -> None:
|
||||
|
|
@ -180,15 +209,19 @@ class XEP_0272:
|
|||
This method checks if preparations of other JIDs needs to be waited, and if not,
|
||||
finishes our own preparation.
|
||||
"""
|
||||
if not muji_data.get("done_collecting", False):
|
||||
# we have not received the broadcast of our own preparation message yet;
|
||||
# a joiner receives the other occupants' data presences (join processing)
|
||||
# before that echo, so waiting for it guarantees that all their data has
|
||||
# been collected
|
||||
return
|
||||
preparing_jids = muji_data.get("preparing_jids")
|
||||
if not preparing_jids:
|
||||
# No preparation left to wait, we can finish our own.
|
||||
muji_data = self.get_muji_data(room)
|
||||
muji_data["done_preparing"] = True
|
||||
|
||||
log.debug(f"[{client.profile}] Done preparing.")
|
||||
|
||||
# We ask frontend to initiate the session, so we know supported codecs.
|
||||
# ask the frontend to advertise our data and to initiate the sessions
|
||||
self.host.bridge.call_group_setup(
|
||||
room.roomJID.full(),
|
||||
data_format.serialise(
|
||||
|
|
@ -203,10 +236,10 @@ class XEP_0272:
|
|||
room_jid: jid.JID,
|
||||
call_data: dict,
|
||||
) -> None:
|
||||
"""Called when frontends has prepared group call.
|
||||
"""Advertise the group call data on the MUC.
|
||||
|
||||
Group call data will be advertised on the MUC, and call will be initiated with all
|
||||
participant which where in preparing state when we made our own preparation.
|
||||
Per spec, the late joiner initiates the sessions with the participants
|
||||
which joined before it.
|
||||
|
||||
@param client: SatXMPPEntity instance.
|
||||
@param room_jid: JID of the room used for MUJI coordination.
|
||||
|
|
@ -215,7 +248,7 @@ class XEP_0272:
|
|||
try:
|
||||
room = self._muc.get_room(client, room_jid)
|
||||
except exceptions.NotFound:
|
||||
log.warning(f"Ignoring MUJI element from an unknown room: {room_jid}")
|
||||
log.warning(f"Ignoring MUJI data for unknown room: {room_jid}")
|
||||
return
|
||||
sdp_data = mapping.parse_sdp(call_data["sdp"], self._j.ROLE_INITIATOR)
|
||||
presence_elt, muji_elt = self.generate_presence_and_muji(client, room)
|
||||
|
|
@ -240,7 +273,102 @@ class XEP_0272:
|
|||
for elt in to_remove:
|
||||
description_elt.children.remove(elt)
|
||||
|
||||
await client.a_send(presence_elt)
|
||||
await client.presence.send(presence_elt)
|
||||
|
||||
async def _on_jingle_elt_built(
|
||||
self, client: SatXMPPEntity, session: dict, iq_elt, jingle_elt: domish.Element
|
||||
) -> bool:
|
||||
"""Add the <muji> element to the Jingle IQ of a MUJI call"""
|
||||
metadata = session.get("metadata")
|
||||
room_jid_s = metadata.get("muji_room") if metadata else None
|
||||
if room_jid_s is not None:
|
||||
muji_elt = jingle_elt.addElement((NS_MUJI, "muji"))
|
||||
muji_elt["room"] = room_jid_s
|
||||
return True
|
||||
|
||||
async def _on_session_initiate(
|
||||
self,
|
||||
client: SatXMPPEntity,
|
||||
session: dict,
|
||||
request: domish.Element,
|
||||
jingle_elt: domish.Element,
|
||||
) -> bool:
|
||||
"""Store MUJI room from a session-initiate request, if any.
|
||||
|
||||
Per spec, the room is only trusted if the sender is a member of it.
|
||||
"""
|
||||
muji_elt = next(jingle_elt.elements(NS_MUJI, "muji"), None)
|
||||
if muji_elt is None:
|
||||
return True
|
||||
room_jid = jid.JID(muji_elt["room"])
|
||||
if not self._muc.is_jid_in_room(client, room_jid, session["peer_jid"]):
|
||||
log.warning(
|
||||
f"Ignoring MUJI room {room_jid}: {session['peer_jid']} is not a member"
|
||||
)
|
||||
return True
|
||||
session["muji_room"] = muji_elt["room"]
|
||||
return True
|
||||
|
||||
def _on_call_invite_received(self, client, message_elt, invite_elt) -> bool:
|
||||
"""Handle a XEP-0482 call invite for a MUJI conference"""
|
||||
muji_elt = next(invite_elt.elements(NS_MUJI, "muji"), None)
|
||||
if muji_elt is None:
|
||||
return True
|
||||
try:
|
||||
room_jid_s = muji_elt["room"]
|
||||
except KeyError:
|
||||
log.warning(f"Ignoring MUJI invite without room: {message_elt.toXml()}")
|
||||
return True
|
||||
from_jid_s = message_elt["from"]
|
||||
log.info(
|
||||
f"MUJI call invite received from {from_jid_s} for room {room_jid_s} "
|
||||
f"[{client.profile}]"
|
||||
)
|
||||
message_id = message_elt["id"]
|
||||
try:
|
||||
sub_type = muji_elt["type"]
|
||||
except KeyError:
|
||||
sub_type = C.META_SUBTYPE_CALL_AUDIO
|
||||
action_extra = {
|
||||
"type": C.META_TYPE_CONFIRM,
|
||||
"subtype": C.META_TYPE_GROUP_CALL,
|
||||
"from_jid": from_jid_s,
|
||||
"room_jid": room_jid_s,
|
||||
"sub_type": sub_type,
|
||||
}
|
||||
confirm_msg = D_(
|
||||
"You have been invited by {user} to join the room {room}. Do you accept?"
|
||||
).format(user=from_jid_s, room=room_jid_s)
|
||||
confirm_title = _("Group call invitation")
|
||||
d = xml_tools.defer_confirm(
|
||||
self.host,
|
||||
confirm_msg,
|
||||
confirm_title,
|
||||
profile=client.profile,
|
||||
action_extra=action_extra,
|
||||
)
|
||||
|
||||
def answer_cb(accepted: bool) -> None:
|
||||
# the accept contains the connection method used to join (XEP-0482)
|
||||
method_elt = domish.Element((NS_MUJI, "muji"))
|
||||
method_elt["room"] = room_jid_s
|
||||
if accepted:
|
||||
self._call_invites.accept(
|
||||
client, jid.JID(from_jid_s), message_id, method_elt
|
||||
)
|
||||
# join the MUJI room and start preparing the call
|
||||
defer.ensureDeferred(self._join_and_prepare(client, jid.JID(room_jid_s)))
|
||||
else:
|
||||
self._call_invites.reject(client, jid.JID(from_jid_s), message_id)
|
||||
|
||||
d.addCallback(answer_cb)
|
||||
return False
|
||||
|
||||
async def _join_and_prepare(self, client: SatXMPPEntity, room_jid: jid.JID) -> None:
|
||||
"""Join a MUJI room and start preparing (after accepting an invitation)"""
|
||||
room = await self._muc.join(client, room_jid, {})
|
||||
if room is not None:
|
||||
await self.start_preparation(client, room)
|
||||
|
||||
def get_muji_data(self, room: muc.Room) -> dict:
|
||||
"""Get MUJI related data for this room
|
||||
|
|
@ -265,10 +393,10 @@ class XEP_0272:
|
|||
return presence_elt, muji_elt
|
||||
|
||||
async def start_preparation(self, client: SatXMPPEntity, room: muc.Room) -> None:
|
||||
"""Start preparation of MUJI"""
|
||||
"""Advertise the MUJI preparation state on the MUC room"""
|
||||
presence_elt, muji_elt = self.generate_presence_and_muji(client, room)
|
||||
muji_elt.addElement("preparing")
|
||||
await client.a_send(presence_elt)
|
||||
await client.presence.send(presence_elt)
|
||||
|
||||
async def call_group_start(
|
||||
self,
|
||||
|
|
@ -278,18 +406,20 @@ class XEP_0272:
|
|||
) -> dict:
|
||||
"""Initiate a group call with the given peers.
|
||||
|
||||
A MUC room will be created, and people in ``list_entities`` will be invited. MUJI
|
||||
session will then been started which each of them upon they arrival.
|
||||
@param entities: JID of the peer to initiate a call session with.
|
||||
A MUC room is created, made non-anonymous, and the peers are invited in it.
|
||||
|
||||
@param client: The client instance.
|
||||
@param entities: JIDs of the peers to invite.
|
||||
@param extra: Extra data.
|
||||
|
||||
@return: group call data, with the following keys:
|
||||
|
||||
``room_jid``
|
||||
MUC room where the MUJI coordination is done. It may also be used for
|
||||
normal chatting.
|
||||
JID of the MUC room where the MUJI coordination is done. It may also
|
||||
be used for normal chatting.
|
||||
|
||||
@raise exceptions.ExternalRequestError: The MUC room can't be created or joined.
|
||||
@raise exceptions.ExternalRequestError: The MUC room can't be created, joined,
|
||||
or made non-anonymous.
|
||||
"""
|
||||
log.debug(f"{client.profile} is starting a MUJI group call with {entities}")
|
||||
room_jid = self._muc.get_unique_name(client, prefix="_muji_")
|
||||
|
|
@ -297,18 +427,33 @@ class XEP_0272:
|
|||
log.info(f"[{client.profile}] MUJI room created at {room_jid}")
|
||||
if not room:
|
||||
raise exceptions.ExternalRequestError("Can't create or join group chat room.")
|
||||
await self.start_preparation(client, room)
|
||||
room.on_joined_callbacks.append(self.on_room_join)
|
||||
room.on_left_callbacks.append(self.on_room_left)
|
||||
for entity in entities:
|
||||
self._muc_invite.invite(
|
||||
client,
|
||||
entity,
|
||||
room_jid,
|
||||
reason="You have been invited to participate in a group call.",
|
||||
# Make the room non-anonymous as participants need to know the real JID of others
|
||||
try:
|
||||
await self._muc.set_room_configuration(
|
||||
client, room_jid, {"muc#roomconfig_whois": "anyone"}
|
||||
)
|
||||
except Exception:
|
||||
raise exceptions.ExternalRequestError(
|
||||
f"Can't make the MUJI room {room_jid} non-anonymous: the MUJI mesh "
|
||||
"can't be established."
|
||||
)
|
||||
await self.start_preparation(client, room)
|
||||
sub_type = (
|
||||
C.META_SUBTYPE_CALL_VIDEO if extra.get("video") else C.META_SUBTYPE_CALL_AUDIO
|
||||
)
|
||||
for entity in entities:
|
||||
self.invite(client, entity, room_jid, sub_type)
|
||||
return {"room_jid": room_jid.full()}
|
||||
|
||||
def invite(
|
||||
self, client: SatXMPPEntity, entity: jid.JID, room_jid: jid.JID, sub_type: str
|
||||
) -> None:
|
||||
"""Invite an entity to the MUJI conference (XEP-0482)"""
|
||||
muji_elt = domish.Element((NS_MUJI, "muji"))
|
||||
muji_elt["room"] = room_jid.full()
|
||||
muji_elt["type"] = sub_type
|
||||
self._call_invites.invite(client, entity, [muji_elt])
|
||||
|
||||
|
||||
@implementer(itx_xmpp.IDisco)
|
||||
class XEP_0272_handler(XMPPHandler):
|
||||
|
|
|
|||
Loading…
Reference in a new issue