UNPKG

teamspeak-server-query

Version:

Lightweight teamspeak server query which prints the response as javascript object

407 lines (353 loc) 11.5 kB
'use strict' const events = require('events') const net = require('net') class Serverquery extends events { constructor(options = {logging: false}) { super() this.connected = false this.logging = options.logging // Shows the query commands in the console this.socket = null this.commandlist = [ 'banadd', 'banclient', 'bandel', 'bandelall', 'banlist', 'bindinglist', 'channeladdperm', 'channelclientaddperm', 'channelclientdelperm', 'channelclientpermlist', 'channelcreate', 'channeldelete', 'channeldelperm', 'channeledit', 'channelfind', 'channelgroupadd', 'channelgroupaddperm', 'channelgroupclientlist', 'channelgroupcopy', 'channelgroupdel', 'channelgroupdelperm', 'channelgrouplist', 'channelgrouppermlist', 'channelgrouprename', 'channelinfo', 'channellist', 'channelmove', 'channelpermlist', 'clientaddperm', 'clientdbdelete', 'clientdbedit', 'clientdbfind', 'clientdbinfo', 'clientdblist', 'clientdelperm', 'clientedit', 'clientfind', 'clientgetdbidfromuid', 'clientgetids', 'clientgetnamefromdbid', 'clientgetnamefromuid', 'clientgetuidfromclid', 'clientinfo', 'clientkick', 'clientlist', 'clientmove', 'clientpermlist', 'clientpoke', 'clientsetserverquerylogin', 'clientupdate', 'complainadd', 'complaindel', 'complaindelall', 'complainlist', 'custominfo', 'customsearch', 'customset', 'customdelete', 'ftcreatedir', 'ftdeletefile', 'ftgetfileinfo', 'ftgetfilelist', 'ftinitdownload', 'ftinitupload', 'ftlist', 'ftrenamefile', 'ftstop', 'gm', 'help', 'hostinfo', 'instanceedit', 'instanceinfo', 'logadd', 'login', 'logout', 'logview', 'messageadd', 'messagedel', 'messageget', 'messagelist', 'messageupdateflag', 'permfind', 'permget', 'permidgetbyname', 'permissionlist', 'permoverview', 'permreset', 'privilegekeyadd', 'privilegekeydelete', 'privilegekeylist', 'privilegekeyuse', 'quit', 'sendtextmessage', 'servercreate', 'serverdelete', 'serveredit', 'servergroupadd', 'servergroupaddclient', 'servergroupaddperm', 'servergroupautoaddperm', 'servergroupautodelperm', 'servergroupclientlist', 'servergroupcopy', 'servergroupdel', 'servergroupdelclient', 'servergroupdelperm', 'servergrouplist', 'servergrouppermlist', 'servergrouprename', 'servergroupsbyclientid', 'serveridgetbyport', 'serverinfo', 'serverlist', 'servernotifyregister', 'servernotifyunregister', 'serverprocessstop', 'serverrequestconnectioninfo', 'serversnapshotcreate', 'serversnapshotdeploy', 'serverstart', 'serverstop', 'servertemppasswordadd', 'servertemppassworddel', 'servertemppasswordlist', 'setclientchannelgroup', 'tokenadd', 'tokendelete', 'tokenlist', 'tokenuse', 'use', 'version', 'whoami' ] this.createProperties() } connect(params) { return new Promise((resolve, reject) => { this.socket = new net.Socket() this.connectionTimeout(reject, 4000) this.socket.setEncoding('utf8') this.socket.connect(params) this.socket.once('connect', () => this.checkTeamspeakVersion(resolve, reject)) this.socket.once('error', err => this.connectionError(err, reject)) this.socket.on('close', () => this.connectionClose()) }) } /** * Resolves an error if the first server response is not "TS3". * @param {Promise} resolve * @param {Promise} reject */ checkTeamspeakVersion(resolve, reject) { let checkVersion = data => { if(data.split('\n')[0].toLowerCase() === 'ts3') { this.connected = true this.notificationEvent() // Add the custome "notify" event resolve(data) } else { this.connected = false reject(new Error('Not a teamspeak 3 server')) } this.socket.removeListener('data', checkVersion) } this.socket.on('data', checkVersion) } /** * If an error occurs while connecting to a server * @param {object} err The default error object * @param {Promise} reject */ connectionError(err, reject) { this.connected = false this.socket.destroy() this.socket.unref() reject(err) } /** * When no connection gets estableshed after a specific time. * @param {Promise} reject Timeout error */ connectionTimeout(reject, milliseconds) { setTimeout(() => { if(this.socket.connecting) { this.connected = false this.socket.destroy() this.socket.unref() reject(new Error('Timeout')) } }, milliseconds) } /** * When the connection gets closed (either by the teamspeak server itself or because of an error) a "close" gets emitted. */ connectionClose() { this.connected = false this.emit('close') } /** * It adds all the available query commands ass methods to this object. * The created methods are parsed to a plain string and then send to the query. * E.g: this.servergroupclientlist({sgid: 1}) => "servergroupclientlist sgid=1" * @return {Promise} The returned data from the teamspeak server query as javascript object */ createProperties() { for(let command of this.commandlist) { Object.defineProperty(Serverquery.prototype, command, { value(props) { this.logging && console.log(`${command} ${this.convertToString(props)}`) return this.send(`${command} ${this.convertToString(props)}`).then(res => this.dataToObject(res)) }, writable: false, // This method can not be overwritten enumerable: true, // You can see this method on console.log configurable: true // Needs to be set otherwise a cannot redefine error will show }) } } /** * Sends the command to the teamspeak 3 serverquery and returns the response from the server. * Some responses are not getting send at once. So the data listener collects all responses until the last line is emitted by the server query. * @param {String} cmd Normal command as you would write directly over telnet in the server query * @return {Promise} Array with the reponse(s) from the server */ send(cmd) { return new Promise((resolve, reject) => { this.socket.once('error', err => reject(err)) let data = '' let errorMessage = /(error)(\s)(id=[0-9]{1,4})(\s)(msg=.*)/ // Last line is always an "error" response let collectResponses = response => { data += response for(let line of response.split('\n')) { line = line.trim() if(errorMessage.test(line)) { resolve(data.split('\n')[0]) // Needs to be called otherwise a MaxListenersExceededWarning will show up and the RAM usage is rising this.socket.removeListener('data', collectResponses) this.socket.removeAllListeners('error') } } } this.socket.write(`${cmd}\n`, 'utf8', () => { this.socket.on('data', collectResponses) }) }) } /** * When a servernotifyregister gets send to the server this method creates the event. * Then it checks if the data from the server begins with "notify" and then the event will be emitted. * Otherwise every reponse would be emitted as event. * @return {Event} Emits the event */ notificationEvent() { let notify = /^notify/ this.socket.on('data', data => { if(notify.test(data)) { this.emit('notify', this.dataToObject(data)) } }) } /** * Converts the raw response from the serverquery into a workable array with objects. * @param {String} raw The reponse from the serverquery * @return {Promise} Array with objects */ dataToObject(raw) { if(typeof raw !== 'string') { //throw new Error('The raw data has to be a string.') return [] } let data = raw let arr = [] // Only at the first "=" character the string gets split. Else e.g. the property client_unique_identifier would be wrong. // This is called "capturing parentheses" let regex = /=(.+)/ data.split('|').forEach(response => { let obj = {} response.split(' ').forEach(prop => { // If property is client_unique_identifier the string will not be escaped obj[prop.split(regex)[0]] = this.escapeChars(prop.split(regex)[1]) }) arr.push(obj) }) return arr } /** * Escapes backslashes, slashes and whitespace into normal characters. * I dont know exacly what happens here with the regular expression, I just added backslashes till it worked. * @param {String} string The channel- or clientname * @return {String} Escaped channel- or clientname */ escapeChars(string) { if(string == null) { return null } let escapedChar = string let chars = { backslash: ['\\\\\\\\', '\\'], slash: ['\\\\/', '\/'], whitespace: ['\\\\s', ' '], pipe: ['\\\\p', '|'] } for(let prop in chars) { escapedChar = escapedChar.replace(new RegExp(chars[prop][0], 'g'), chars[prop][1]) } return escapedChar } /** * The opposite of the method escapeChars() for the input values which are getting send by the forms. * @param {String} value The value of the a parameter * @return {String} The serialized value */ serializeInput(value) { // Empty values are null by default. To prevent errors null gets converted to an empty string. if(value === null) { return '' } let serializedChar = typeof value === 'number' ? value.toString() : value let chars = { backslash: {regex: /\\/g, string: '\\\\'}, whitespace: {regex: /\s/g, string: '\\s'}, slash: {regex: /\//g, string: '/'}, // Weird, thought you have to send // (double slash) pipe: {regex: /\|/, string: '\\p'} } for(let prop in chars) { serializedChar = serializedChar.replace(chars[prop].regex, chars[prop].string) } return serializedChar } /** * Converts the given object with the paramters into a plain string so the telnet query can read it * @param {Object} params This {sgid: 1, cldbid: 2} ... * @return {String} ... becomes this "sgid=1 cldbid=2" */ convertToString(params) { let string = '' if(typeof params === 'object' && Object.keys(params).length) { for(let [prop, value] of Object.entries(params)) { string += `${prop}=${this.serializeInput(value)} ` } } return string } } module.exports = Serverquery