ts3-nodejs-library
Version:
TeamSpeak Server Query API
286 lines (259 loc) • 8.96 kB
text/typescript
import { EventEmitter } from "events"
import { Command } from "./Command"
import { ProtocolRAW } from "./protocols/raw"
import { ProtocolSSH } from "./protocols/ssh"
import { ConnectionParams, QueryProtocol } from "../TeamSpeak"
import { QueryResponseTypes } from "../types/QueryResponse"
export class TeamSpeakQuery extends EventEmitter {
static IGNORE_LINES_INITIAL = 2
private config: ConnectionParams
private queue: Array<TeamSpeakQuery.QueueItem> = []
private active: TeamSpeakQuery.QueueItem|undefined
private ignoreLines: number = TeamSpeakQuery.IGNORE_LINES_INITIAL
private lastEvent: string = ""
private lastcmd: number = Date.now()
private connected: boolean = false
private keepAliveTimeout: any
private floodTimeout: NodeJS.Timeout
private socket: TeamSpeakQuery.QueryProtocolInterface
private pauseQueue: boolean = true
readonly doubleEvents: Array<string> = [
"notifyclientleftview",
"notifyclientmoved",
"notifycliententerview"
]
constructor(config: ConnectionParams) {
super()
this.config = config
}
/**
* start connecting to the teamspeak server
*/
connect() {
if (this.socket) {
if (this.connected) {
throw new Error("already connected")
} else {
/**
* socket has already been connected and there was an active item
* push it back into the queue for possible priorized elements
*/
if (this.active) {
this.queue.unshift(this.active)
this.active = undefined
}
this.socket.removeAllListeners()
this.ignoreLines = TeamSpeakQuery.IGNORE_LINES_INITIAL
}
}
this.socket = TeamSpeakQuery.getSocket(this.config)
this.socket.on("debug", data => this.emit("debug", data))
this.socket.on("connect", this.handleConnect.bind(this))
this.socket.on("line", this.handleLine.bind(this))
this.socket.on("error", this.handleError.bind(this))
}
/** returns a constructed Socket */
static getSocket(config: ConnectionParams): TeamSpeakQuery.QueryProtocolInterface {
if (config.protocol === QueryProtocol.RAW) {
return new ProtocolRAW(config)
} else if (config.protocol === QueryProtocol.SSH) {
return new ProtocolSSH(config)
} else {
throw new Error("Invalid Protocol given! Expected (\"raw\" or \"ssh\")")
}
}
/** sends a command to the TeamSpeak Server */
execute(command: string, ...args: TeamSpeakQuery.executeArgs[]): Promise<QueryResponseTypes[]> {
return this.handleCommand(command, args)
}
/** sends a priorized command to the TeamSpeak Server */
executePrio(command: string, ...args: TeamSpeakQuery.executeArgs[]): Promise<QueryResponseTypes[]> {
return this.handleCommand(command, args, true)
}
/**
* @param command command to send
* @param args arguments which gets parsed
* @param prio wether this command should be handled as priority and be queued before others
*/
private handleCommand(command: string, args: TeamSpeakQuery.executeArgs[], priority: boolean = false): Promise<QueryResponseTypes[]> {
return new Promise((fulfill, reject) => {
const cmd = new Command().setCommand(command)
Object.values(args).forEach(v => {
if (Array.isArray(v)) {
if (v.some(value => typeof value === "object" && value !== null)) {
return cmd.setMultiOptions((<Command.multiOpts>v).filter(n => n !== null))
} else {
return cmd.setFlags(<Command.flags>v)
}
} else if (typeof v === "function") {
return cmd.setParser(v)
} else {
return cmd.setOptions(v)
}
})
this.queueWorker({ cmd, fulfill, reject, priority })
})
}
/** forcefully closes the socket connection */
forceQuit() {
this.pause(true)
return this.socket.close()
}
pause(pause: boolean) {
this.pauseQueue = pause
if (!this.pauseQueue) this.queueWorker()
return this
}
/** gets called when the underlying transport layer connects to a server */
private handleConnect() {
this.connected = true
this.socket.on("close", this.handleClose.bind(this))
this.emit("connect")
}
/** handles a single line response from the teamspeak server */
private handleLine(line: string): void {
line = line.trim()
this.emit("debug", { type: "receive", data: line })
if (this.ignoreLines > 0 && !line.startsWith("error")) {
this.ignoreLines -= 1
if (this.ignoreLines > 0) return
this.emit("ready")
this.queueWorker()
} else if (line.startsWith("error")) {
this.handleQueryError(line)
} else if (line.startsWith("notify")) {
this.handleQueryEvent(line)
} else if (this.active && this.active.cmd) {
this.active.cmd.setResponse(line)
}
}
/** handles the error line which finnishes a command */
private handleQueryError(line: string): void {
if (!this.active) return
this.active.cmd.setError(line)
if (this.active.cmd.hasError()) {
const error = this.active.cmd.getError()!
if (error.id === 524) {
this.emit("flooding", this.active.cmd.getError())
const match = error.message.match(/(\d*) second/i)
const waitTimeout = match ? parseInt(match[1], 10) : 1000
clearTimeout(this.floodTimeout)
this.floodTimeout = setTimeout((cmd => (() => {
cmd.reset()
this.send(cmd.build())
}))(this.active.cmd), waitTimeout * 1000 + 100)
return
} else {
this.active.reject(this.active.cmd.getError())
}
} else {
this.active.fulfill(this.active.cmd.getResponse())
}
this.active = undefined
this.queueWorker()
}
/**
* Handles an event which has been received from the TeamSpeak Server
* @param line event response line from the teamspeak server
*/
private handleQueryEvent(line: string): void {
if (this.doubleEvents.some(s => line.includes(s)) && line === this.lastEvent) return
/**
* Query Event
* Gets fired when the Query receives an Event
* @event TeamSpeakQuery#<TeamSpeakEvent>
* @memberof TeamSpeakQuery
* @type {object}
*/
this.emit(
line.substr(6, line.indexOf(" ") - 6),
Command.parse({ raw: line.substr(line.indexOf(" ") + 1) })[0]
)
}
/**
* Emits an Error which the given arguments
* @param {...any} args arguments which gets passed to the error event
*/
private handleError(error: Error) {
/**
* Query Event
* Gets fired when the Socket had an Error
* @event TeamSpeakQuery#error
* @memberof TeamSpeakQuery
*/
this.emit("error", error)
}
/** handles socket closing */
private handleClose() {
this.connected = false
this.pause(true)
clearTimeout(this.floodTimeout)
clearTimeout(this.keepAliveTimeout)
const cmd = new Command().setError(this.socket.chunk || "")
this.emit("close", cmd.getError())
}
/** handles the timer for the keepalive request */
private keepAlive() {
if (!this.config.keepAlive) return
clearTimeout(this.keepAliveTimeout)
this.keepAliveTimeout = setTimeout(() => this.sendKeepAlive(), 250 * 1000 - (Date.now() - this.lastcmd))
}
/** dispatches the keepalive */
private sendKeepAlive() {
this.emit("debug", { type: "keepalive" })
this.lastcmd = Date.now()
this.socket.sendKeepAlive()
this.keepAlive()
}
/** executes the next command */
private queueWorker(cmd?: TeamSpeakQuery.QueueItem) {
if (cmd) this.queue.push(cmd)
if (!this.connected || this.active || this.pauseQueue) return
this.active = this.getNextQueueItem()
if (!this.active) return
this.send(this.active.cmd.build())
}
/**
* retrieves the next available queue item
* respects priorized queue
*/
private getNextQueueItem() {
let item = this.queue.find(i => i.priority)
if (item) {
this.queue = this.queue.filter(i => i !== item)
return item
}
return this.queue.shift()
}
/** sends data to the socket */
private send(data: string) {
this.lastcmd = Date.now()
this.emit("debug", { type: "send", data })
this.socket.send(data)
this.keepAlive()
}
isConnected() {
return this.connected
}
}
export namespace TeamSpeakQuery {
export type executeArgs = Command.ParserCallback|Command.multiOpts|Command.options|Command.flags
export interface QueueItem {
fulfill: Function
reject: Function
cmd: Command
priority: boolean
}
export interface QueryProtocolInterface extends EventEmitter {
readonly chunk: string
/** sends a keepalive to the TeamSpeak Server */
sendKeepAlive: () => void
/**
* sends the data in the first argument, appends a newline
* @param {string} str the data which should be sent
*/
send: (data: string) => void
/** forcefully closes the socket */
close: () => void
}
}