mirror of
https://github.com/ZHANGTIANYAO1/teamspeak-music-bot.git
synced 2026-10-02 04:52:50 +08:00
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:
6 files changed
+386
-5
No files matched your search
@@ -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);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user