From ca4cb1790ad94301f333e23a985d5d7454b93333 Mon Sep 17 00:00:00 2001 From: saopig1 Date: Sat, 11 Apr 2026 01:54:43 +0800 Subject: [PATCH] chore(deps): bump @honeybbq/teamspeak-client to 0.2.1, drop ts6-compat shim MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Version 0.2.1 ships a universal clientinit format that works natively against both TS3 and TS6 servers: client_version: "3.?.? [Build: 5680278000]" client_version_sign: DX5NIYLvfJEUjuIbCidnoeozxIDRRkpq3I9vVMBmE9L2qnekOo BzSenkzsg2lC9CMv8K5hkEzhr2TYUYSwUXCg== The old ts6-compat.ts workaround (monkey-patching handler.sendPacket to rewrite clientinit's client_version to "3.6.2") is now actively wrong: it replaces the library's new correct version/signature pair with a stale one that TS6 servers reject, which is why the first 0.2.1 TS6 handshake attempt still hung at `received initivexpand2`. Changes: - package.json: "@honeybbq/teamspeak-client": "^0.1.0" -> "^0.2.1" - src/ts-protocol/client.ts: remove patchClientInitVersion import and the sendPacket monkey-patch block. Leave an inline comment so anyone reading the git blame understands why the shim is gone. - Delete src/ts-protocol/ts6-compat.ts and ts6-compat.test.ts — no callers remain. Other 0.2.x notes worth knowing (no code change here, just documenting): - ClientOptions gained serverPassword / defaultChannel / defaultChannelPassword that are sent DURING clientinit. We still call our own joinChannel() post-connect because the existing flow works and switching is an orthogonal refactor. - 0.1.1 contains the P-256 DER encoding fix (PR #5 by ZHANGTIANYAO1). Identities generated by 0.1.0 are cryptographically incompatible with 0.2.x's corrected handshake path — a bot whose identity column was populated before this upgrade will hang at `received initivexpand2` and fall through the 15s connect deadline. Workaround: clear the identity column so the next start generates a fresh key. Server groups assigned to the old UID must be re-granted once against the new one. Verification: - tsc --noEmit clean - vitest: 93/93 unit tests pass (1 test file removed with ts6-compat) - scripts/test_full_feature.py against a local TS6 server: 51/51 pass, including handshake, voice playback, identity persistence, WebSocket stateChange broadcasts, and all corner-case regressions. - Live: bot connected to TS6 in ~80ms after identity regeneration, played NetEase audio through the voice channel, clean stop. Co-Authored-By: Claude Opus 4.6 (1M context) --- package-lock.json | 8 ++--- package.json | 2 +- src/ts-protocol/client.ts | 38 ++++------------------ src/ts-protocol/ts6-compat.test.ts | 22 ------------- src/ts-protocol/ts6-compat.ts | 52 ------------------------------ 5 files changed, 11 insertions(+), 111 deletions(-) delete mode 100644 src/ts-protocol/ts6-compat.test.ts delete mode 100644 src/ts-protocol/ts6-compat.ts diff --git a/package-lock.json b/package-lock.json index 468387e..adbc059 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,7 @@ "license": "MIT", "dependencies": { "@discordjs/opus": "^0.10.0", - "@honeybbq/teamspeak-client": "^0.1.0", + "@honeybbq/teamspeak-client": "^0.2.1", "@koa/router": "^15.4.0", "@sansenjian/qq-music-api": "^2.2.9", "axios": "^1.14.0", @@ -573,9 +573,9 @@ "license": "BSD-3-Clause" }, "node_modules/@honeybbq/teamspeak-client": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/@honeybbq/teamspeak-client/-/teamspeak-client-0.1.0.tgz", - "integrity": "sha512-Gh/siE4a0NRxqEqiwbvYn6p4ulphG6IVZaTCuCUjS/+cD6rVvWNrCgbJ0724RdkEY5yndoQ34zkjE/Lglk1wTQ==", + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/@honeybbq/teamspeak-client/-/teamspeak-client-0.2.1.tgz", + "integrity": "sha512-iLZRWsqeAA/IuEL0zMAhHm9+wQ1YmByXjh9T6PZLYSxFk56T39MpQobT6eohM/PnIz3wQZymd8h701hyliLo2w==", "license": "MIT", "dependencies": { "@noble/curves": "^2.0.1" diff --git a/package.json b/package.json index 1123e8d..164e7ef 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,7 @@ "license": "MIT", "dependencies": { "@discordjs/opus": "^0.10.0", - "@honeybbq/teamspeak-client": "^0.1.0", + "@honeybbq/teamspeak-client": "^0.2.1", "@koa/router": "^15.4.0", "@sansenjian/qq-music-api": "^2.2.9", "axios": "^1.14.0", diff --git a/src/ts-protocol/client.ts b/src/ts-protocol/client.ts index b333e79..fdf2f1a 100644 --- a/src/ts-protocol/client.ts +++ b/src/ts-protocol/client.ts @@ -17,7 +17,6 @@ import { type ServerProtocol, } from "./protocol-detect.js"; import { TS6HttpQuery } from "./http-query.js"; -import { patchClientInitVersion } from "./ts6-compat.js"; export { CODEC_OPUS_MUSIC } from "./voice.js"; export type { ServerProtocol } from "./protocol-detect.js"; @@ -206,37 +205,12 @@ 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); - }; - } - + // Note: @honeybbq/teamspeak-client 0.2.x ships a universal clientinit + // (client_version "3.?.? [Build: 5680278000]" + matching signature) + // that works against both TS3 and TS6 servers. The old 3.6.2 monkey- + // patch on handler.sendPacket was removed when we bumped to 0.2.1 — it + // would have replaced the library's new correct version with a stale + // signature and made TS6 handshakes fail. await this.client.waitConnected(); this.clientId = this.client.clientID(); this.voiceFramesSent = 0; diff --git a/src/ts-protocol/ts6-compat.test.ts b/src/ts-protocol/ts6-compat.test.ts deleted file mode 100644 index 3a6211c..0000000 --- a/src/ts-protocol/ts6-compat.test.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { describe, it, expect } from "vitest"; -import { patchClientInitVersion } from "./ts6-compat.js"; - -describe("ts6-compat", () => { - 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"; - - const patched = patchClientInitVersion(original); - - expect(patched).toContain("client_version=3.6.2\\s[Build:\\s1695203293]"); - expect(patched).toContain( - "client_version_sign=4OH3unxjGNBYS5EN4RFNrEo3UJz2Jn5KW1JqMDh3Yy93mSd0IwPm3FBrv8hCJgLuv99y6yBSN7pOmOpFjDaCw==" - ); - // Other fields preserved - expect(patched).toContain("client_nickname=Bot"); - expect(patched).toContain("client_key_offset=42"); - expect(patched).toContain("hwid=abc123"); - }); -}); diff --git a/src/ts-protocol/ts6-compat.ts b/src/ts-protocol/ts6-compat.ts deleted file mode 100644 index 4ff8bd9..0000000 --- a/src/ts-protocol/ts6-compat.ts +++ /dev/null @@ -1,52 +0,0 @@ -/** - * TS6 version used for compatibility. The version_sign is a P-256 ECDSA - * signature verified by TeamSpeak servers; only officially signed pairs work. - */ -const TS6_VERSION = { - version: "3.6.2 [Build: 1695203293]", - platform: "Windows", - sign: "4OH3unxjGNBYS5EN4RFNrEo3UJz2Jn5KW1JqMDh3Yy93mSd0IwPm3FBrv8hCJgLuv99y6yBSN7pOmOpFjDaCw==", -}; - -/** - * Patch a raw `clientinit` command string to use the 3.6.2 version. - * - * 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. - * - * This function is called from a monkey-patched handler.sendPacket() so it - * intercepts the actual handshake packets. - */ -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; -} - -/** - * Replace an escaped field value in a TS3 command string. - * Fields are in the format: key=escaped_value (space-separated) - */ -function replaceField(cmd: string, key: string, value: string): string { - // Escape the value using TS3 escaping rules - const escaped = escapeTS3(value); - const regex = new RegExp(`${key}=\\S*`); - if (regex.test(cmd)) { - return cmd.replace(regex, `${key}=${escaped}`); - } - return cmd; -} - -function escapeTS3(value: string): string { - return value - .replace(/\\/g, "\\\\") - .replace(/\//g, "\\/") - .replace(/ /g, "\\s") - .replace(/\|/g, "\\p") - .replace(/\n/g, "\\n") - .replace(/\r/g, "\\r") - .replace(/\t/g, "\\t"); -}