Skip to content

Auto Voice Channels

Jason Tucker edited this page May 12, 2026 · 5 revisions

Auto Voice Channels

The hub system

One or more hub voice channels act as entry points. When a user joins a hub:

  1. The hub channel is renamed in place to the user's new channel name — the user stays in it, no move needed
  2. A replacement hub is created immediately in the same category
  3. An attached text channel is created below the voice channel — private, only visible to members in the voice channel
  4. A compact control panel is posted (silently) in the text channel with interactive buttons
  5. When the channel becomes empty, both voice + text channels are deleted after 30 seconds

Channel naming

Channel names track Discord rich presence (requires the Presence Intent enabled in Dev Portal) for every member in the voice channel, not just the owner. Any member's Playing X activity can drive the rename.

Scenario Channel name
1 of 1 member playing Overwatch Overwatch
3 of 4 members playing Overwatch (the most-played game wins, count prefixed when more than one matches) (3) Overwatch
Manually renamed via the panel, or Tryhard / Chill template Whatever was set — also stored as the channel's fallback_name
Nobody is playing anything Reverts to the channel's fallback_name (the manual name, the initial random tech name on creation, or the last template choice)
Pre-existing channel with no fallback_name (legacy row) Stays at its current name until the next manual rename or template pick

The auto-rename is implemented in services/voice/autoNaming.ts and is shared between presenceUpdate (the live driver), the Auto / Counter template buttons, and the boot-time reconciler retry. presenceUpdate keys off the changed user's voice channel rather than the channel's owner — so a non-owner's game change can flip the channel name.

Control panel

The panel is posted automatically in the private text channel as soon as the channel is created, with SuppressNotifications set so nobody is pinged. It stays the first/top message in the channel and is re-rendered on every voice-state change so the member list and timestamps stay current.

Layout (Components V2):

🔊 host @owner · created <t:N:R>
👥 In channel
• @member-a joined <t:N:R> · 🎮 Overwatch
• @member-b joined <t:N:R>
[Rename] [Hosts] [Templates]
[Locked / Unlocked] [Visible / Hidden] [Claim] [Delete]

A silent 📋 Open Panel sticky message lives at the bottom of the text channel so the panel is always one click away — the sticky is a single non-CV2 button (no header / warning text). Use /voice from any channel to get an ephemeral copy too.

Status-flip toggles

Toggle buttons show the current state, not the pending action — same convention as the profile birthday-pings / year toggles and the /games view/pings buttons.

Button Current state ‑ green Current state ‑ red
Lock Unlocked (anyone can join) Locked (@everyone Connect denied)
Hide Visible Hidden

Click to flip. The button label updates immediately because the panel re-renders on every state change.

Buttons

Button Who can use What it does
✏️ Rename Owner, hosts, sudo Modal to set a custom name (also updates fallback_name)
👑 Hosts Owner, hosts, sudo One panel listing every member with their current rank emoji (👑 host · 🛡️ sudo · 👤 member). Click toggles host status.
📋 Templates Owner, hosts, sudo Auto / Counter ([x/y]) / Comp 5-stack / Tryhard / Chill — sets name + user limit in one click. Tryhard / Chill also overwrite fallback_name.
🔒/🔓 Locked / Unlocked Owner, hosts, sudo Toggles @everyone Connect
🙈/👁️ Hidden / Visible Owner, hosts, sudo Toggles channel visibility
👤 Claim Anyone in channel Claim ownership when owner has left
🗑️ Delete Owner, hosts, sudo Deletes both channels right away

Text channel permissions

Who Access
@everyone Denied (hidden)
Bot Full access (send, manage)
Channel owner View + send + read history
Hosts View + send + read history
Members in voice channel View + send + read history (added on join, removed on leave)
Sudo roles View + send + manage messages

Ownership transfer

When the owner leaves a non-empty room, ownership does not transfer immediately. Instead, the bot opens a grace window (default 5 min, configurable in /sudo → Settings → Voice → Owner grace (ms); set to 0 to disable and restore the old instant-transfer behavior).

During the grace window:

  • owner_user_id stays pointed at the original owner — they don't lose text-channel access and rejoining the VC restores them automatically.
  • acting_owner_user_id is set to a temporary driver. Picker priority: first existing host (from host_user_ids) still in the VC, then longest-tenured remaining member.
  • The acting owner gets text-channel access for the duration of the grace, even if they briefly step out of the voice channel.
  • The control panel shows 🔊 host @owner (away — returns by <t:N:R>) · 🎙️ acting host @acting.

