Add dual TS3/TS6 protocol support with auto-detection

The @honeybbq/teamspeak-client library already handles TS6 license block
type 8 (Ts5Server) in its handshake, so voice connections work with both
TS3 and TS6 servers. This commit adds the surrounding infrastructure:

- protocol-detect.ts: Auto-detect server type by probing TS3 ServerQuery
  (port 10011) and TS6 HTTP Query (port 10080) in parallel
- http-query.ts: TS6 HTTP Query client replacing the raw-TCP ServerQuery
  that TS6 servers no longer support (ports 10080/10443)
- client.ts: Protocol-aware connection with auto-detection, TS6 HTTP
  Query setup, and forced protocol override option
- manager.ts: Pass through serverProtocol and ts6ApiKey config options
- connection.ts: Mark legacy TS3 ServerQuery as deprecated for TS6

https://claude.ai/code/session_016WhH58avUD9xy2dgADJgTh
This commit is contained in:
Claude committed 2026-04-03 13:03:47 +00:00
1 parent 01940e74dd
commit 9e51ba6cd4
6 files changed
+386 -5

No files matched your search

+171
View File
@@ -0,0 +1,171 @@
import http from "node:http";
import https from "node:https";
import { EventEmitter } from "node:events";
export interface HttpQueryOptions {
host: string;
port: number; // 10080 (HTTP) or 10443 (HTTPS)
useTls?: boolean;
apiKey?: string;
timeoutMs?: number;
}
export interface HttpQueryResult {
status: number;
body: unknown;
}
/**
* TS6 HTTP Query client.
*
* TeamSpeak 6 Server replaces the TS3 raw-TCP ServerQuery (port 10011)
* with an HTTP/HTTPS API on ports 10080/10443.
*
* Common endpoints (TS6 HTTP Query):
* GET / → server info / health check
* POST /api-key → create API key
* GET /1/serverlist → list virtual servers
* GET /1/clientlist?sid={sid} → list clients
* POST /1/sendtextmessage → send text message
* POST /1/clientmove → move a client
* GET /1/channellist?sid={sid} → list channels
* POST /1/clientupdate → update client properties
*/
export class TS6HttpQuery extends EventEmitter {
private options: Required<HttpQueryOptions>;
constructor(options: HttpQueryOptions) {
super();
this.options = {
host: options.host,
port: options.port,
useTls: options.useTls ?? options.port === 10443,
apiKey: options.apiKey ?? "",
timeoutMs: options.timeoutMs ?? 5000,
};
}
async request(
method: "GET" | "POST" | "PUT" | "DELETE",
path: string,
body?: Record<string, unknown>,
): Promise<HttpQueryResult> {
const { host, port, useTls, apiKey, timeoutMs } = this.options;
const transport = useTls ? https : http;
const headers: Record<string, string> = {
Accept: "application/json",
};
if (apiKey) {
headers["x-api-key"] = apiKey;
}
let bodyStr: string | undefined;
if (body) {
bodyStr = JSON.stringify(body);
headers["Content-Type"] = "application/json";
headers["Content-Length"] = String(Buffer.byteLength(bodyStr));
}
return new Promise((resolve, reject) => {
const req = transport.request(
{
hostname: host,
port,
path,
method,
timeout: timeoutMs,
headers,
rejectUnauthorized: false, // self-signed certs common on self-hosted
},
(res) => {
let data = "";
res.setEncoding("utf-8");
res.on("data", (chunk: string) => (data += chunk));
res.on("end", () => {
let parsed: unknown;
try {
parsed = JSON.parse(data);
} catch {
parsed = data;
}
resolve({
status: res.statusCode ?? 0,
body: parsed,
});
});
},
);
req.on("error", reject);
req.on("timeout", () => {
req.destroy();
reject(new Error("TS6 HTTP Query timeout"));
});
if (bodyStr) {
req.write(bodyStr);
}
req.end();
});
}
/** Check if the TS6 HTTP Query is reachable */
async healthCheck(): Promise<boolean> {
try {
const result = await this.request("GET", "/");
return result.status >= 200 && result.status < 500;
} catch {
return false;
}
}
/** List virtual servers */
async serverList(): Promise<HttpQueryResult> {
return this.request("GET", "/1/serverlist");
}
/** List clients on a virtual server */
async clientList(sid = 1): Promise<HttpQueryResult> {
return this.request("GET", `/1/clientlist?sid=${sid}`);
}
/** List channels on a virtual server */
async channelList(sid = 1): Promise<HttpQueryResult> {
return this.request("GET", `/1/channellist?sid=${sid}`);
}
/** Send a text message */
async sendTextMessage(
targetMode: number,
target: number,
msg: string,
sid = 1,
): Promise<HttpQueryResult> {
return this.request("POST", `/1/sendtextmessage?sid=${sid}`, {
targetmode: targetMode,
target,
msg,
});
}
/** Update client properties (e.g., description) */
async clientUpdate(
properties: Record<string, string | number>,
sid = 1,
): Promise<HttpQueryResult> {
return this.request("POST", `/1/clientupdate?sid=${sid}`, properties);
}
/** Move a client to a channel */
async clientMove(
clid: number,
cid: number,
cpw?: string,
sid = 1,
): Promise<HttpQueryResult> {
const body: Record<string, unknown> = { clid, cid };
if (cpw) body.cpw = cpw;
return this.request("POST", `/1/clientmove?sid=${sid}`, body);
}
}