From b6cf05f6cfea36b488ea81db5d8e68eb32237c14 Mon Sep 17 00:00:00 2001 From: javis-bot Date: Wed, 10 Jun 2026 11:37:39 +0900 Subject: [PATCH] feat: humanise selfbot voice-join and go-live pacing Joining voice and starting the broadcast instantly looks like a bot. Add randomised, human-plausible pauses (~0.9-2.2s after coming online before joining the channel, ~2.5-5s after joining before hitting Go Live) so the cadence isn't machine-instant or fingerprintable. The pause resolves immediately on stop() so teardown never hangs mid-wait. Verified live: end-to-end join -> settle -> Go Live took ~8s before the stream went live, held for 15s, and tore down cleanly. tsc --noEmit passes. --- bot/src/stream/selfbot.ts | 30 +++++++++++++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/bot/src/stream/selfbot.ts b/bot/src/stream/selfbot.ts index bfa4d45..f14580e 100644 --- a/bot/src/stream/selfbot.ts +++ b/bot/src/stream/selfbot.ts @@ -33,6 +33,27 @@ export class SelfbotStreamer implements ScreenStreamer { return this.active; } + /** + * Wait a randomised, human-plausible amount of time. Resolves immediately if + * the stream is aborted (stop()) mid-wait, so teardown never hangs on a pause. + */ + private humanPause(minMs: number, maxMs: number): Promise { + const ms = Math.floor(minMs + Math.random() * Math.max(0, maxMs - minMs)); + return new Promise((resolve) => { + const signal = this.controller?.signal; + if (signal?.aborted) return resolve(); + const onAbort = () => { + clearTimeout(timer); + resolve(); + }; + const timer = setTimeout(() => { + signal?.removeEventListener("abort", onAbort); + resolve(); + }, ms); + signal?.addEventListener("abort", onAbort, { once: true }); + }); + } + private async loadLib() { let selfbot: any, vs: any; try { @@ -66,12 +87,19 @@ export class SelfbotStreamer implements ScreenStreamer { const { selfbot, vs } = await this.loadLib(); const { Streamer, prepareStream, playStream } = vs; + this.controller = new AbortController(); + this.streamer = new Streamer(new selfbot.Client()); await this.streamer.client.login(this.config.selfbotToken); + + // Act like a person, not a bot: take a breath after coming online before + // navigating into the voice channel, then settle in for a few seconds + // before hitting "Go Live". Randomised so the cadence isn't fingerprintable. + await this.humanPause(900, 2200); await this.streamer.joinVoice(ctx.guildId, ctx.voiceChannelId); + await this.humanPause(2500, 5000); const [w, h] = this.config.vncResolution.split("x").map((n) => parseInt(n, 10)); - this.controller = new AbortController(); // Capture the VNC X display with the SYSTEM ffmpeg (which reliably has // x11grab), then pipe that stream into the library. Relying on the lib's