fix(video): detect pre-existing voice users from GUILD_CREATE.voice_states
Real root cause of 'tidak mendeteksi user yg sudah ada di voice':
The selfbot's discord.js-selfbot-v13 GUILD_CREATE handler only sends
GUILD_SUBSCRIPTIONS_BULK — it DROPS d.voice_states from the payload.
So when the gateway (re)starts, every user who was ALREADY in the voice
channel (with camera or screen-share on) is invisible: channel.members is
empty, and REST fallbacks don't work for user tokens (verified live:
GET /channels/{id}/voice-states -> 404, GET /guilds/{id}/members -> 403).
Fix: register our own raw listener for GUILD_CREATE, capture
d.voice_states per guild (buffer it), and consume it in
scanExistingStreamers when the channel gets tracked (voice join happens
after GUILD_CREATE). This is the ONLY user-token-compatible source of
'who is in voice with video right now'.
- videoRecorder: +pendingGuildCreateVoiceStates buffer, +raw GUILD_CREATE
listener, Path C consumes buffered states (self_video/self_stream,
matching channel, skip self) before the REST fallback
- scanExistingStreamers: Path A cache -> Path C GUILD_CREATE -> Path D
REST members.fetch() best-effort
- +1 test: GUILD_CREATE voice_states buffer -> watch camera+screen-share,
skip self/no-video/other-channel (192/192 pass)
- typecheck + build + biome clean (1 pre-existing warning)
This commit is contained in:
1 parent
cee33c0d1f
commit
ac32179bb1
2 files changed
+168
-12
No files matched your search
@@ -45,8 +45,9 @@ describe("videoRecorder (stream-watch orchestration)", () => {
|
||||
const client = makeClient();
|
||||
setVideoRecorderClient(client);
|
||||
setVideoRecorderClient(client); // idempotent (videoRecorder + streamWatch)
|
||||
// one raw (streamWatch) + one voiceStateUpdate (videoRecorder)
|
||||
expect(client.on).toHaveBeenCalledTimes(2);
|
||||
// one voiceStateUpdate (videoRecorder) + raw GUILD_CREATE (videoRecorder)
|
||||
// + raw STREAM_* (streamWatch) = 3
|
||||
expect(client.on).toHaveBeenCalledTimes(3);
|
||||
expect(client.on).toHaveBeenCalledWith(
|
||||
"voiceStateUpdate",
|
||||
expect.any(Function),
|
||||
@@ -190,4 +191,74 @@ describe("videoRecorder (stream-watch orchestration)", () => {
|
||||
expect(spy).toHaveBeenCalledWith(ch as any, "u-cam");
|
||||
});
|
||||
});
|
||||
|
||||
it("detects pre-existing camera user from buffered GUILD_CREATE.voice_states (selfbot fix)", async () => {
|
||||
const client = makeClient() as any;
|
||||
setVideoRecorderClient(client);
|
||||
const spy = vi
|
||||
.spyOn(streamWatch, "startStreamWatch")
|
||||
.mockImplementation(() => {});
|
||||
const ch = {
|
||||
id: "c1",
|
||||
guild: {
|
||||
id: "g1",
|
||||
members: { fetch: vi.fn().mockResolvedValue(new Map()) },
|
||||
},
|
||||
members: new Map(), // empty — library drops voice_states
|
||||
};
|
||||
// Simulate GUILD_CREATE arriving at WS connect (after setVideoRecorderClient
|
||||
// registered the raw listener, before trackChannel runs).
|
||||
const rawListener = client.on.mock.calls.find(
|
||||
(c: unknown[]) => c[0] === "raw",
|
||||
)?.[1];
|
||||
expect(rawListener).toBeDefined();
|
||||
rawListener({
|
||||
t: "GUILD_CREATE",
|
||||
d: {
|
||||
id: "g1",
|
||||
voice_states: [
|
||||
{
|
||||
user_id: "u-cam",
|
||||
channel_id: "c1",
|
||||
self_video: true,
|
||||
self_stream: false,
|
||||
},
|
||||
{
|
||||
user_id: "u-share",
|
||||
channel_id: "c1",
|
||||
self_video: false,
|
||||
self_stream: true,
|
||||
},
|
||||
{
|
||||
user_id: "u-novideo",
|
||||
channel_id: "c1",
|
||||
self_video: false,
|
||||
self_stream: false,
|
||||
},
|
||||
{
|
||||
user_id: "bot1",
|
||||
channel_id: "c1",
|
||||
self_video: true,
|
||||
self_stream: false,
|
||||
}, // self — skip
|
||||
{
|
||||
user_id: "u-otherchan",
|
||||
channel_id: "c9",
|
||||
self_video: true,
|
||||
self_stream: false,
|
||||
}, // different channel — skip
|
||||
],
|
||||
},
|
||||
});
|
||||
// Now track the channel — scanExistingStreamers consumes buffered states.
|
||||
trackChannel("g1", ch as any);
|
||||
await vi.waitFor(() => {
|
||||
// camera user + screen-share user watched, self + no-video + other-channel skipped
|
||||
expect(spy).toHaveBeenCalledWith(ch as any, "u-cam");
|
||||
expect(spy).toHaveBeenCalledWith(ch as any, "u-share");
|
||||
});
|
||||
expect(spy).not.toHaveBeenCalledWith(ch as any, "u-novideo");
|
||||
expect(spy).not.toHaveBeenCalledWith(ch as any, "bot1");
|
||||
expect(spy).not.toHaveBeenCalledWith(ch as any, "u-otherchan");
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user