import {AbstractCommandHandler, AbstractCommandHandlerBoss} from "tc-shared/connection/AbstractCommandHandler"; import { AbstractServerConnection, CommandOptionDefaults, CommandOptions, ConnectionStateListener, ServerCommand, voice } from "tc-shared/connection/ConnectionBase"; import {CommandResult} from "tc-shared/connection/ServerConnectionDeclaration"; import {tr} from "tc-shared/i18n/localize"; import {ConnectionHandler, ConnectionState, DisconnectReason} from "tc-shared/ConnectionHandler"; import {NativeServerConnection, ServerType, spawn_server_connection as spawn_native_server_connection, destroy_server_connection as destroy_native_server_connection} from "tc-native/connection"; import {ConnectionCommandHandler} from "tc-shared/connection/CommandHandler"; import {HandshakeHandler} from "tc-shared/connection/HandshakeHandler"; import {ServerAddress} from "tc-shared/ui/server"; import {TeaSpeakHandshakeHandler} from "tc-shared/profiles/identities/TeamSpeakIdentity"; import AbstractVoiceConnection = voice.AbstractVoiceConnection; import {VoiceConnection} from "./VoiceConnection"; class ErrorCommandHandler extends AbstractCommandHandler { private _handle: ServerConnection; constructor(handle: ServerConnection) { super(handle); this._handle = handle; } handle_command(command: ServerCommand): boolean { if(command.command === "error") { const return_listener: {[key: string]: (result: CommandResult) => any} = this._handle["_return_listener"]; const data = command.arguments[0]; let return_code : string = data["return_code"]; if(!return_code) { const listener = return_listener["last_command"] || return_listener["_clientinit"]; if(typeof(listener) === "function") { console.warn(tr("Received error without return code. Using last command (%o)"), listener); listener(new CommandResult(command.arguments)); delete return_listener["last_command"]; delete return_listener["_clientinit"]; } else { console.warn(tr("Received error without return code."), data); } return false; } if(return_listener[return_code]) { return_listener[return_code](new CommandResult(command.arguments)); } else { console.warn(tr("Error received for no handler! (%o)"), data); } return true; } else if(command.command == "initivexpand") { if(command.arguments[0]["teaspeak"] == true) { console.log("Using TeaSpeak identity type"); this._handle.handshake_handler().startHandshake(); } return true; } else if(command.command == "initivexpand2") { /* its TeamSpeak or TeaSpeak with experimental 3.1 and not up2date */ this._handle["_do_teamspeak"] = true; } else if(command.command == "initserver") { const return_listener: {[key: string]: (result: CommandResult) => any} = this._handle["_return_listener"]; if(typeof(return_listener["_clientinit"]) === "function") { return_listener["_clientinit"](new CommandResult([{id: 0, message: ""}])); delete return_listener["_clientinit"]; } } else if(command.command == "notifyconnectioninforequest") { this._handle.send_command("setconnectioninfo", { //TODO calculate connection_ping: 0.0000, connection_ping_deviation: 0.0, connection_packets_sent_speech: 0, connection_packets_sent_keepalive: 0, connection_packets_sent_control: 0, connection_bytes_sent_speech: 0, connection_bytes_sent_keepalive: 0, connection_bytes_sent_control: 0, connection_packets_received_speech: 0, connection_packets_received_keepalive: 0, connection_packets_received_control: 0, connection_bytes_received_speech: 0, connection_bytes_received_keepalive: 0, connection_bytes_received_control: 0, connection_server2client_packetloss_speech: 0.0000, connection_server2client_packetloss_keepalive: 0.0000, connection_server2client_packetloss_control: 0.0000, connection_server2client_packetloss_total: 0.0000, connection_bandwidth_sent_last_second_speech: 0, connection_bandwidth_sent_last_second_keepalive: 0, connection_bandwidth_sent_last_second_control: 0, connection_bandwidth_sent_last_minute_speech: 0, connection_bandwidth_sent_last_minute_keepalive: 0, connection_bandwidth_sent_last_minute_control: 0, connection_bandwidth_received_last_second_speech: 0, connection_bandwidth_received_last_second_keepalive: 0, connection_bandwidth_received_last_second_control: 0, connection_bandwidth_received_last_minute_speech: 0, connection_bandwidth_received_last_minute_keepalive: 0, connection_bandwidth_received_last_minute_control: 0 } ); } return false; } } export class ServerConnection extends AbstractServerConnection { private _native_handle: NativeServerConnection; private _voice_connection: VoiceConnection; private _do_teamspeak: boolean; private _return_listener: {[key: string]: (result: CommandResult) => any} = {}; private _command_handler: NativeConnectionCommandBoss; private _command_error_handler: ErrorCommandHandler; private _command_handler_default: ConnectionCommandHandler; private _remote_address: ServerAddress; private _handshake_handler: HandshakeHandler; private _return_code_index: number = 0; onconnectionstatechanged: ConnectionStateListener; constructor(props: ConnectionHandler) { super(props); this._command_handler = new NativeConnectionCommandBoss(this); this._command_error_handler = new ErrorCommandHandler(this); this._command_handler_default = new ConnectionCommandHandler(this); this._command_handler.register_handler(this._command_error_handler); this._command_handler.register_handler(this._command_handler_default); this._native_handle = spawn_native_server_connection(); this._native_handle.callback_disconnect = reason => { this.client.handleDisconnect(DisconnectReason.CONNECTION_CLOSED, { reason: reason, event: event }); }; this._native_handle.callback_command = (command, args, switches) => { console.log("Received: %o %o %o", command, args, switches); //FIXME catch error this._command_handler.invoke_handle({ command: command, arguments: args }); }; this._voice_connection = new VoiceConnection(this, this._native_handle._voice_connection); this.command_helper.initialize(); this._voice_connection.setup(); } native_handle() : NativeServerConnection { return this._native_handle; } finalize() { if(this._native_handle) destroy_native_server_connection(this._native_handle); this._native_handle = undefined; } connect(address: ServerAddress, handshake: HandshakeHandler, timeout?: number): Promise { this.updateConnectionState(ConnectionState.CONNECTING); this._remote_address = address; this._handshake_handler = handshake; this._do_teamspeak = false; handshake.setConnection(this); handshake.initialize(); return new Promise((resolve, reject) => { this._native_handle.connect({ remote_host: address.host, remote_port: address.port, timeout: typeof(timeout) === "number" ? timeout : -1, callback: error => { if(error != 0) { /* required to notify the handle, just a promise reject does not work */ this.client.handleDisconnect(DisconnectReason.CONNECT_FAILURE, error); this.updateConnectionState(ConnectionState.UNCONNECTED); reject(this._native_handle.error_message(error)); return; } else { resolve(); } this.updateConnectionState(ConnectionState.AUTHENTICATING); console.log("Remote server type: %o (%s)", this._native_handle.server_type, ServerType[this._native_handle.server_type]); if(this._native_handle.server_type == ServerType.TEAMSPEAK || this._do_teamspeak) { console.log("Trying to use TeamSpeak's identity system"); this.handshake_handler().on_teamspeak(); } }, identity_key: (handshake.get_identity_handler() as TeaSpeakHandshakeHandler).identity.private_key, teamspeak: false }) }); } remote_address(): ServerAddress { return this._remote_address; } handshake_handler(): HandshakeHandler { return this._handshake_handler; } connected(): boolean { return typeof(this._native_handle) !== "undefined" && this._native_handle.connected(); } disconnect(reason?: string): Promise { console.trace("Disconnect: %s",reason); return new Promise((resolve, reject) => this._native_handle.disconnect(reason || "", error => { if(error == 0) resolve(); else reject(this._native_handle.error_message(error)); })); } support_voice(): boolean { return true; } voice_connection(): AbstractVoiceConnection { return this._voice_connection; } command_handler_boss(): AbstractCommandHandlerBoss { return this._command_handler; } private generate_return_code() : string { return (this._return_code_index++).toString(); } send_command(command: string, data?: any, _options?: CommandOptions): Promise { if(!this.connected()) { console.warn(tr("Tried to send a command without a valid connection.")); return Promise.reject(tr("not connected")); } const options: CommandOptions = {}; Object.assign(options, CommandOptionDefaults); Object.assign(options, _options); data = $.isArray(data) ? data : [data || {}]; if(data.length == 0) /* we require min one arg to append return_code */ data.push({}); let return_code = data[0]["return_code"] !== undefined ? data[0].return_code : this.generate_return_code(); data[0]["return_code"] = return_code; console.log("Sending %s (%o)", command, data); const promise = new Promise((resolve, reject) => { const timeout_id = setTimeout(() => { delete this._return_listener[return_code]; reject("timeout"); }, 5000); this._return_listener[return_code] = result => { clearTimeout(timeout_id); delete this._return_listener[return_code]; (result.success ? resolve : reject)(result); }; if(command == "clientinit") this._return_listener["_clientinit"] = this._return_listener[return_code]; /* fix for TS3 (clientinit does not accept a return code) */ try { this._native_handle.send_command(command, data, options.flagset || []); } catch(error) { console.warn(tr("Failed to send command: %o"), error); } }); return this._command_handler_default.proxy_command_promise(promise, options); } ping(): { native: number; javascript?: number } { return { native: this._native_handle ? (this._native_handle.current_ping() / 1000) : -2 }; } } export class NativeConnectionCommandBoss extends AbstractCommandHandlerBoss { constructor(connection: AbstractServerConnection) { super(connection); } } /* override the "normal" connection */ export function spawn_server_connection(handle: ConnectionHandler) : AbstractServerConnection { console.log("Spawning native connection"); return new ServerConnection(handle); /* will be overridden by the client */ } export function destroy_server_connection(handle: AbstractServerConnection) { if(!(handle instanceof ServerConnection)) throw "invalid handle"; //TODO: Here! console.log("Call to destroy a server connection"); }