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: ConnectionError

Talking 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: object

An 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.

async __aenter__()[source]#
Return type:

Self

async __aexit__(*exc_info)[source]#
Return type:

None

__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

_handle_presence(stanza)[source]#
Return type:

None

_occupant_jid: str | None#
_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:

None

_reader_task: Task | None#
_ws: ClientConnection | None#
changed#

Set whenever the roster changes or the connection ends. Callers clear it themselves after they've looked at the new state.

async close()[source]#

Leave the room (if joined) and close the connection.

Return type:

None

ended#

Set once the background reader stops (room closed or connection lost).

error: Exception | None#

Why the reader stopped, if the connection was lost.

async get_participant_count()[source]#

Return the number of participants currently in the room.

Return type:

int

async get_participants()[source]#

Return the participants currently known to be in the room.

Return type:

list[Participant]

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:

None

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:

list[Participant]

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.

property ws: ClientConnection#

The underlying WebSocket connection.

None before open() is called and after close() - accessing it in either state is a bug, so it raises instead of silently returning None.

exception inspect_jitsi.xmpp.connection.RoomDoesNotExist[source]#

Bases: Exception

A 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.js to 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.

Return type:

dict[str, str | None]