318 lines
13 KiB
TypeScript
318 lines
13 KiB
TypeScript
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(data));
|
|
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(data));
|
|
} 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"];
|
|
}
|
|
|
|
if(this._handle.onconnectionstatechanged)
|
|
this._handle.onconnectionstatechanged(ConnectionState.INITIALISING, ConnectionState.CONNECTING);
|
|
} 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<void> {
|
|
this._remote_address = address;
|
|
this._handshake_handler = handshake;
|
|
this._do_teamspeak = false;
|
|
handshake.setConnection(this);
|
|
handshake.initialize();
|
|
|
|
return new Promise<void>((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);
|
|
reject(this._native_handle.error_message(error));
|
|
return;
|
|
} else {
|
|
resolve();
|
|
}
|
|
|
|
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<void> {
|
|
console.trace("Disconnect: %s",reason);
|
|
return new Promise<void>((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<CommandResult> {
|
|
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<CommandResult>((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");
|
|
} |