Acting owner permissions during grace:

Action Acting owner
✏️ Rename
🔒 Lock / 🔓 Unlock
🙈 Hide / 👁️ Show
📋 Templates
👑 Hosts (manage) ❌ (original owner / sudo only)
👤 Claim ❌ during grace (denial cites the grace ETA)
🗑️ Delete ❌ (original owner / sudo only)

Outcomes:

  • Owner returns within grace → instantly restored. Acting owner is cleared. Panel re-renders.
  • Grace expires without return → acting owner is promoted to permanent owner. Original owner is removed from host_user_ids if they were ever there.
  • Acting owner ALSO leaves during grace → the chain is broken: grace is cancelled and ownership permanently transfers to whoever's still in the room (per spec — original owner forfeits).

State persists in auto_channels.acting_owner_user_id + auto_channels.owner_grace_expires_at. The reconciler reschedules in-flight grace timers on bot startup; any overdue grace promotes immediately.

Sudo override — Force owner transfer

When the rules above aren't fast enough (owner has ghosted permanently, or is being abusive and you can't wait out the grace), sudo can override via /sudo → Force owner transfer:

  1. Pick an auto-channel from the StringSelect.
  2. Pick a new owner from the UserSelect (any guild member, not just current VC members).
  3. Transfer is immediate — DB updated, any active grace cancelled, text-channel permissions resynced, control panel re-rendered.

The new owner is automatically removed from host_user_ids if they were a host. The action is logged: Force owner transfer: vc=... A → B (by sudo X).

Hub defaults — pinned template / name / user limit

/sudo → Settings → Hub Channels → "Edit defaults for a hub…" opens a modal where each hub can pin three optional defaults that apply to every auto-channel spawned from it:

Field Effect
Template One of auto, counter, squad, detail, state, party, stealth. Drives the new channel's name_template.
Manual name Literal name override. Supports the {member} token (substituted to the joiner's display name). When set, auto_name_enabled is flipped off so presence-driven renames don't churn the pinned name.
User limit 0–99. Applied directly to the Discord voice channel on rename. 0 (or blank) = no limit.

Any field left blank = the bot's existing built-in default applies. Columns live on hub_channels (default_template_key, default_manual_name, default_user_limit).

Hub lockdown — temporary kill switch

/sudo → Settings → Hub Channels → 🚨 Lockdown opens a dedicated panel that denies Connect on @everyone for hub voice channels so Discord blocks joins entirely.

Two scopes:

  • Per-hub (sudo) — Pick a hub from the "Lock an individual hub…" select, then enter a duration (1–1440 minutes) in the modal. Persists to hub_channels.lockdown_until.
  • Server-wide (bot-owner only) — Preset buttons (Lock all 15 m / 1 h / 4 h) lock every hub in the guild at once. Persists to bot_settings key voice.guild_lockdown_until. Bot-owner status uses the dynamic isBotOwner check.

Both kinds of lockdown persist across restarts: restoreHubLockdowns(client) runs in the reconciler startup path, re-applies in-flight Connect denials, reschedules unlock timers, and cleans up rows whose lockdown already expired while the bot was down.

Per-hub unlock respects server-wide lockdown — clearing one hub's lockdown_until won't punch a hole in the guild-wide policy.

DB state tracked

auto_channels holds the per-channel config (guild ID, voice channel ID, text channel ID, owner, hosts, allowed users/roles, lock state, hidden state, user limit, auto_name_enabled, name_template, fallback_name, control panel message ID, cleanup timestamp, source hub ID).

auto_channel_members(voice_channel_id, user_id, joined_at) backs the panel's "In channel" list. Written from voiceStateUpdate on join (upsert) and leave (delete). On boot, the reconciler:

  1. Backfills currently-occupying members at now() (so old times pre-restart are lost but new joins are tracked accurately).
  2. Re-runs the auto-rename for any tracked auto channel where the owner is currently in the channel and playing a game (and auto_name_enabled is on) — closes the gap where presence updates between bot restarts were lost.

Configuration

Set in .env (or override at runtime via /sudo → Settings → Voice / Hub Channels):

  • AUTO_VOICE_CATEGORY_ID — the Discord category that contains both hubs and auto channels
  • HUB_CHANNEL_IDS — legacy comma-separated voice channel IDs that act as hubs (one-time seed; runtime list lives in the hub_channels table)
  • VOICE_CLEANUP_DELAY_MS — how long to wait before deleting empty channels (default: 30000)

Clone this wiki locally