4.3 KiB
Spec: Selfbot-Viable Video Capture — manual screen-share watch command (Phase D)
Status: PLANNED (not yet built)
Date: 2026-09-02
Author: Hermes
Related: docs/specs/2026-08-31_video-receive-phaseC-spec.md (auto-receive, superseded
for selfbot), gmw-ops/references/selfbot-presence-detection-limits.md,
gmw-ops/references/discord-voice-fork-video-receive.md
TL;DR — the decisive finding (verified live 2026-09-02)
User insists on keeping the selfbot (no bot-token migration). Live diagnostics prove a selfbot CANNOT auto-detect other members' camera/share because:
- It never receives
VOICE_STATE_UPDATEfor other members (only its own). guild.members.fetch()→ 403,GET /channels/{id}/voice-states→ 404.- No
GUILD_CREATE, noREADY.broadcaster_user_idspresence. scanExistingStreamers+handleVoiceStateUpdate(the only twostartStreamWatchtriggers) are therefore both dead on a selfbot.- No manual watch command exists today, so even on-demand capture is impossible.
→ The ONE selfbot-viable path is a manual, operator-initiated STREAM_WATCH on a
member known to be screen-sharing. Gateway op 20 (STREAM_WATCH) is NOT gated on
bot-vs-user; the DAVE handshake to Ready+MLS was already verified live in earlier
sessions. The receive/mux/segment/upload pipeline (streamWatchReceiver.ts) is already
built and only lacks a real streamer to produce its first .mp4.
Camera-of-others is NOT viable on a selfbot even with unknown-ssrc fallback:
@discordjs/voice parsePacket calls daveSession.decrypt(packet, userId) keyed per
REAL userId (vendor fork dist/index.js:2143), so a fake id selects no MLS decryptor →
garbage, not H264. (The uncommitted unknown-ssrc change was reverted this session.)
Selfbot CAN capture the OWNER's own video (its own VOICE_STATE_UPDATE + fork op12
videoSSRC are attributable), but videoRecorder.ts hard-skips its own id — parameterized
self-capture is a follow-up, not the default.
Goal
Add a manual watch command so an operator can say "record 's screen share"
and the gateway startStreamWatchs that member → DAVE watch → per-burst .mp4 segments
(mirroring voice silence split) → upload → DB voice_recordings → dashboard <video>.
This is the only form of OTHER-member video capture a selfbot can deliver, and it is genuinely buildable with the existing receive pipeline.
Scope / files
Gateway (services/discord-gateway):
- New command type
VIDEO_WATCH+ handler incommand-handler/(dedicatedvideo.handler.ts), routed viacreateHandlerRegistry. - Handler resolves a VoiceChannel (from persisted
voice_auto_reconnect/ active connections) + target memberId from the command payload, callsstartStreamWatch(channel, memberId)(already exported). - Idempotent (startStreamWatch early-returns if a watch exists); a
VIDEO_UNWATCHcommand callsstopStreamWatch(guildId, userId). - Reply: success/failure via the standard
CommandReplypublish.
Backend (services/backend):
- oRPC procedure (or the existing command bridge) that publishes a
VIDEO_WATCHcommand tobackend:commandwith{ guildId, channelId, userId }. Reuse the same bridge the FE already uses for voice commands.
Frontend (services/frontend):
- A "Video Watch" control: pick a voice member + a "Record screen" button → calls the backend procedure. Shows live status (watching / recording / segments uploaded).
(Each layer optional independently; gateway alone gives a Redis-testable path.)
Verification
pnpm typecheck+pnpm build+biome check src/green in discord-gateway.- Unit test: handler publishes reply + calls startStreamWatch with the right args (mock the module).
- Live: operator invokes
!videorec <member>while that member screen-shares → journal showsSending STREAM_WATCH→STREAM_CREATE→DAVE watch READY→Video burst opened→Video muxed to mp4→ avideo-*.mp4appears under<recordingsDir>/<uid>/and avideo-%row lands invoice_recordings. Build & Deploy (Nix)CI green.
Out of scope (documented dead ends on selfbot)
- Auto camera/share capture of OTHER members (impossible at detection layer).
- Camera-of-others via
unknown-ssrc(DAVE decrypt needs real userId). - Bot-token migration (user declined).