node-ts
Version:
TeamSpeak® 3 Server Query client for node.js implemented using TypeScript
434 lines (431 loc) • 15.9 kB
JavaScript
/**
* @autor Niklas Mollenhauer <holzig@outlook.com>
* @autor Tim Kluge <timklge@wh2.tu-dresden.de>
*/
import * as net from "node:net";
import { EventEmitter } from "node:events";
import { chunksToLinesAsync, chomp } from "@rauschma/stringio";
import { escapeQueryString, unescapeQueryString } from "./queryStrings.js";
/**
* Client that can be used to connect to a TeamSpeak server query API.
*/
export class TeamSpeakClient extends EventEmitter {
queue = [];
_executing;
socket;
isConnected = false;
static DefaultHost = "localhost";
static DefaultPort = 10011;
host;
port;
/**
* Creates a new instance of TeamSpeakClient for a specific remote host:port.
* @param {string = TeamSpeakClient.DefaultHost} host Remote host of the TeamSpeak server. Can be an IP address or a host name.
* @param {number = TeamSpeakClient.DefaultPort} port TCP port of the server query instance of the remote host.
* @constructor
*/
constructor(host = TeamSpeakClient.DefaultHost, port = TeamSpeakClient.DefaultPort) {
super();
this.host = host;
this.port = port;
}
connect() {
this.isConnected = false;
return new Promise((resolve, reject) => {
this.socket = net.connect(this.port, this.host);
this.socket.on("error", err => this.emit("error", err));
// We'll try to reject the promise if the connection closes, to make sure
// the promise gets rejected if we get an error while connecting.
// (This will just do nothing if the promise is already fulfilled)
this.socket.once("close", err => reject(err));
this.socket.on("close", () => this.emit("close", this.queue));
this.socket.on("connect", () => this.onConnect(resolve, reject));
});
}
/**
* Gets called on an opened connection
*/
async onConnect(connectionEstablished, error) {
const lineGenerator = chunksToLinesAsync(this.socket);
let lineCounter = 0;
for await (const lineWithNewLine of lineGenerator) {
const line = chomp(lineWithNewLine).trim();
if (line === "")
continue;
++lineCounter;
switch (lineCounter) {
case 1: {
if (line !== "TS3") {
this.isConnected = false;
error(new Error("Remove server is not a TS3 Query Server endpoint."));
return;
}
continue;
}
case 2:
// We have read a second non-empty line, so we are ready to take commands
this.isConnected = true;
connectionEstablished();
continue; // Welcome message, followed by empty line (which is skipped)
default: {
this.handleSingleLine(line);
this.checkQueue();
}
}
}
}
handleSingleLine(line) {
// Server answers with:
// [- One line containing the answer ]
// - "error id=XX msg=YY". ID is zero if command was executed successfully.
if (line.startsWith("error")) {
const errorResponse = line.substr("error ".length);
const response = this.parseResponse(errorResponse);
const executing = this._executing;
if (response !== undefined && executing !== undefined) {
const res = response.shift();
if (res !== undefined) {
const currentError = {
id: res["id"] || 0,
msg: res["msg"] || ""
};
if (currentError.id !== 0)
executing.error = currentError;
if (executing.rejectFunction && executing.resolveFunction) {
//item: executing || null,
const e = executing;
const data = {
cmd: e.cmd,
options: e.options || [],
text: e.text || null,
parameters: e.parameters || {},
error: e.error || null,
response: e.response || null,
rawResponse: e.rawResponse || null
};
if (data.error && data.error.id !== 0)
executing.rejectFunction(data);
else
executing.resolveFunction(data);
}
}
}
this._executing = undefined;
this.checkQueue();
}
else if (line.startsWith("notify")) {
const notificationResponse = line.substr("notify".length);
const response = this.parseResponse(notificationResponse);
const notificationName = notificationResponse.substr(0, notificationResponse.indexOf(" "));
this.emit(notificationName, response);
}
else if (this._executing) {
this._executing.rawResponse = line;
this._executing.response = this.parseResponse(line);
}
}
send(cmd, params = {}, options = []) {
if (!cmd)
return Promise.reject(new Error("Empty command"));
if (!this.isConnected)
return Promise.reject(new Error("Not connected to any server. Call \"connect()\" before sending anything."));
let tosend = escapeQueryString(cmd);
for (const v of options)
tosend += ` -${escapeQueryString(v)}`;
for (const key in params) {
if (!params.hasOwnProperty(key))
continue;
const value = params[key];
if (!Array.isArray(value)) {
tosend += ` ${escapeQueryString(key.toString())}=${escapeQueryString(value.toString())}`;
}
}
// Handle multiple arrays correctly
// Get all array in the params
const arrayParamKeys = [];
for (const key in params) {
if (params.hasOwnProperty(key) && Array.isArray(params[key]))
arrayParamKeys.push(key);
}
if (arrayParamKeys.length > 0) {
let escapedSegments = "";
const firstArray = params[arrayParamKeys[0]];
for (let i = 0; i < firstArray.length; ++i) {
let segment = "";
for (const key of arrayParamKeys) {
segment += `${escapeQueryString(key)}=${escapeQueryString(params[key][i])} `;
}
escapedSegments += `${segment.slice(0, -1)}|`;
}
if (escapedSegments.length > 0)
tosend += ` ${escapedSegments.slice(0, -1)}`;
}
return new Promise((resolve, reject) => {
this.queue.push({
cmd: cmd,
options: options,
parameters: params,
text: tosend,
resolveFunction: resolve,
rejectFunction: reject,
});
if (this.isConnected)
this.checkQueue();
});
}
subscribeChannelEvents(channelId) {
return this.send("servernotifyregister", { event: "channel", id: channelId });
}
subscribeServerEvents() {
return this.send("servernotifyregister", { event: "server" });
}
subscribeServerTextEvents() {
return this.send("servernotifyregister", { event: "textserver" });
}
subscribeChannelTextEvents() {
return this.send("servernotifyregister", { event: "textchannel" });
}
subscribePrivateTextEvents() {
return this.send("servernotifyregister", { event: "textprivate" });
}
on(event, listener) {
return super.on(event, listener);
}
once(event, listener) {
return super.once(event, listener);
}
/**
* Parses a query API response.
*/
parseResponse(s) {
const records = s.split("|");
// Test this
const response = records.map(currentItem => {
const args = currentItem.split(" ");
const thisrec = {};
for (let v of args) {
if (v.indexOf("=") <= -1) {
thisrec[v] = "";
continue;
}
const key = unescapeQueryString(v.substr(0, v.indexOf("=")));
const value = unescapeQueryString(v.substr(v.indexOf("=") + 1));
thisrec[key] = (Number.parseInt(value, 10).toString() == value) ? Number.parseInt(value, 10) : value;
}
return thisrec;
});
if (response.length === 0)
return undefined;
return response;
}
/**
* Gets pending commands that are going to be sent to the server. Note that they have been parsed - Access pending[0].text to get the full text representation of the command.
* @return {QueryCommand[]} Pending commands that are going to be sent to the server.
*/
get pending() {
return this.queue.slice(0);
}
/**
* Clears the queue of pending commands so that any command that is currently queued won't be executed.
* @return {QueryCommand[]} Array of commands that have been removed from the queue.
*/
clearPending() {
const q = this.queue;
this.queue = [];
return q;
}
/**
* Checks the current command queue and sends them if needed.
*/
checkQueue() {
if (this._executing !== undefined)
return;
const executing = this.queue.shift();
if (executing) {
this._executing = executing;
this.socket.write(this._executing.text + "\n");
}
}
/**
* Sets the socket to timeout after timeout milliseconds of inactivity on the socket. By default net.Socket do not have a timeout.
*/
setTimeout(timeout) {
this.socket.setTimeout(timeout, () => {
this.socket.destroy();
this.emit("timeout");
});
}
unsetTimeout() {
/*
* If timeout is 0, then the existing idle timeout is disabled.
* See: https://nodejs.org/api/net.html#net_socket_settimeout_timeout_callback
*/
return this.setTimeout(0);
}
}
/*
Enums imported from documentation.
*/
export var YesNo;
(function (YesNo) {
YesNo[YesNo["No"] = 0] = "No";
YesNo[YesNo["Yes"] = 1] = "Yes";
})(YesNo || (YesNo = {}));
export var HostMessageMode;
(function (HostMessageMode) {
/**
* 1: display message in chatlog
*/
HostMessageMode[HostMessageMode["LOG"] = 1] = "LOG";
/**
* 2: display message in modal dialog
*/
HostMessageMode[HostMessageMode["MODAL"] = 2] = "MODAL";
/**
* 3: display message in modal dialog and close connection
*/
HostMessageMode[HostMessageMode["MODALQUIT"] = 3] = "MODALQUIT";
})(HostMessageMode || (HostMessageMode = {}));
export var HostBannerMode;
(function (HostBannerMode) {
/**
* 0: do not adjust
*/
HostBannerMode[HostBannerMode["NOADJUST"] = 0] = "NOADJUST";
/**
* 1: adjust but ignore aspect ratio (like TeamSpeak 2)
*/
HostBannerMode[HostBannerMode["IGNOREASPECT"] = 1] = "IGNOREASPECT";
/**
* 2: adjust and keep aspect ratio
*/
HostBannerMode[HostBannerMode["KEEPASPECT"] = 2] = "KEEPASPECT";
})(HostBannerMode || (HostBannerMode = {}));
export var Codec;
(function (Codec) {
/**
* 0: speex narrowband (mono, 16bit, 8kHz)
*/
Codec[Codec["SPEEX_NARROWBAND"] = 0] = "SPEEX_NARROWBAND";
/**
* 1: speex wideband (mono, 16bit, 16kHz)
*/
Codec[Codec["SPEEX_WIDEBAND"] = 1] = "SPEEX_WIDEBAND";
/**
* 2: speex ultra-wideband (mono, 16bit, 32kHz)
*/
Codec[Codec["SPEEX_ULTRAWIDEBAND"] = 2] = "SPEEX_ULTRAWIDEBAND";
/**
* 3: celt mono (mono, 16bit, 48kHz)
*/
Codec[Codec["CELT_MONO"] = 3] = "CELT_MONO";
})(Codec || (Codec = {}));
export var CodecEncryptionMode;
(function (CodecEncryptionMode) {
/**
* 0: configure per channel
*/
CodecEncryptionMode[CodecEncryptionMode["INDIVIDUAL"] = 0] = "INDIVIDUAL";
/**
* 1: globally disabled
*/
CodecEncryptionMode[CodecEncryptionMode["DISABLED"] = 1] = "DISABLED";
/**
* 2: globally enabled
*/
CodecEncryptionMode[CodecEncryptionMode["ENABLED"] = 2] = "ENABLED";
})(CodecEncryptionMode || (CodecEncryptionMode = {}));
export var TextMessageTargetMode;
(function (TextMessageTargetMode) {
/**
* 1: target is a client
*/
TextMessageTargetMode[TextMessageTargetMode["CLIENT"] = 1] = "CLIENT";
/**
* 2: target is a channel
*/
TextMessageTargetMode[TextMessageTargetMode["CHANNEL"] = 2] = "CHANNEL";
/**
* 3: target is a virtual server
*/
TextMessageTargetMode[TextMessageTargetMode["SERVER"] = 3] = "SERVER";
})(TextMessageTargetMode || (TextMessageTargetMode = {}));
export var LogLevel;
(function (LogLevel) {
/**
* 1: everything that is really bad
*/
LogLevel[LogLevel["ERROR"] = 1] = "ERROR";
/**
* 2: everything that might be bad
*/
LogLevel[LogLevel["WARNING"] = 2] = "WARNING";
/**
* 3: output that might help find a problem
*/
LogLevel[LogLevel["DEBUG"] = 3] = "DEBUG";
/**
* 4: informational output
*/
LogLevel[LogLevel["INFO"] = 4] = "INFO";
})(LogLevel || (LogLevel = {}));
export var ReasonIdentifier;
(function (ReasonIdentifier) {
/**
* 4: kick client from channel
*/
ReasonIdentifier[ReasonIdentifier["CHANNEL"] = 4] = "CHANNEL";
/**
* 5: kick client from server
*/
ReasonIdentifier[ReasonIdentifier["SERVER"] = 5] = "SERVER";
})(ReasonIdentifier || (ReasonIdentifier = {}));
export var PermissionGroupDatabaseTypes;
(function (PermissionGroupDatabaseTypes) {
/**
* 0: template group (used for new virtual servers)
*/
PermissionGroupDatabaseTypes[PermissionGroupDatabaseTypes["TEMPLATE"] = 0] = "TEMPLATE";
/**
* 1: regular group (used for regular clients)
*/
PermissionGroupDatabaseTypes[PermissionGroupDatabaseTypes["REGULAR"] = 1] = "REGULAR";
/**
* 2: global query group (used for ServerQuery clients)
*/
PermissionGroupDatabaseTypes[PermissionGroupDatabaseTypes["QUERY"] = 2] = "QUERY";
})(PermissionGroupDatabaseTypes || (PermissionGroupDatabaseTypes = {}));
export var PermissionGroupTypes;
(function (PermissionGroupTypes) {
/**
* 0: server group permission
*/
PermissionGroupTypes[PermissionGroupTypes["SERVER_GROUP"] = 0] = "SERVER_GROUP";
/**
* 1: client specific permission
*/
PermissionGroupTypes[PermissionGroupTypes["GLOBAL_CLIENT"] = 1] = "GLOBAL_CLIENT";
/**
* 2: channel specific permission
*/
PermissionGroupTypes[PermissionGroupTypes["CHANNEL"] = 2] = "CHANNEL";
/**
* 3: channel group permission
*/
PermissionGroupTypes[PermissionGroupTypes["CHANNEL_GROUP"] = 3] = "CHANNEL_GROUP";
/**
* 4: channel-client specific permission
*/
PermissionGroupTypes[PermissionGroupTypes["CHANNEL_CLIENT"] = 4] = "CHANNEL_CLIENT";
})(PermissionGroupTypes || (PermissionGroupTypes = {}));
export var TokenType;
(function (TokenType) {
/**
* 0: server group token (id1={groupID} id2=0)
*/
TokenType[TokenType["SERVER_GROUP"] = 0] = "SERVER_GROUP";
/**
* 1: channel group token (id1={groupID} id2={channelID})
*/
TokenType[TokenType["CHANNEL_GROUP"] = 1] = "CHANNEL_GROUP";
})(TokenType || (TokenType = {}));