teamspeak-server-query
Version:
Lightweight teamspeak server query which prints the response as javascript object
407 lines (353 loc) • 11.5 kB
JavaScript
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