inspect_jitsi.xmpp.connection module#
A minimal anonymous XMPP/MUC connection to a Jitsi Meet room.
Jitsi's prosody deployment locks down disco#info on MUC rooms to occupants
only (confirmed live: a bare disco#info query gets <error type="auth">
<forbidden/></error>), so the usual XEP-0045 "peek without joining" trick
does not work here. Instead, JitsiXmppConnection joins the room as
a real (if anonymous) occupant over an XMPP connection carried by WebSocket
(the same "wss://<domain>/xmpp-websocket" endpoint the web client uses), and
reads the roster of <presence> stanzas the MUC sends.
jicofo (the conference focus component) also joins every active Jitsi conference's MUC as a pseudo-participant nicknamed "focus" - that occupant is filtered out of the roster so it reflects only humans.
No Python XMPP library (slixmpp, aioxmpp, …) supports the WebSocket transport Jitsi requires - they're TCP-only, and Jitsi deployments generally don't expose raw XMPP client-to-server (5222) publicly - so this hand-rolls the small slice of RFC 6120 (stream/SASL/bind) and RFC 7395 (XMPP over WebSocket framing) needed to join a MUC room.
A conference URL is arbitrary, caller-supplied input, so the server it resolves to is untrusted - stanzas are parsed with defusedxml rather than the standard library's xml.etree.ElementTree directly, which guards against billion-laughs/XXE-style XML bombs a malicious server could send.
- exception inspect_jitsi.xmpp.connection.JitsiConnectionError[source]#
Bases:
ConnectionErrorTalking to a Jitsi deployment's XMPP endpoint failed or was lost.
Covers everything below the room-existence/participant-roster level - the WebSocket connection being refused, dropped, or timing out, and a server response this hand-rolled client can't parse - as well as the protocol-level rejections (stream errors, failed SASL, a failed MUC join) this module raises as it negotiates a session. Catch this (or the plain ConnectionError it subclasses) instead of guessing at every stdlib/websockets exception a live XMPP session might raise.
- __annotations__ = {}#
- __firstlineno__ = 126#
- __module__ = 'inspect_jitsi.xmpp.connection'#
- __static_attributes__ = ()#
- __weakref__#
list of weak references to the object
- class inspect_jitsi.xmpp.connection.JitsiXmppConnection(conference_url, nick=None, *, name=None, anonymous_domain=None, muc_domain=None, timeout=10)[source]#
Bases:
objectAn anonymous XMPP/MUC connection, joined to one Jitsi Meet room.
Connects over the WebSocket endpoint the Jitsi web client itself uses, joins the room's MUC under the given nickname (disclosing name as its XEP-0172 display name, if given - no display name is disclosed by default), and keeps a live roster of the other participants (updated as they join/leave for as long as the connection stays open).
Use as an async context manager:
async with JitsiXmppConnection(conference_url, "probe") as conn: print(await conn.get_participant_count())
or call
open()/close()directly.- __annotations__ = {'_occupant_jid': 'str | None', '_participants': 'dict[str, Participant]', '_reader_task': 'asyncio.Task | None', '_ws': 'websockets.ClientConnection | None', 'error': 'Exception | None', 'room_created': 'bool | None'}#
- __dict__ = mappingproxy({'__module__': 'inspect_jitsi.xmpp.connection', '__firstlineno__': 332, '__doc__': 'An anonymous XMPP/MUC connection, joined to one Jitsi Meet room.\n\nConnects over the WebSocket endpoint the Jitsi web client itself uses,\njoins the room\'s MUC under the given nickname (disclosing `name` as its\nXEP-0172 display name, if given - no display name is disclosed by\ndefault), and keeps a live roster of the other participants (updated as\nthey join/leave for as long as the connection stays open).\n\nUse as an async context manager::\n\n async with JitsiXmppConnection(conference_url, "probe") as conn:\n print(await conn.get_participant_count())\n\nor call :meth:`open`/:meth:`close` directly.\n', '__init__': <function JitsiXmppConnection.__init__>, 'ws': <property object>, 'open': <function JitsiXmppConnection.open>, '_read_loop': <function JitsiXmppConnection._read_loop>, '_handle_presence': <function JitsiXmppConnection._handle_presence>, 'close': <function JitsiXmppConnection.close>, 'other_participants': <function JitsiXmppConnection.other_participants>, 'get_participants': <function JitsiXmppConnection.get_participants>, 'get_participant_count': <function JitsiXmppConnection.get_participant_count>, '__aenter__': <function JitsiXmppConnection.__aenter__>, '__aexit__': <function JitsiXmppConnection.__aexit__>, '__static_attributes__': ('_anonymous_domain', '_focus_seen', '_joined', '_muc_domain', '_occupant_jid', '_participants', '_reader_task', '_ws', 'changed', 'conference_url', 'ended', 'error', 'name', 'nick', 'room', 'room_closed', 'room_created', 'timeout', 'ws_domain'), '__dict__': <attribute '__dict__' of 'JitsiXmppConnection' objects>, '__weakref__': <attribute '__weakref__' of 'JitsiXmppConnection' objects>, '__annotations__': {'_ws': 'websockets.ClientConnection | None', '_occupant_jid': 'str | None', '_participants': 'dict[str, Participant]', '_reader_task': 'asyncio.Task | None', 'error': 'Exception | None', 'room_created': 'bool | None'}})#
- __firstlineno__ = 332#
- __init__(conference_url, nick=None, *, name=None, anonymous_domain=None, muc_domain=None, timeout=10)[source]#
- __module__ = 'inspect_jitsi.xmpp.connection'#
- __static_attributes__ = ('_anonymous_domain', '_focus_seen', '_joined', '_muc_domain', '_occupant_jid', '_participants', '_reader_task', '_ws', 'changed', 'conference_url', 'ended', 'error', 'name', 'nick', 'room', 'room_closed', 'room_created', 'timeout', 'ws_domain')#
- __weakref__#
list of weak references to the object
- _participants: dict[str, Participant]#
- async _read_loop()[source]#
Continuously update the roster from incoming presence stanzas.
Waits without a timeout - a quiet room is normal here; a dead connection is detected by the WebSocket's own keepalive pings.
- Return type:
- changed#
Set whenever the roster changes or the connection ends. Callers clear it themselves after they've looked at the new state.
- ended#
Set once the background reader stops (room closed or connection lost).
- async get_participant_count()[source]#
Return the number of participants currently in the room.
- Return type:
- async get_participants()[source]#
Return the participants currently known to be in the room.
- Return type:
- async open()[source]#
Connect, authenticate anonymously, and join the room's MUC.
If the room doesn't exist yet and this deployment restricts room creation to privileged users, this still succeeds - room_created is set to False and the room is treated as empty (no participants), rather than raising.
- Return type:
- other_participants()[source]#
The participants known to be in the room, in the order they joined.
Not counting this connection itself. Synchronous, unlike
get_participants().- Return type:
- room_closed#
the MUC removed us (room destroyed, we were kicked) or jicofo, the conference focus, left it.
- Type:
True once the room is known to be over
- room_created: bool | None#
Whether the room existed to join. None before open().
False means the room didn't exist yet (or was destroyed once everyone left) and this deployment restricts room creation to privileged users (jicofo) - not a genuine failure, just an empty room: open() still succeeds, with no participants.
- exception inspect_jitsi.xmpp.connection.RoomDoesNotExist[source]#
Bases:
ExceptionA Jitsi MUC room doesn't exist (yet) and this deployment won't create it.
Not raised by JitsiXmppConnection.open() itself, which instead treats this as an empty room (room_created is set to False, with 0 participants) rather than an error - this is available for callers that would rather treat "room doesn't exist" as a hard failure.
- __firstlineno__ = 116#
- __module__ = 'inspect_jitsi.xmpp.connection'#
- __static_attributes__ = ()#
- __weakref__#
list of weak references to the object
- async inspect_jitsi.xmpp.connection.discover_hosts(domain, timeout=10)[source]#
Read
https://<domain>/config.jsto find the real XMPP/MUC domains.A Jitsi deployment's internal domain names often differ from the public hostname (e.g. docker-jitsi-meet defaults to "meet.jitsi" internally). Returns a dict with keys "domain", "muc", "anonymousdomain" - any of which may be None if not found.