mirror of
https://github.com/ZHANGTIANYAO1/teamspeak-music-bot.git
synced 2026-10-01 20:42:50 +08:00
Merge pull request #12 from ZHANGTIANYAO1/claude/start-music-api-servers-XrUNR
Fix TS6 compatibility by patching handshake clientinit packet
This commit is contained in:
5 files changed
+78
-93
No files matched your search
+8
-3
@@ -105,6 +105,9 @@ export class BotInstance extends EventEmitter {
|
||||
});
|
||||
|
||||
this.tsClient.on("disconnected", () => {
|
||||
// Avoid duplicate event if disconnect() was called explicitly
|
||||
// (it already set connected = false and emitted "disconnected").
|
||||
if (!this.connected) return;
|
||||
this.connected = false;
|
||||
this.player.stop();
|
||||
this.emit("disconnected");
|
||||
@@ -119,8 +122,10 @@ export class BotInstance extends EventEmitter {
|
||||
|
||||
disconnect(): void {
|
||||
this.player.stop();
|
||||
this.tsClient.disconnect();
|
||||
// Set connected = false BEFORE tsClient.disconnect() so that the
|
||||
// "disconnected" event handler (setupTsEvents) won't emit a duplicate.
|
||||
this.connected = false;
|
||||
this.tsClient.disconnect();
|
||||
this.emit("disconnected");
|
||||
}
|
||||
|
||||
@@ -488,7 +493,7 @@ export class BotInstance extends EventEmitter {
|
||||
}
|
||||
|
||||
private async playNext(): Promise<void> {
|
||||
if (this.isAdvancing) return;
|
||||
if (this.isAdvancing || !this.connected) return;
|
||||
this.isAdvancing = true;
|
||||
try {
|
||||
this.voteSkipUsers.clear();
|
||||
@@ -497,7 +502,7 @@ export class BotInstance extends EventEmitter {
|
||||
let started = await this.resolveAndPlay(next);
|
||||
if (!started) {
|
||||
// Skip to next if URL resolve fails (up to 3 retries)
|
||||
for (let i = 0; i < 3; i++) {
|
||||
for (let i = 0; i < 3 && this.connected; i++) {
|
||||
const retry = this.queue.next();
|
||||
if (!retry) break;
|
||||
if (await this.resolveAndPlay(retry)) {
|
||||
|
||||
@@ -17,7 +17,9 @@ export interface ApiServerManager {
|
||||
function isPortFree(port: number): Promise<boolean> {
|
||||
return new Promise((resolve) => {
|
||||
const server = net.createServer();
|
||||
server.once("error", () => resolve(false));
|
||||
server.once("error", () => {
|
||||
server.close(() => resolve(false));
|
||||
});
|
||||
server.once("listening", () => {
|
||||
server.close(() => resolve(true));
|
||||
});
|
||||
|
||||
+41
-12
@@ -10,7 +10,6 @@ import {
|
||||
type Identity,
|
||||
type TextMessage,
|
||||
type ClientInfo,
|
||||
type CommandMiddleware,
|
||||
} from "@honeybbq/teamspeak-client";
|
||||
import type { Logger } from "../logger.js";
|
||||
import {
|
||||
@@ -18,7 +17,7 @@ import {
|
||||
type ServerProtocol,
|
||||
} from "./protocol-detect.js";
|
||||
import { TS6HttpQuery } from "./http-query.js";
|
||||
import { ts6VersionMiddleware } from "./ts6-compat.js";
|
||||
import { patchClientInitVersion } from "./ts6-compat.js";
|
||||
|
||||
export { CODEC_OPUS_MUSIC } from "./voice.js";
|
||||
export type { ServerProtocol } from "./protocol-detect.js";
|
||||
@@ -120,10 +119,15 @@ export class TS3Client extends EventEmitter {
|
||||
});
|
||||
}
|
||||
|
||||
// Guard against calling connect() while already connected
|
||||
// Guard against calling connect() while already connected.
|
||||
// Save detectedProtocol first because disconnect() resets it.
|
||||
if (this.client) {
|
||||
this.logger.warn("connect() called while already connected, disconnecting first");
|
||||
const savedProtocol = this.detectedProtocol;
|
||||
const savedHttpQuery = this.httpQuery;
|
||||
this.disconnect();
|
||||
this.detectedProtocol = savedProtocol;
|
||||
this.httpQuery = savedHttpQuery;
|
||||
// Give the old client a moment to tear down
|
||||
await new Promise((r) => setTimeout(r, 100));
|
||||
}
|
||||
@@ -140,7 +144,9 @@ export class TS3Client extends EventEmitter {
|
||||
udpErrorCount++;
|
||||
if (udpErrorCount === 1) {
|
||||
this.logger.warn(msg);
|
||||
// After 2 seconds, log a summary and reset
|
||||
// After 2 seconds, log a summary and reset.
|
||||
// Clear any previous timer to avoid leaking it.
|
||||
if (this.udpErrorTimer) clearTimeout(this.udpErrorTimer);
|
||||
this.udpErrorTimer = setTimeout(() => {
|
||||
if (udpErrorCount > 1) {
|
||||
this.logger.warn(`udp send error (repeated ${udpErrorCount} times, connection may be lost)`);
|
||||
@@ -154,13 +160,6 @@ export class TS3Client extends EventEmitter {
|
||||
this.logger.warn(msg);
|
||||
};
|
||||
|
||||
// Apply TS6 version middleware if connecting to a TS6 server
|
||||
const commandMiddleware: CommandMiddleware[] = [];
|
||||
if (this.detectedProtocol === "ts6") {
|
||||
commandMiddleware.push(ts6VersionMiddleware("3.6.2"));
|
||||
this.logger.info("Applying TS6 compatibility: upgrading client_version to 3.6.2");
|
||||
}
|
||||
|
||||
this.client = new TS3FullClient(this.identity, addr, this.options.nickname, {
|
||||
logger: {
|
||||
debug: (msg) => this.logger.debug(msg),
|
||||
@@ -168,7 +167,6 @@ export class TS3Client extends EventEmitter {
|
||||
warn: throttledWarn,
|
||||
error: (msg) => this.logger.error(msg),
|
||||
},
|
||||
commandMiddleware,
|
||||
});
|
||||
|
||||
this.client.on("textMessage", (msg: TextMessage) => {
|
||||
@@ -196,6 +194,37 @@ export class TS3Client extends EventEmitter {
|
||||
});
|
||||
|
||||
await this.client.connect();
|
||||
|
||||
// Patch handler.sendPacket to intercept clientinit during handshake for TS6.
|
||||
//
|
||||
// Why after connect(): The library's connect() internally calls #S() which
|
||||
// replaces this.handler with a fresh PacketHandler. Any patch applied before
|
||||
// connect() would be discarded. Patching here is safe because connect() only
|
||||
// establishes the UDP socket and sends the first Init1 packet — the actual
|
||||
// clientinit command is sent later in async message callbacks (after Init1
|
||||
// round-trips complete), which cannot fire before this sync continuation.
|
||||
//
|
||||
// Why not commandMiddleware: The library's middleware chain only applies to
|
||||
// post-connection commands (sendCommandNoWait/execCommand). The handshake's
|
||||
// clientinit is sent directly via handler.sendPacket(), bypassing middleware.
|
||||
if (this.detectedProtocol === "ts6") {
|
||||
this.logger.info("Applying TS6 compatibility: upgrading client_version to 3.6.2");
|
||||
const handler = this.client.handler;
|
||||
const origSendPacket = handler.sendPacket.bind(handler);
|
||||
handler.sendPacket = (pType: number, data: Uint8Array, flags: number) => {
|
||||
// PacketType.Command = 2
|
||||
if (pType === 2) {
|
||||
const str = Buffer.from(data).toString("utf-8");
|
||||
if (str.startsWith("clientinit ")) {
|
||||
const patched = patchClientInitVersion(str);
|
||||
origSendPacket(pType, Buffer.from(patched), flags);
|
||||
return;
|
||||
}
|
||||
}
|
||||
origSendPacket(pType, data, flags);
|
||||
};
|
||||
}
|
||||
|
||||
await this.client.waitConnected();
|
||||
this.clientId = this.client.clientID();
|
||||
this.voiceFramesSent = 0;
|
||||
|
||||
@@ -1,45 +1,22 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { ts6VersionMiddleware } from "./ts6-compat.js";
|
||||
import { patchClientInitVersion } from "./ts6-compat.js";
|
||||
|
||||
describe("ts6-compat", () => {
|
||||
it("patches clientinit version to 3.6.2", async () => {
|
||||
const middleware = ts6VersionMiddleware("3.6.2");
|
||||
let captured = "";
|
||||
const next = async (cmd: string) => {
|
||||
captured = cmd;
|
||||
};
|
||||
const handler = middleware(next);
|
||||
|
||||
it("patches clientinit version to 3.6.2", () => {
|
||||
const original =
|
||||
"clientinit client_nickname=Bot client_version=3.5.3\\s[Build:\\s1587971024] " +
|
||||
"client_platform=Windows client_version_sign=Kvmj7qX6wJCPI5GVT71samfmhz/bvs7M+OTXWB/JWxdQbxDe17xda7dzUWLX7pjvdJTqZmbse1HBmTxThPKvAg== " +
|
||||
"client_key_offset=42 hwid=abc123";
|
||||
|
||||
await handler(original);
|
||||
const patched = patchClientInitVersion(original);
|
||||
|
||||
expect(captured).toContain("client_version=3.6.2\\s[Build:\\s1695203293]");
|
||||
expect(captured).toContain(
|
||||
expect(patched).toContain("client_version=3.6.2\\s[Build:\\s1695203293]");
|
||||
expect(patched).toContain(
|
||||
"client_version_sign=4OH3unxjGNBYS5EN4RFNrEo3UJz2Jn5KW1JqMDh3Yy93mSd0IwPm3FBrv8hCJgLuv99y6yBSN7pOmOpFjDaCw=="
|
||||
);
|
||||
// Other fields preserved
|
||||
expect(captured).toContain("client_nickname=Bot");
|
||||
expect(captured).toContain("client_key_offset=42");
|
||||
expect(captured).toContain("hwid=abc123");
|
||||
});
|
||||
|
||||
it("does not modify non-clientinit commands", async () => {
|
||||
const middleware = ts6VersionMiddleware("3.6.2");
|
||||
let captured = "";
|
||||
const handler = middleware(async (cmd) => {
|
||||
captured = cmd;
|
||||
});
|
||||
|
||||
const original = "sendtextmessage targetmode=2 msg=hello";
|
||||
await handler(original);
|
||||
expect(captured).toBe(original);
|
||||
});
|
||||
|
||||
it("throws for unknown target version", () => {
|
||||
expect(() => ts6VersionMiddleware("9.9.9")).toThrow("Unknown version");
|
||||
expect(patched).toContain("client_nickname=Bot");
|
||||
expect(patched).toContain("client_key_offset=42");
|
||||
expect(patched).toContain("hwid=abc123");
|
||||
});
|
||||
});
|
||||
@@ -1,57 +1,29 @@
|
||||
import type { CommandMiddleware } from "@honeybbq/teamspeak-client";
|
||||
|
||||
/**
|
||||
* Known-compatible client versions with their signatures.
|
||||
*
|
||||
* The version_sign is a P-256 ECDSA signature over the version string,
|
||||
* verified by TeamSpeak servers. Only officially signed versions work.
|
||||
* TS6 version used for compatibility. The version_sign is a P-256 ECDSA
|
||||
* signature verified by TeamSpeak servers; only officially signed pairs work.
|
||||
*/
|
||||
const KNOWN_VERSIONS: Record<
|
||||
string,
|
||||
{ version: string; platform: string; sign: string }
|
||||
> = {
|
||||
// TS3 client 3.5.3 (default in @honeybbq/teamspeak-client)
|
||||
"3.5.3": {
|
||||
version: "3.5.3 [Build: 1587971024]",
|
||||
platform: "Windows",
|
||||
sign: "Kvmj7qX6wJCPI5GVT71samfmhz/bvs7M+OTXWB/JWxdQbxDe17xda7dzUWLX7pjvdJTqZmbse1HBmTxThPKvAg==",
|
||||
},
|
||||
// TS3 client 3.6.2 (used by NeteaseTSBot, known to work with TS6 servers)
|
||||
"3.6.2": {
|
||||
version: "3.6.2 [Build: 1695203293]",
|
||||
platform: "Windows",
|
||||
sign: "4OH3unxjGNBYS5EN4RFNrEo3UJz2Jn5KW1JqMDh3Yy93mSd0IwPm3FBrv8hCJgLuv99y6yBSN7pOmOpFjDaCw==",
|
||||
},
|
||||
const TS6_VERSION = {
|
||||
version: "3.6.2 [Build: 1695203293]",
|
||||
platform: "Windows",
|
||||
sign: "4OH3unxjGNBYS5EN4RFNrEo3UJz2Jn5KW1JqMDh3Yy93mSd0IwPm3FBrv8hCJgLuv99y6yBSN7pOmOpFjDaCw==",
|
||||
};
|
||||
|
||||
/**
|
||||
* Command middleware that upgrades the client_version in `clientinit` to
|
||||
* a newer version known to work with TS6 servers.
|
||||
* Patch a raw `clientinit` command string to use the 3.6.2 version.
|
||||
*
|
||||
* TS6 servers may reject connections from older client versions. The default
|
||||
* library sends version 3.5.3; this middleware patches it to 3.6.2 which is
|
||||
* the version NeteaseTSBot uses successfully with TS6.
|
||||
* The @honeybbq/teamspeak-client library hardcodes version 3.5.3 in the
|
||||
* handshake and sends it directly via handler.sendPacket(), bypassing the
|
||||
* commandMiddleware chain. TS6 servers silently reject 3.5.3 (they never
|
||||
* respond with `initserver`), causing an idle timeout.
|
||||
*
|
||||
* The version_sign must match the version string — it's a server-verified
|
||||
* ECDSA signature, so we can only use known-valid pairs.
|
||||
* This function is called from a monkey-patched handler.sendPacket() so it
|
||||
* intercepts the actual handshake packets.
|
||||
*/
|
||||
export function ts6VersionMiddleware(
|
||||
targetVersion = "3.6.2",
|
||||
): CommandMiddleware {
|
||||
const vInfo = KNOWN_VERSIONS[targetVersion];
|
||||
if (!vInfo) {
|
||||
throw new Error(`Unknown version ${targetVersion}. Known: ${Object.keys(KNOWN_VERSIONS).join(", ")}`);
|
||||
}
|
||||
|
||||
return (next) => async (cmd) => {
|
||||
// Only intercept clientinit commands
|
||||
if (cmd.startsWith("clientinit ")) {
|
||||
cmd = replaceField(cmd, "client_version", vInfo.version);
|
||||
cmd = replaceField(cmd, "client_platform", vInfo.platform);
|
||||
cmd = replaceField(cmd, "client_version_sign", vInfo.sign);
|
||||
}
|
||||
return next(cmd);
|
||||
};
|
||||
export function patchClientInitVersion(cmd: string): string {
|
||||
cmd = replaceField(cmd, "client_version", TS6_VERSION.version);
|
||||
cmd = replaceField(cmd, "client_platform", TS6_VERSION.platform);
|
||||
cmd = replaceField(cmd, "client_version_sign", TS6_VERSION.sign);
|
||||
return cmd;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in new issue
Block a user