teamspeak-async
Version:
Use Teamspeak ServerQuery from Node
208 lines (178 loc) • 5.9 kB
JavaScript
'use strict'
/**
* Teamspeak-async
* written by Nicholai Nissen
* MIT licensed
*/
import {EventEmitter} from 'events'
import net from 'net'
import debug from 'debug'
import byline from 'byline'
import Promise from 'bluebird'
import Request from './request'
import Queue from './queue'
export class TeamSpeakClient extends EventEmitter { // eslint-disable-line import/prefer-default-export
constructor({host, port, user, password, server, disableUse, disableRegister}) {
super(EventEmitter)
this.debug = debug('TeamSpeakClient')
this.debug('Trying to create new client with information: %o', {host, port, user, password})
if (!host) {
throw new Error('Cannot create instance without host')
}
this.host = host
this.port = port || 10011
this.user = user
this.password = password
this.server = server || 1
this.disableRegister = disableRegister
this.disableUse = disableUse
this.debug('Connecting to %s:%s', host, port)
this.socket = net.connect({host, port: this.port})
this._connection = {}
this._connection.promise = new Promise((resolve, reject) => {
this._connection.resolve = resolve
this._connection.reject = reject
})
this.socket.on('connect', reportConnection.bind(this))
this.socket.on('error', err => this.emit('error', err))
this.queue = new Queue()
this.lineCount = 0
this.processing = null
function reportConnection() {
this.debug('Socket connected')
this.reader = byline.createStream(this.socket, {encoding: 'utf-8', keepEmptyLines: false})
this.reader.on('data', this.readLine.bind(this))
selfAuth.apply(this)
}
async function selfAuth() {
try {
/* If username and password is supplied during creation, authenticate now */
if (this.user && this.password) {
await this.authenticate(this.user, this.password)
}
this.debug('Self-Authenticated')
if (!this.disableUse) {
await this.use(this.server)
this.debug('Self-Using server %d', this.server)
}
if (!this.disableRegister) {
await this.register('server')
this.debug('Self-registered for server')
}
this._connection.resolve()
} catch (err) {
console.error('Teamspeak Authentication error')
console.error(err)
}
}
/* End of constructor */
}
send(command, params) {
this.debug('Enqueuing c:: %s \np:: %o', command, params)
const request = new Request(command, params)
this.queue.enqueue(request)
this.parseQueue()
return request.promise
}
use(server) {
return this.send('use', server)
}
authenticate(username, password) {
return this.send('login', [username, password])
}
register(e) {
return this.send('servernotifyregister', {event: e})
}
poke(clid, msg) {
return this.send('clientpoke', {clid, msg})
}
listClients() {
return this.send('clientlist')
}
parseQueue() {
if (Boolean(this.processing) || this.queue.isEmpty()) {
this.debug(`Skipping parsing. \np::${Boolean(this.processing)} q::${this.queue.isEmpty()}`)
return null
}
this.processing = this.queue.dequeue()
this.debug('Writing request :: %s', this.processing.request)
this.socket.write(this.processing.request + '\n')
}
readLine(line) {
this.lineCount++
this.debug('#%d Receiving line :: %s', this.lineCount, line)
line = line.trim()
/* Skip greetings */
if (this.lineCount < 3) {
this.debug('Skipping handling of greeting #%d', this.lineCount)
return
}
if (line.indexOf('error') === 0) {
/* Request done, status message */
this.debug('#%d Line is status message', this.lineCount)
const response = parseResponse(line)
if (response.error && response.msg !== 'ok') {
this.debug('#%d Carries error %o', this.lineCount, response.error)
this.processing.done(response)
} else {
if (!this.processing._response) {
this.processing._response = response
}
this.processing.done()
}
this.debug('#%d Processing line done', this.lineCount)
/* Clear processing state regardless of error or success */
this.processing = null
/* Take another piece of the cake, if any left */
if (!this.queue.isEmpty()) {
this.debug('Restarting queue')
this.parseQueue()
}
} else if (line.indexOf('notify') === 0) {
/* Incoming server notification */
this.debug('#%d Line is notification', this.lineCount)
const notification = line.substr('notify'.length)
const type = notification.substr(notification, notification.indexOf(' '))
this.debug('#%d Emitting event type %s and *', this.lineCount, type)
const data = parseResponse(line)
/* Save notification type in event data */
data.type = type
this.emit(type, data)
this.emit('*', data)
} else if (this.processing) {
/* Actual response, not status message ,which in teamspeak for some reason goes localhost */
this.processing._response = parseResponse(line)
this.debug('#%d Line is response :: %s', this.lineCount, this.processing._response)
}
}
/* End of class */
}
/**
* Unescapes a string that has been escaped by Teamspeak.
* @param {String} string String returned by ServerQuery
* @return {String} Plain string
*/
function unEscape(string) {
return string.replace(/\\s/g, ' ').replace(/\\p/, '|').replace(/\\\\/g, '\\').replace(/\\\//g, '\/') // eslint-disable-line no-useless-escape
}
function parseResponse(data) {
let parsed = data.split('|').map(group => {
return group.split(' ').reduce((obj, val) => {
const [k, v] = val.split('=')
obj[k] = v ? unEscape(v) : ''
return obj
}, {})
})
if (parsed.length === 1) {
parsed = parsed[0]
}
/* Reclaim stripped equal-sign in UUID */
if (parsed.invokeruid && parsed.invokeruid.length === 27) {
parsed.invokeruid += '='
}
if (parsed.client_unique_identifier && parsed.client_unique_identifier.length === 27) {
parsed.client_unique_identifier += '=' // eslint-disable-line camelcase
}
/* */
return parsed
}