UNPKG

node-ts

Version:

TeamSpeak® 3 Server Query client for node.js implemented using TypeScript

434 lines (431 loc) 15.9 kB
/** * @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 = {}));