Per-user opt-out: /bridge optout excludes a Discord user from capture and ghosting #63

Closed
opened 2026-07-07 01:23:35 +00:00 by robocub · 1 comment
Collaborator

Part of the #43 ToS follow-ups (Dev Policy §2/§3/§7: leave a way to abstain from being bridged).

A Discord user who does not want their voice relayed should be able to run /bridge optout (and /bridge optin) on the approval bot. Effect, per (user, guild) or global-per-user:

  • their audio is never forwarded Discord→Matrix (exclude at the anchor demux / capture-router assign),
  • no presence ghost is minted for them (gate in ensure_ghost/note_presence),
  • persisted (a small optout list next to banlist.toml) so it survives restarts,
  • reflected in the audit log.

Notes: the privacy policy (docs/privacy-policy.md) currently says "leave the channel or ask a manager to unlink" and flags the command as planned — update it when this ships. The interaction plumbing from #47 (/bridge command, permission-checked handler, pool→mgmt channel) is reusable; opt-out needs no Matrix-side round-trip if the list lives pool-side.

Part of the #43 ToS follow-ups (Dev Policy §2/§3/§7: leave a way to abstain from being bridged). A Discord user who does not want their voice relayed should be able to run `/bridge optout` (and `/bridge optin`) on the approval bot. Effect, per (user, guild) or global-per-user: - their audio is never forwarded Discord→Matrix (exclude at the anchor demux / capture-router assign), - no presence ghost is minted for them (gate in `ensure_ghost`/`note_presence`), - persisted (a small optout list next to banlist.toml) so it survives restarts, - reflected in the audit log. Notes: the privacy policy (docs/privacy-policy.md) currently says "leave the channel or ask a manager to unlink" and flags the command as planned — update it when this ships. The interaction plumbing from #47 (`/bridge` command, permission-checked handler, pool→mgmt channel) is reusable; opt-out needs no Matrix-side round-trip if the list lives pool-side.
Author
Collaborator

Shipped: merged to master cfda71b, released in v0.3.0-alpha.12.

/bridge optout and /bridge optin, open to every Discord user (the /bridge command dropped its command-level Manage Server restriction — Discord has no per-subcommand permissions — and the approve subcommand keeps its server-side Manage Server check instead). Scope is global per user id across the instance, persisted at <data_dir>/optout.toml (banlist-style module, atomic writes).

Enforcement is defense-in-depth, all pool-side:

  • Capture demux (the single Discord→Matrix choke point in VoiceReceiver::demux_tick): an opted-out user's frames are dropped even if a ghost sink lingers mid-retirement.
  • Ghost minting (ensure_ghost): gated — covers live voice events and the anchor's member-seeding path.
  • Opt-out moment: existing ghosts are retired immediately in every bridged channel (apply_optout).
  • Opt-in takes effect on the next presence edge (leave/rejoin the channel — stated in the command reply).

Live-tested on staging 2026-07-07: optout → ephemeral confirmation, ghost tile vanished from the Element Call, file written; staging restart with the user still in voice → the ghost re-seeding skipped them (persistence + seeding gate proven); optin + rejoin → ghost returned. 249 tests + clippy green.

Deviation from the issue text: recorded via tracing INFO rather than the mgmt audit.jsonl (the pool has no audit file; the audit log is a mgmt-side construct). Fine for now — the persisted list itself is the authoritative record.

**Shipped: merged to master `cfda71b`, released in v0.3.0-alpha.12.** `/bridge optout` and `/bridge optin`, open to every Discord user (the `/bridge` command dropped its command-level Manage Server restriction — Discord has no per-subcommand permissions — and the approve subcommand keeps its server-side Manage Server check instead). Scope is **global per user id** across the instance, persisted at `<data_dir>/optout.toml` (banlist-style module, atomic writes). Enforcement is defense-in-depth, all pool-side: - **Capture demux** (the single Discord→Matrix choke point in `VoiceReceiver::demux_tick`): an opted-out user's frames are dropped even if a ghost sink lingers mid-retirement. - **Ghost minting** (`ensure_ghost`): gated — covers live voice events and the anchor's member-seeding path. - **Opt-out moment**: existing ghosts are retired immediately in every bridged channel (`apply_optout`). - Opt-in takes effect on the next presence edge (leave/rejoin the channel — stated in the command reply). **Live-tested on staging 2026-07-07**: optout → ephemeral confirmation, ghost tile vanished from the Element Call, file written; staging restart with the user still in voice → the ghost re-seeding *skipped* them (persistence + seeding gate proven); optin + rejoin → ghost returned. 249 tests + clippy green. Deviation from the issue text: recorded via `tracing` INFO rather than the mgmt `audit.jsonl` (the pool has no audit file; the audit log is a mgmt-side construct). Fine for now — the persisted list itself is the authoritative record.
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#63
No description provided.