mirror of
https://github.com/ZHANGTIANYAO1/teamspeak-music-bot.git
synced 2026-10-01 20:42:50 +08:00
Add TS6 clientinit version middleware and fix playNext retry bug
clientinit compatibility: - Analyzed @honeybbq/teamspeak-client's clientinit: it already sends a clean 14-field payload without problematic fields (no badges, integrations, security_hash, etc.) - The key difference vs NeteaseTSBot is client_version: our library sends 3.5.3, NeteaseTSBot sends 3.6.2. TS6 servers may reject older versions. - Add ts6-compat.ts with CommandMiddleware that patches clientinit to use version 3.6.2 + matching ECDSA signature when connecting to detected TS6 servers - Middleware is automatically applied when detectedProtocol === "ts6" Pre-existing bug fix: - Fix playNext() in instance.ts where successful retry still fell through to player.stop(), killing the just-started playback https://claude.ai/code/session_016WhH58avUD9xy2dgADJgTh
This commit is contained in:
4 files changed
+143
-4
No files matched your search
+8
-3
@@ -494,14 +494,19 @@ export class BotInstance extends EventEmitter {
|
|||||||
this.voteSkipUsers.clear();
|
this.voteSkipUsers.clear();
|
||||||
const next = this.queue.next();
|
const next = this.queue.next();
|
||||||
if (next) {
|
if (next) {
|
||||||
const ok = await this.resolveAndPlay(next);
|
let started = await this.resolveAndPlay(next);
|
||||||
if (!ok) {
|
if (!started) {
|
||||||
// Skip to next if URL resolve fails (up to 3 retries)
|
// Skip to next if URL resolve fails (up to 3 retries)
|
||||||
for (let i = 0; i < 3; i++) {
|
for (let i = 0; i < 3; i++) {
|
||||||
const retry = this.queue.next();
|
const retry = this.queue.next();
|
||||||
if (!retry) break;
|
if (!retry) break;
|
||||||
if (await this.resolveAndPlay(retry)) break;
|
if (await this.resolveAndPlay(retry)) {
|
||||||
|
started = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
if (!started) {
|
||||||
this.player.stop();
|
this.player.stop();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -10,14 +10,15 @@ import {
|
|||||||
type Identity,
|
type Identity,
|
||||||
type TextMessage,
|
type TextMessage,
|
||||||
type ClientInfo,
|
type ClientInfo,
|
||||||
|
type CommandMiddleware,
|
||||||
} from "@honeybbq/teamspeak-client";
|
} from "@honeybbq/teamspeak-client";
|
||||||
import type { Logger } from "../logger.js";
|
import type { Logger } from "../logger.js";
|
||||||
import {
|
import {
|
||||||
detectServerProtocol,
|
detectServerProtocol,
|
||||||
type ServerProtocol,
|
type ServerProtocol,
|
||||||
type ProtocolDetectResult,
|
|
||||||
} from "./protocol-detect.js";
|
} from "./protocol-detect.js";
|
||||||
import { TS6HttpQuery } from "./http-query.js";
|
import { TS6HttpQuery } from "./http-query.js";
|
||||||
|
import { ts6VersionMiddleware } from "./ts6-compat.js";
|
||||||
|
|
||||||
export { CODEC_OPUS_MUSIC } from "./voice.js";
|
export { CODEC_OPUS_MUSIC } from "./voice.js";
|
||||||
export type { ServerProtocol } from "./protocol-detect.js";
|
export type { ServerProtocol } from "./protocol-detect.js";
|
||||||
@@ -153,6 +154,13 @@ export class TS3Client extends EventEmitter {
|
|||||||
this.logger.warn(msg);
|
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, {
|
this.client = new TS3FullClient(this.identity, addr, this.options.nickname, {
|
||||||
logger: {
|
logger: {
|
||||||
debug: (msg) => this.logger.debug(msg),
|
debug: (msg) => this.logger.debug(msg),
|
||||||
@@ -160,6 +168,7 @@ export class TS3Client extends EventEmitter {
|
|||||||
warn: throttledWarn,
|
warn: throttledWarn,
|
||||||
error: (msg) => this.logger.error(msg),
|
error: (msg) => this.logger.error(msg),
|
||||||
},
|
},
|
||||||
|
commandMiddleware,
|
||||||
});
|
});
|
||||||
|
|
||||||
this.client.on("textMessage", (msg: TextMessage) => {
|
this.client.on("textMessage", (msg: TextMessage) => {
|
||||||
|
|||||||
@@ -0,0 +1,45 @@
|
|||||||
|
import { describe, it, expect } from "vitest";
|
||||||
|
import { ts6VersionMiddleware } 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);
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
expect(captured).toContain("client_version=3.6.2\\s[Build:\\s1695203293]");
|
||||||
|
expect(captured).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");
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
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.
|
||||||
|
*/
|
||||||
|
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==",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Command middleware that upgrades the client_version in `clientinit` to
|
||||||
|
* a newer version known to work with TS6 servers.
|
||||||
|
*
|
||||||
|
* 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 version_sign must match the version string — it's a server-verified
|
||||||
|
* ECDSA signature, so we can only use known-valid pairs.
|
||||||
|
*/
|
||||||
|
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);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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");
|
||||||
|
}
|
||||||
Reference in new issue
Block a user