mirror of
https://github.com/ZHANGTIANYAO1/teamspeak-music-bot.git
synced 2026-10-02 21:12:49 +08:00
fix: harden voice ducking bot detection
This commit is contained in:
1 parent
97e8a87305
commit
bc711758b7
7 files changed
+333
-35
No files matched your search
@@ -3,6 +3,7 @@ import { Readable } from "node:stream";
|
||||
import {
|
||||
Client as TS3FullClient,
|
||||
generateIdentity as genTS3Identity,
|
||||
getUidFromPublicKey,
|
||||
identityFromString,
|
||||
sendTextMessage,
|
||||
listChannels,
|
||||
@@ -75,8 +76,15 @@ export interface TS3TextMessage {
|
||||
export interface TS3VoiceActivity {
|
||||
clientId: number;
|
||||
codec: number;
|
||||
/** Stable TeamSpeak identity when the sender is present in the client view. */
|
||||
clientUid?: string;
|
||||
}
|
||||
|
||||
// Command notifications and UDP voice packets can be reordered in flight.
|
||||
// Retain a leaving client's UID briefly so its final packet is still
|
||||
// attributable; a new clientEnter for the same id cancels and overwrites it.
|
||||
const VISIBLE_CLIENT_UID_RELEASE_GRACE_MS = 1_000;
|
||||
|
||||
/**
|
||||
* Map the library's TextMessage to our wrapper. Preserves invokerGroups (the
|
||||
* sender's TS server groups), which the library populates only when the sender
|
||||
@@ -97,7 +105,13 @@ export function toTS3TextMessage(msg: TextMessage): TS3TextMessage {
|
||||
export class TS3Client extends EventEmitter {
|
||||
private client: TS3FullClient | null = null;
|
||||
private identity: Identity;
|
||||
private readonly clientUid: string;
|
||||
private clientId = 0;
|
||||
private readonly visibleClientUids = new Map<number, string>();
|
||||
private readonly visibleClientUidReleaseTimers = new Map<
|
||||
number,
|
||||
ReturnType<typeof setTimeout>
|
||||
>();
|
||||
private logger: Logger;
|
||||
private disconnecting = false;
|
||||
private detectedProtocol: ServerProtocol = "unknown";
|
||||
@@ -114,6 +128,7 @@ export class TS3Client extends EventEmitter {
|
||||
} else {
|
||||
this.identity = genTS3Identity(8);
|
||||
}
|
||||
this.clientUid = getUidFromPublicKey(this.identity.publicKeyBase64());
|
||||
}
|
||||
|
||||
/** The detected (or forced) server protocol after connect(). */
|
||||
@@ -128,6 +143,7 @@ export class TS3Client extends EventEmitter {
|
||||
|
||||
async connect(): Promise<void> {
|
||||
this.voiceEndpointResolver.reset();
|
||||
this.clearVisibleClientUids();
|
||||
// Clean up any existing connection before creating a new one
|
||||
if (this.client) {
|
||||
this.logger.info("Cleaning up previous connection before reconnecting");
|
||||
@@ -245,9 +261,11 @@ export class TS3Client extends EventEmitter {
|
||||
// The library normally suppresses our own packets; retain the explicit
|
||||
// guard so a future protocol change cannot make a bot duck itself.
|
||||
if (voice.clientId === this.clientId) return;
|
||||
const clientUid = this.visibleClientUids.get(voice.clientId);
|
||||
const activity: TS3VoiceActivity = {
|
||||
clientId: voice.clientId,
|
||||
codec: voice.codec,
|
||||
...(clientUid ? { clientUid } : {}),
|
||||
};
|
||||
this.emit("voiceActivity", activity);
|
||||
});
|
||||
@@ -255,10 +273,12 @@ export class TS3Client extends EventEmitter {
|
||||
this.client.on("disconnected", (err) => {
|
||||
this.logger.warn({ err: err?.message }, "Connection closed");
|
||||
this.clientId = 0;
|
||||
this.clearVisibleClientUids();
|
||||
this.emit("disconnected");
|
||||
});
|
||||
|
||||
this.client.on("clientEnter", (info: ClientInfo) => {
|
||||
this.rememberVisibleClientUid(info.id, info.uid);
|
||||
this.logger.debug(
|
||||
{ nickname: info.nickname, id: info.id },
|
||||
"Client entered"
|
||||
@@ -267,6 +287,7 @@ export class TS3Client extends EventEmitter {
|
||||
});
|
||||
|
||||
this.client.on("clientLeave", (ev: ClientLeftViewEvent) => {
|
||||
this.releaseVisibleClientUid(ev.id);
|
||||
this.logger.debug({ id: ev.id }, "Client left");
|
||||
this.emit("clientLeave", ev);
|
||||
});
|
||||
@@ -461,6 +482,47 @@ export class TS3Client extends EventEmitter {
|
||||
return this.voiceEndpointResolver.getEndpoint();
|
||||
}
|
||||
|
||||
/** Stable identity of this managed TeamSpeak client. */
|
||||
getClientUid(): string {
|
||||
return this.clientUid;
|
||||
}
|
||||
|
||||
private rememberVisibleClientUid(clientId: number, clientUid: string): void {
|
||||
const pendingRelease = this.visibleClientUidReleaseTimers.get(clientId);
|
||||
if (pendingRelease) clearTimeout(pendingRelease);
|
||||
this.visibleClientUidReleaseTimers.delete(clientId);
|
||||
|
||||
if (clientId > 0 && clientUid) {
|
||||
this.visibleClientUids.set(clientId, clientUid);
|
||||
} else {
|
||||
this.visibleClientUids.delete(clientId);
|
||||
}
|
||||
}
|
||||
|
||||
private releaseVisibleClientUid(clientId: number): void {
|
||||
const clientUid = this.visibleClientUids.get(clientId);
|
||||
if (!clientUid) return;
|
||||
|
||||
const previous = this.visibleClientUidReleaseTimers.get(clientId);
|
||||
if (previous) clearTimeout(previous);
|
||||
const timer = setTimeout(() => {
|
||||
if (this.visibleClientUids.get(clientId) === clientUid) {
|
||||
this.visibleClientUids.delete(clientId);
|
||||
}
|
||||
this.visibleClientUidReleaseTimers.delete(clientId);
|
||||
}, VISIBLE_CLIENT_UID_RELEASE_GRACE_MS);
|
||||
timer.unref?.();
|
||||
this.visibleClientUidReleaseTimers.set(clientId, timer);
|
||||
}
|
||||
|
||||
private clearVisibleClientUids(): void {
|
||||
for (const timer of this.visibleClientUidReleaseTimers.values()) {
|
||||
clearTimeout(timer);
|
||||
}
|
||||
this.visibleClientUidReleaseTimers.clear();
|
||||
this.visibleClientUids.clear();
|
||||
}
|
||||
|
||||
disconnect(): void {
|
||||
if (this.client && !this.disconnecting) {
|
||||
this.disconnecting = true;
|
||||
@@ -473,6 +535,7 @@ export class TS3Client extends EventEmitter {
|
||||
});
|
||||
}
|
||||
this.clientId = 0;
|
||||
this.clearVisibleClientUids();
|
||||
this.httpQuery = null;
|
||||
this.detectedProtocol = "unknown";
|
||||
if (this.udpErrorTimer) {
|
||||
|
||||
Reference in new issue
Block a user