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.
This commit is contained in:
javis-bot
2026-06-10 11:37:39 +09:00
parent 40fd7dbb59
commit b6cf05f6cf

View File

@@ -33,6 +33,27 @@ export class SelfbotStreamer implements ScreenStreamer {
return this.active; 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<void> {
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() { private async loadLib() {
let selfbot: any, vs: any; let selfbot: any, vs: any;
try { try {
@@ -66,12 +87,19 @@ export class SelfbotStreamer implements ScreenStreamer {
const { selfbot, vs } = await this.loadLib(); const { selfbot, vs } = await this.loadLib();
const { Streamer, prepareStream, playStream } = vs; const { Streamer, prepareStream, playStream } = vs;
this.controller = new AbortController();
this.streamer = new Streamer(new selfbot.Client()); this.streamer = new Streamer(new selfbot.Client());
await this.streamer.client.login(this.config.selfbotToken); 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.streamer.joinVoice(ctx.guildId, ctx.voiceChannelId);
await this.humanPause(2500, 5000);
const [w, h] = this.config.vncResolution.split("x").map((n) => parseInt(n, 10)); 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 // 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 // x11grab), then pipe that stream into the library. Relying on the lib's