Decouple ghost key-recipient enumeration from the shared bot Client (follow-up to #44) #50

Closed
opened 2026-06-28 17:08:12 +00:00 by robocub · 1 comment
Collaborator

Follow-up to #44 (deferred, low priority).

#44 replaced the per-ghost full matrix_sdk::Client with a send-only OlmMachine, but left one thread on the bridge bot's shared Client: enumerate_key_recipients (crates/matrix-rtc/src/rtc/ghost.rs) still reads the room's m.call.member state through client.get_room(...).get_state_events_static to build the key-send recipient list. CallRecipients/distribute_key_task thread that Client in just for this read.

This was deliberately deferred in #44 because that client is the shared, bridge-wide bot client (one /sync for the whole bridge, driving the subscriber) — not a per-ghost FD/sync cost. So it does not affect #44's acceptance criteria (zero per-ghost crypto FDs, no per-ghost /sync). It's a decoupling/cleanliness item, not a scaling fix.

Scope

Convert enumerate_key_recipients to read room state via the same raw CS-API masquerade the rest of the ghost path already uses:

  • GET /_matrix/client/v3/rooms/{room_id}/state with bearer_auth(as_token) + ?user_id=<ghost_mxid> (the ghost is already joined), reusing cs_url.
  • Filter org.matrix.msc3401.call.member, deserialize each content into CallMemberEventContent, reuse the existing active_memberships(...) -> (sender, device_id) logic and the is_bridge_excluded_identity / bot_user_id exclusions unchanged.
  • Drop the client field from CallRecipients and the Client arg from distribute_key_task.

Why bother

Fully decouples the ghost media-key path from the matrix-rust-sdk Client, so the ghost subsystem depends only on the appservice raw-HTTP surface. Minor, but tidy once #44 lands; not worth doing before #44 is live-verified.

Optional, separate

#44's body also notes a per-room shared device-tracker (collapse /keys/query from O(ghosts) to O(1) per room). That is a different optimization and is already tracked in #44's description; not part of this issue.

**Follow-up to #44 (deferred, low priority).** #44 replaced the per-ghost full `matrix_sdk::Client` with a send-only `OlmMachine`, but left one thread on the bridge bot's shared `Client`: `enumerate_key_recipients` (`crates/matrix-rtc/src/rtc/ghost.rs`) still reads the room's `m.call.member` state through `client.get_room(...).get_state_events_static` to build the key-send recipient list. `CallRecipients`/`distribute_key_task` thread that `Client` in just for this read. This was **deliberately deferred** in #44 because that `client` is the *shared*, bridge-wide bot client (one `/sync` for the whole bridge, driving the subscriber) — **not** a per-ghost FD/sync cost. So it does not affect #44's acceptance criteria (zero per-ghost crypto FDs, no per-ghost `/sync`). It's a decoupling/cleanliness item, not a scaling fix. ### Scope Convert `enumerate_key_recipients` to read room state via the same raw CS-API masquerade the rest of the ghost path already uses: - `GET /_matrix/client/v3/rooms/{room_id}/state` with `bearer_auth(as_token)` + `?user_id=<ghost_mxid>` (the ghost is already joined), reusing `cs_url`. - Filter `org.matrix.msc3401.call.member`, deserialize each `content` into `CallMemberEventContent`, reuse the existing `active_memberships(...)` -> `(sender, device_id)` logic and the `is_bridge_excluded_identity` / `bot_user_id` exclusions unchanged. - Drop the `client` field from `CallRecipients` and the `Client` arg from `distribute_key_task`. ### Why bother Fully decouples the ghost media-key path from the matrix-rust-sdk `Client`, so the ghost subsystem depends only on the appservice raw-HTTP surface. Minor, but tidy once #44 lands; not worth doing before #44 is live-verified. ### Optional, separate #44's body also notes a *per-room shared device-tracker* (collapse `/keys/query` from O(ghosts) to O(1) per room). That is a different optimization and is already tracked in #44's description; not part of this issue.
dark added this to the Roadmap project 2026-06-28 17:11:03 +00:00
Author
Collaborator

Shipped on master e66eb1a (fast-forward of refactor/issue-50-recipient-decoupling).

What changed (crates/matrix-rtc/src/rtc/ghost.rs only):

  • enumerate_key_recipients now reads the room's org.matrix.msc3401.call.member state via a raw masqueraded GET /_matrix/client/v3/rooms/{roomId}/state (cs_url + bearer_auth(as_token) + ?user_id=<ghost_mxid> — the ghost is a joined member), exactly as scoped.
  • Filtering unchanged and factored into a pure recipients_from_room_state (type filter → ghost/legacy-ghost/bot exclusions → active_memberships per (user, device) → dedup), with 3 new unit tests (fixtures built via the ruma constructors, so wire-shape parity is guaranteed): active-humans-only, expired/empty/redacted skipped, dedupe + legacy-format + malformed-event tolerance.
  • CallRecipients drops its Client field (carries the ghost MXID instead); distribute_key_task loses its Client param. run_ghost keeps its Client — still needed by invite_ghost, which deliberately cannot masquerade (out of scope here, as the issue says).
  • A transient enumeration HTTP error surfaces as Err, which distribute_key_loop already idles through.

Verified: full workspace cargo test + clippy -D warnings green (matrix-rtc 70 tests).

Live-tested 2026-07-07 on staging, encrypted Dark Voice room (Olm transport, cold-start stores): ghost media key sent recipients=["@dark:nether.im:ihjNlBVX90"] transport="olm" delivered=1 on every send — the raw-state enumeration produced exactly the human (user, device), bot + ghosts excluded; reached NO recipient devices count 0; membership-change re-key latency 316–493 ms (the #51 fast path intact); user confirmed Discord→Matrix audio decrypts in Element Call. Prod restored to alpha.13 baseline afterwards (all device ids reused).

Unreleased — rides in the next tag. The optional per-room shared device-tracker stays tracked in #44's description.

Shipped on master `e66eb1a` (fast-forward of `refactor/issue-50-recipient-decoupling`). **What changed** (`crates/matrix-rtc/src/rtc/ghost.rs` only): - `enumerate_key_recipients` now reads the room's `org.matrix.msc3401.call.member` state via a raw masqueraded `GET /_matrix/client/v3/rooms/{roomId}/state` (`cs_url` + `bearer_auth(as_token)` + `?user_id=<ghost_mxid>` — the ghost is a joined member), exactly as scoped. - Filtering unchanged and factored into a pure `recipients_from_room_state` (type filter → ghost/legacy-ghost/bot exclusions → `active_memberships` per `(user, device)` → dedup), with 3 new unit tests (fixtures built via the ruma constructors, so wire-shape parity is guaranteed): active-humans-only, expired/empty/redacted skipped, dedupe + legacy-format + malformed-event tolerance. - `CallRecipients` drops its `Client` field (carries the ghost MXID instead); `distribute_key_task` loses its `Client` param. `run_ghost` keeps its `Client` — still needed by `invite_ghost`, which deliberately cannot masquerade (out of scope here, as the issue says). - A transient enumeration HTTP error surfaces as `Err`, which `distribute_key_loop` already idles through. **Verified:** full workspace cargo test + clippy `-D warnings` green (matrix-rtc 70 tests). **Live-tested 2026-07-07 on staging, encrypted Dark Voice room (Olm transport, cold-start stores):** `ghost media key sent recipients=["@dark:nether.im:ihjNlBVX90"] transport="olm" delivered=1` on every send — the raw-state enumeration produced exactly the human `(user, device)`, bot + ghosts excluded; `reached NO recipient devices` count 0; membership-change re-key latency 316–493 ms (the #51 fast path intact); user confirmed Discord→Matrix audio decrypts in Element Call. Prod restored to alpha.13 baseline afterwards (all device ids reused). Unreleased — rides in the next tag. The optional per-room shared device-tracker stays tracked in #44's description.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
dark/nether-voicebridge#50
No description provided.