mirror of
https://github.com/ZHANGTIANYAO1/teamspeak-music-bot.git
synced 2026-10-02 04:52:50 +08:00
feat: add TS3 command encoding/decoding with escape handling
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
2b6e81119b
commit
e059ca5bfe
2 files changed
+132
No files matched your search
@@ -0,0 +1,55 @@
|
|||||||
|
import { describe, it, expect } from "vitest";
|
||||||
|
import {
|
||||||
|
encodeCommand,
|
||||||
|
decodeResponse,
|
||||||
|
escapeValue,
|
||||||
|
unescapeValue,
|
||||||
|
} from "./commands.js";
|
||||||
|
|
||||||
|
describe("TS3 Commands", () => {
|
||||||
|
it("encodes a simple command", () => {
|
||||||
|
const encoded = encodeCommand("login", {
|
||||||
|
client_login_name: "bot",
|
||||||
|
client_login_password: "pass",
|
||||||
|
});
|
||||||
|
expect(encoded).toBe(
|
||||||
|
"login client_login_name=bot client_login_password=pass\n"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("escapes special characters in values", () => {
|
||||||
|
expect(escapeValue("hello world")).toBe("hello\\sworld");
|
||||||
|
expect(escapeValue("foo|bar")).toBe("foo\\pbar");
|
||||||
|
expect(escapeValue("a/b")).toBe("a\\/b");
|
||||||
|
expect(escapeValue("line\nnew")).toBe("line\\nnew");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("unescapes special characters", () => {
|
||||||
|
expect(unescapeValue("hello\\sworld")).toBe("hello world");
|
||||||
|
expect(unescapeValue("foo\\pbar")).toBe("foo|bar");
|
||||||
|
expect(unescapeValue("a\\/b")).toBe("a/b");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("decodes a single response", () => {
|
||||||
|
const response =
|
||||||
|
"virtualserver_name=My\\sServer virtualserver_port=9987";
|
||||||
|
const result = decodeResponse(response);
|
||||||
|
expect(result).toHaveLength(1);
|
||||||
|
expect(result[0].virtualserver_name).toBe("My Server");
|
||||||
|
expect(result[0].virtualserver_port).toBe("9987");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("decodes a piped multi-entry response", () => {
|
||||||
|
const response =
|
||||||
|
"clid=1 client_nickname=User1|clid=2 client_nickname=User2";
|
||||||
|
const result = decodeResponse(response);
|
||||||
|
expect(result).toHaveLength(2);
|
||||||
|
expect(result[0].client_nickname).toBe("User1");
|
||||||
|
expect(result[1].client_nickname).toBe("User2");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("handles command with no params", () => {
|
||||||
|
const encoded = encodeCommand("quit", {});
|
||||||
|
expect(encoded).toBe("quit\n");
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
const ESCAPE_MAP: [string, string][] = [
|
||||||
|
["\\", "\\\\"],
|
||||||
|
["/", "\\/"],
|
||||||
|
[" ", "\\s"],
|
||||||
|
["|", "\\p"],
|
||||||
|
["\n", "\\n"],
|
||||||
|
["\r", "\\r"],
|
||||||
|
["\t", "\\t"],
|
||||||
|
["\x07", "\\a"],
|
||||||
|
["\x08", "\\b"],
|
||||||
|
["\x0C", "\\f"],
|
||||||
|
["\x0B", "\\v"],
|
||||||
|
];
|
||||||
|
|
||||||
|
const UNESCAPE_MAP: [string, string][] = ESCAPE_MAP.map(
|
||||||
|
([plain, escaped]) => [escaped, plain] as [string, string]
|
||||||
|
).reverse();
|
||||||
|
|
||||||
|
export function escapeValue(value: string): string {
|
||||||
|
let result = value;
|
||||||
|
for (const [plain, escaped] of ESCAPE_MAP) {
|
||||||
|
result = result.replaceAll(plain, escaped);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function unescapeValue(value: string): string {
|
||||||
|
let result = value;
|
||||||
|
for (const [escaped, plain] of UNESCAPE_MAP) {
|
||||||
|
result = result.replaceAll(escaped, plain);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function encodeCommand(
|
||||||
|
command: string,
|
||||||
|
params: Record<string, string | number>
|
||||||
|
): string {
|
||||||
|
const parts = [command];
|
||||||
|
for (const [key, value] of Object.entries(params)) {
|
||||||
|
parts.push(`${key}=${escapeValue(String(value))}`);
|
||||||
|
}
|
||||||
|
return parts.join(" ") + "\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
export function decodeResponse(raw: string): Record<string, string>[] {
|
||||||
|
const entries = raw.split("|");
|
||||||
|
return entries.map((entry) => {
|
||||||
|
const result: Record<string, string> = {};
|
||||||
|
const pairs = entry.trim().split(" ");
|
||||||
|
for (const pair of pairs) {
|
||||||
|
const eqIndex = pair.indexOf("=");
|
||||||
|
if (eqIndex === -1) {
|
||||||
|
result[pair] = "";
|
||||||
|
} else {
|
||||||
|
const key = pair.substring(0, eqIndex);
|
||||||
|
const value = unescapeValue(pair.substring(eqIndex + 1));
|
||||||
|
result[key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TS3Response {
|
||||||
|
errorId: number;
|
||||||
|
errorMessage: string;
|
||||||
|
data: Record<string, string>[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parseErrorLine(line: string): { id: number; msg: string } {
|
||||||
|
const decoded = decodeResponse(line.replace(/^error\s+/, ""))[0];
|
||||||
|
return {
|
||||||
|
id: parseInt(decoded.id ?? "0", 10),
|
||||||
|
msg: decoded.msg ?? "ok",
|
||||||
|
};
|
||||||
|
}
|
||||||
Reference in new issue
Block a user