TeaWeb/shared/js/ConnectionHandler.ts

1075 lines
48 KiB
TypeScript
Raw Normal View History

2020-03-30 11:44:18 +00:00
import {ChannelTree} from "tc-shared/ui/view";
import {AbstractServerConnection} from "tc-shared/connection/ConnectionBase";
import {PermissionManager} from "tc-shared/permission/PermissionManager";
import {GroupManager} from "tc-shared/permission/GroupManager";
import {ServerSettings, Settings, settings, StaticSettings} from "tc-shared/settings";
2020-03-30 11:44:18 +00:00
import {Sound, SoundManager} from "tc-shared/sound/Sounds";
import {LocalClientEntry} from "tc-shared/ui/client";
import * as server_log from "tc-shared/ui/frames/server_log";
2020-03-30 11:44:18 +00:00
import {ConnectionProfile, default_profile, find_profile} from "tc-shared/profiles/ConnectionProfile";
import {ServerAddress} from "tc-shared/ui/server";
import * as log from "tc-shared/log";
import {LogCategory} from "tc-shared/log";
import {createErrorModal, createInfoModal, createInputModal, Modal} from "tc-shared/ui/elements/Modal";
import {hashPassword} from "tc-shared/utils/helpers";
import {HandshakeHandler} from "tc-shared/connection/HandshakeHandler";
import * as htmltags from "./ui/htmltags";
import {ChannelEntry} from "tc-shared/ui/channel";
import {InputStartResult, InputState} from "tc-shared/voice/RecorderBase";
import {CommandResult, ErrorID} from "tc-shared/connection/ServerConnectionDeclaration";
import {guid} from "tc-shared/crypto/uid";
import * as bipc from "./BrowserIPC";
import {FileManager, spawn_upload_transfer, UploadKey} from "tc-shared/FileManager";
import {RecorderProfile} from "tc-shared/voice/RecorderProfile";
import {Frame} from "tc-shared/ui/frames/chat_frame";
import {Hostbanner} from "tc-shared/ui/frames/hostbanner";
import {server_connections} from "tc-shared/ui/frames/connection_handlers";
import {connection_log, Regex} from "tc-shared/ui/modal/ModalConnect";
import {formatMessage} from "tc-shared/ui/frames/chat";
import {spawnAvatarUpload} from "tc-shared/ui/modal/ModalAvatar";
import * as connection from "tc-backend/connection";
import * as dns from "tc-backend/dns";
2020-04-06 14:29:40 +00:00
import * as top_menu from "tc-shared/ui/frames/MenuBar";
import {EventHandler, Registry} from "tc-shared/events";
import {ServerLog} from "tc-shared/ui/frames/server_log";
2020-03-30 11:44:18 +00:00
export enum DisconnectReason {
2019-04-04 19:47:52 +00:00
HANDLER_DESTROYED,
REQUESTED,
2019-04-15 13:33:51 +00:00
DNS_FAILED,
2019-04-04 19:47:52 +00:00
CONNECT_FAILURE,
CONNECTION_CLOSED,
CONNECTION_FATAL_ERROR,
CONNECTION_PING_TIMEOUT,
CLIENT_KICKED,
CLIENT_BANNED,
HANDSHAKE_FAILED,
HANDSHAKE_TEAMSPEAK_REQUIRED,
HANDSHAKE_BANNED,
2019-04-04 19:47:52 +00:00
SERVER_CLOSED,
SERVER_REQUIRES_PASSWORD,
2019-08-21 08:00:01 +00:00
SERVER_HOSTMESSAGE,
2019-04-15 13:33:51 +00:00
IDENTITY_TOO_LOW,
2019-04-04 19:47:52 +00:00
UNKNOWN
}
2020-03-30 11:44:18 +00:00
export enum ConnectionState {
UNCONNECTED, /* no connection is currenting running */
CONNECTING, /* we try to establish a connection to the target server */
INITIALISING, /* we're setting up the connection encryption */
AUTHENTICATING, /* we're authenticating ourself so we get a unique ID */
CONNECTED, /* we're connected to the server. Server init has been done, may not everything is initialized */
DISCONNECTING/* we're curently disconnecting from the server and awaiting disconnect acknowledge */
}
export namespace ConnectionState {
export function socket_connected(state: ConnectionState) {
switch (state) {
case ConnectionState.CONNECTED:
case ConnectionState.AUTHENTICATING:
//case ConnectionState.INITIALISING: /* its not yet possible to send any data */
return true;
default:
return false;
}
}
2019-04-04 19:47:52 +00:00
}
2020-03-30 11:44:18 +00:00
export enum ViewReasonId {
2019-04-04 19:47:52 +00:00
VREASON_USER_ACTION = 0,
VREASON_MOVED = 1,
VREASON_SYSTEM = 2,
VREASON_TIMEOUT = 3,
VREASON_CHANNEL_KICK = 4,
VREASON_SERVER_KICK = 5,
VREASON_BAN = 6,
VREASON_SERVER_STOPPED = 7,
VREASON_SERVER_LEFT = 8,
VREASON_CHANNEL_UPDATED = 9,
VREASON_EDITED = 10,
VREASON_SERVER_SHUTDOWN = 11
}
export interface LocalClientStatus {
2019-04-04 19:47:52 +00:00
input_hardware: boolean;
input_muted: boolean;
output_muted: boolean;
channel_codec_encoding_supported: boolean;
channel_codec_decoding_supported: boolean;
sound_playback_supported: boolean;
sound_record_supported;
away: boolean | string;
channel_subscribe_all: boolean;
queries_visible: boolean;
}
2020-03-30 11:44:18 +00:00
export interface ConnectParameters {
nickname?: string;
channel?: {
target: string | number;
password?: string;
};
token?: string;
password?: {password: string, hashed: boolean};
2019-10-19 15:13:40 +00:00
auto_reconnect_attempt?: boolean;
}
2020-03-30 11:44:18 +00:00
declare const native_client;
export class ConnectionHandler {
private readonly event_registry: Registry<ConnectionEvents>;
2019-04-04 19:47:52 +00:00
channelTree: ChannelTree;
2020-03-30 11:44:18 +00:00
serverConnection: AbstractServerConnection;
2019-04-04 19:47:52 +00:00
fileManager: FileManager;
permissions: PermissionManager;
groups: GroupManager;
2020-03-30 11:44:18 +00:00
side_bar: Frame;
2019-04-04 19:47:52 +00:00
settings: ServerSettings;
2020-03-30 11:44:18 +00:00
sound: SoundManager;
2019-04-04 19:47:52 +00:00
2019-08-21 08:00:01 +00:00
hostbanner: Hostbanner;
tag_connection_handler: JQuery;
2019-04-04 19:47:52 +00:00
private _clientId: number = 0;
private _local_client: LocalClientEntry;
2019-10-19 15:13:40 +00:00
2019-04-04 19:47:52 +00:00
private _reconnect_timer: NodeJS.Timer;
private _reconnect_attempt: boolean = false;
2019-10-19 15:13:40 +00:00
2019-04-15 13:33:51 +00:00
private _connect_initialize_id: number = 1;
2019-04-04 19:47:52 +00:00
private client_status: LocalClientStatus = {
2019-04-04 19:47:52 +00:00
input_hardware: false,
input_muted: false,
output_muted: false,
away: false,
channel_subscribe_all: true,
queries_visible: false,
sound_playback_supported: undefined,
sound_record_supported: undefined,
channel_codec_encoding_supported: undefined,
channel_codec_decoding_supported: undefined
};
invoke_resized_on_activate: boolean = false;
2020-03-30 11:44:18 +00:00
log: ServerLog;
2019-04-04 19:47:52 +00:00
constructor() {
this.event_registry = new Registry<ConnectionEvents>();
this.event_registry.enable_debug("connection-handler");
2019-04-04 19:47:52 +00:00
this.settings = new ServerSettings();
2020-03-30 11:44:18 +00:00
this.log = new ServerLog(this);
2019-04-04 19:47:52 +00:00
this.channelTree = new ChannelTree(this);
2020-03-30 11:44:18 +00:00
this.side_bar = new Frame(this);
this.sound = new SoundManager(this);
2019-08-21 08:00:01 +00:00
this.hostbanner = new Hostbanner(this);
2019-04-04 19:47:52 +00:00
2019-04-15 13:33:51 +00:00
this.serverConnection = connection.spawn_server_connection(this);
2019-04-04 19:47:52 +00:00
this.serverConnection.onconnectionstatechanged = this.on_connection_state_changed.bind(this);
this.fileManager = new FileManager(this);
this.permissions = new PermissionManager(this);
2019-08-21 08:00:01 +00:00
this.side_bar.channel_conversations().initialize_needed_listener();
2019-04-04 19:47:52 +00:00
this.groups = new GroupManager(this);
this._local_client = new LocalClientEntry(this);
2019-08-21 08:00:01 +00:00
/* initialize connection handler tab entry */
{
this.tag_connection_handler = $.spawn("div").addClass("connection-container");
$.spawn("div").addClass("server-icon icon client-server_green").appendTo(this.tag_connection_handler);
$.spawn("div").addClass("server-name").appendTo(this.tag_connection_handler);
$.spawn("div").addClass("button-close icon client-tab_close_button").appendTo(this.tag_connection_handler);
this.tag_connection_handler.on('click', event => {
if(event.isDefaultPrevented())
return;
server_connections.set_active_connection(this);
2019-08-21 08:00:01 +00:00
});
this.tag_connection_handler.find(".button-close").on('click', event => {
server_connections.destroy_server_connection(this);
2019-08-21 08:00:01 +00:00
event.preventDefault();
});
this.tab_set_name(tr("Not connected"));
}
this.event_registry.register_handler(this);
}
initialize_client_state(source?: ConnectionHandler) {
this.client_status.input_muted = source ? source.client_status.input_muted : settings.global(Settings.KEY_CLIENT_STATE_MICROPHONE_MUTED);
this.client_status.output_muted = source ? source.client_status.output_muted : settings.global(Settings.KEY_CLIENT_STATE_SPEAKER_MUTED);
this.update_voice_status();
this.setSubscribeToAllChannels(source ? source.client_status.channel_subscribe_all : settings.global(Settings.KEY_CLIENT_STATE_SUBSCRIBE_ALL_CHANNELS));
2020-04-09 21:23:34 +00:00
this.setAway_(source ? source.client_status.away : (settings.global(Settings.KEY_CLIENT_STATE_AWAY) ? settings.global(Settings.KEY_CLIENT_AWAY_MESSAGE) : false), false);
this.setQueriesShown(source ? source.client_status.queries_visible : settings.global(Settings.KEY_CLIENT_STATE_QUERY_SHOWN));
}
events() : Registry<ConnectionEvents> {
return this.event_registry;
2019-08-21 08:00:01 +00:00
}
tab_set_name(name: string) {
this.tag_connection_handler.toggleClass('cutoff-name', name.length > 30);
this.tag_connection_handler.find(".server-name").text(name);
2019-04-04 19:47:52 +00:00
}
2020-03-30 11:44:18 +00:00
async startConnection(addr: string, profile: ConnectionProfile, user_action: boolean, parameters: ConnectParameters) {
2019-08-21 08:00:01 +00:00
this.tab_set_name(tr("Connecting"));
this.cancel_reconnect(false);
2019-10-19 15:13:40 +00:00
this._reconnect_attempt = parameters.auto_reconnect_attempt || false;
2019-04-04 19:47:52 +00:00
if(this.serverConnection)
this.handleDisconnect(DisconnectReason.REQUESTED);
let server_address: ServerAddress = {
host: "",
port: -1
};
{
2019-08-21 08:00:01 +00:00
let _v6_end = addr.indexOf(']');
let idx = addr.lastIndexOf(':');
2019-08-21 08:00:01 +00:00
if(idx != -1 && idx > _v6_end) {
server_address.port = parseInt(addr.substr(idx + 1));
server_address.host = addr.substr(0, idx);
2019-04-15 13:33:51 +00:00
} else {
server_address.host = addr;
server_address.port = 9987;
2019-04-15 13:33:51 +00:00
}
}
2019-08-30 21:06:39 +00:00
log.info(LogCategory.CLIENT, tr("Start connection to %s:%d"), server_address.host, server_address.port);
2020-03-30 11:44:18 +00:00
this.log.log(server_log.Type.CONNECTION_BEGIN, {
address: {
server_hostname: server_address.host,
server_port: server_address.port
},
client_nickname: parameters.nickname
});
this.channelTree.initialiseHead(addr, server_address);
if(parameters.password && !parameters.password.hashed){
try {
2020-03-30 11:44:18 +00:00
const password = await hashPassword(parameters.password.password);
parameters.password = {
hashed: true,
password: password
}
} catch(error) {
2019-08-30 21:06:39 +00:00
log.error(LogCategory.CLIENT, tr("Failed to hash connect password: %o"), error);
createErrorModal(tr("Error while hashing password"), tr("Failed to hash server password!<br>") + error).open();
}
}
2019-08-21 08:00:01 +00:00
if(parameters.password) {
connection_log.update_address_password({
hostname: server_address.host,
port: server_address.port
}, parameters.password.password);
}
2019-04-15 13:33:51 +00:00
2019-08-21 08:00:01 +00:00
const original_address = {host: server_address.host, port: server_address.port};
2020-03-30 11:44:18 +00:00
if(dns.supported() && !server_address.host.match(Regex.IP_V4) && !server_address.host.match(Regex.IP_V6)) {
2019-04-15 13:33:51 +00:00
const id = ++this._connect_initialize_id;
2020-03-30 11:44:18 +00:00
this.log.log(server_log.Type.CONNECTION_HOSTNAME_RESOLVE, {});
try {
const resolved = await dns.resolve_address(server_address, { timeout: 5000 }) || {} as any;
2019-04-15 13:33:51 +00:00
if(id != this._connect_initialize_id)
return; /* we're old */
server_address.host = typeof(resolved.target_ip) === "string" ? resolved.target_ip : server_address.host;
server_address.port = typeof(resolved.target_port) === "number" ? resolved.target_port : server_address.port;
2020-03-30 11:44:18 +00:00
this.log.log(server_log.Type.CONNECTION_HOSTNAME_RESOLVED, {
address: {
server_port: server_address.port,
server_hostname: server_address.host
}
});
} catch(error) {
2019-04-15 13:33:51 +00:00
if(id != this._connect_initialize_id)
return; /* we're old */
this.handleDisconnect(DisconnectReason.DNS_FAILED, error);
}
2019-04-04 19:47:52 +00:00
}
2020-03-30 11:44:18 +00:00
await this.serverConnection.connect(server_address, new HandshakeHandler(profile, parameters));
2019-08-21 08:00:01 +00:00
setTimeout(() => {
const connected = this.serverConnection.connected();
if(user_action && connected) {
connection_log.log_connect({
hostname: original_address.host,
port: original_address.port
});
}
}, 50);
2019-04-04 19:47:52 +00:00
}
async disconnectFromServer(reason?: string) {
this.cancel_reconnect(true);
this.handleDisconnect(DisconnectReason.REQUESTED); //TODO message?
try {
await this.serverConnection.disconnect();
} catch (error) {
log.warn(LogCategory.CLIENT, tr("Failed to successfully disconnect from server: {}"), error);
}
this.sound.play(Sound.CONNECTION_DISCONNECTED);
this.log.log(server_log.Type.DISCONNECTED, {});
}
2019-04-04 19:47:52 +00:00
getClient() : LocalClientEntry { return this._local_client; }
getClientId() { return this._clientId; }
set clientId(id: number) {
this._clientId = id;
this._local_client["_clientId"] = id;
}
get clientId() {
return this._clientId;
}
2020-03-30 11:44:18 +00:00
getServerConnection() : AbstractServerConnection { return this.serverConnection; }
2019-04-04 19:47:52 +00:00
@EventHandler<ConnectionEvents>("notify_connection_state_changed")
private handleConnectionConnected(event: ConnectionEvents["notify_connection_state_changed"]) {
if(event.new_state !== ConnectionState.CONNECTED) return;
2019-08-30 21:06:39 +00:00
log.info(LogCategory.CLIENT, tr("Client connected"));
this.log.log(server_log.Type.CONNECTION_CONNECTED, {
own_client: this.getClient().log_data()
});
this.sound.play(Sound.CONNECTION_CONNECTED);
2019-04-04 19:47:52 +00:00
this.permissions.requestPermissionList();
if(this.groups.serverGroups.length == 0)
this.groups.requestGroups();
this.settings.setServer(this.channelTree.server.properties.virtualserver_unique_identifier);
2019-04-04 19:47:52 +00:00
/* apply the server settings */
if(this.client_status.channel_subscribe_all)
this.channelTree.subscribe_all_channels();
else
this.channelTree.unsubscribe_all_channels();
this.channelTree.toggle_server_queries(this.client_status.queries_visible);
this.sync_status_with_server();
this.channelTree.server.updateProperties();
2019-04-04 19:47:52 +00:00
/*
No need to update the voice stuff because as soon we see ourself we're doing it
this.update_voice_status();
if(control_bar.current_connection_handler() === this)
control_bar.apply_server_voice_state();
*/
}
get connected() : boolean {
return this.serverConnection && this.serverConnection.connected();
}
private generate_ssl_certificate_accept() : JQuery {
const properties = {
connect_default: true,
2019-04-15 13:33:51 +00:00
connect_profile: this.serverConnection.handshake_handler().profile.id,
connect_address: this.serverConnection.remote_address().host + (this.serverConnection.remote_address().port !== 9987 ? ":" + this.serverConnection.remote_address().port : "")
2019-04-04 19:47:52 +00:00
};
2019-09-01 15:24:06 +00:00
const build_url = (base: string, search: string, props: any) => {
2019-04-04 19:47:52 +00:00
const parameters: string[] = [];
for(const key of Object.keys(props))
parameters.push(key + "=" + encodeURIComponent(props[key]));
2019-09-01 15:24:06 +00:00
let callback = base + search; /* don't use document.URL because it may contains a #! */
if(!search)
2019-04-04 19:47:52 +00:00
callback += "?" + parameters.join("&");
else
callback += "&" + parameters.join("&");
2019-04-15 13:33:51 +00:00
return "https://" + this.serverConnection.remote_address().host + ":" + this.serverConnection.remote_address().port + "/?forward_url=" + encodeURIComponent(callback);
2019-04-04 19:47:52 +00:00
};
/* generate the tag */
const tag = $.spawn("a").text(tr("here"));
let pathname = document.location.pathname;
if(pathname.endsWith(".php"))
pathname = pathname.substring(0, pathname.lastIndexOf("/"));
/* certaccept is currently not working! */
if(bipc.supported() && false) {
2019-04-04 19:47:52 +00:00
tag.attr('href', "#");
let popup: Window;
tag.on('click', event => {
const features = {
status: "no",
location: "no",
toolbar: "no",
menubar: "no",
width: 600,
height: 400
};
if(popup)
popup.close();
properties["certificate_callback"] = bipc.get_handler().register_certificate_accept_callback(() => {
log.info(LogCategory.GENERAL, tr("Received notification that the certificate has been accepted! Attempting reconnect!"));
if(this._certificate_modal)
this._certificate_modal.close();
popup.close(); /* no need, but nicer */
2020-03-30 11:44:18 +00:00
const profile = find_profile(properties.connect_profile) || default_profile();
const cprops = this.reconnect_properties(profile);
2019-08-21 08:00:01 +00:00
this.startConnection(properties.connect_address, profile, true, cprops);
2019-04-04 19:47:52 +00:00
});
const url = build_url(document.location.origin + pathname + "/popup/certaccept/", "", properties);
2019-04-04 19:47:52 +00:00
const features_string = [...Object.keys(features)].map(e => e + "=" + features[e]).reduce((a, b) => a + "," + b);
popup = window.open(url, "TeaWeb certificate accept", features_string);
try {
popup.focus();
} catch(e) {
log.warn(LogCategory.GENERAL, tr("Certificate accept popup has been blocked. Trying a blank page and replacing href"));
window.open(url, "TeaWeb certificate accept"); /* trying without features */
tag.attr("target", "_blank");
tag.attr("href", url);
tag.unbind('click');
}
});
} else {
tag.attr('href', build_url(document.location.origin + pathname, document.location.search, properties));
2019-04-04 19:47:52 +00:00
}
return tag;
}
private _certificate_modal: Modal;
handleDisconnect(type: DisconnectReason, data: any = {}) {
2019-04-15 13:33:51 +00:00
this._connect_initialize_id++;
2019-08-21 08:00:01 +00:00
this.tab_set_name(tr("Not connected"));
2019-04-04 19:47:52 +00:00
let auto_reconnect = false;
switch (type) {
case DisconnectReason.REQUESTED:
2019-08-21 08:00:01 +00:00
case DisconnectReason.SERVER_HOSTMESSAGE: /* already handled */
2019-04-04 19:47:52 +00:00
break;
case DisconnectReason.HANDLER_DESTROYED:
2019-09-12 21:59:35 +00:00
if(data) {
2019-04-04 19:47:52 +00:00
this.sound.play(Sound.CONNECTION_DISCONNECTED);
2020-03-30 11:44:18 +00:00
this.log.log(server_log.Type.DISCONNECTED, {});
2019-09-12 21:59:35 +00:00
}
2019-04-04 19:47:52 +00:00
break;
2019-04-15 13:33:51 +00:00
case DisconnectReason.DNS_FAILED:
2019-08-30 21:06:39 +00:00
log.error(LogCategory.CLIENT, tr("Failed to resolve hostname: %o"), data);
2020-03-30 11:44:18 +00:00
this.log.log(server_log.Type.CONNECTION_HOSTNAME_RESOLVE_ERROR, {
message: data as any
});
2019-04-15 13:33:51 +00:00
this.sound.play(Sound.CONNECTION_REFUSED);
break;
2019-04-04 19:47:52 +00:00
case DisconnectReason.CONNECT_FAILURE:
if(this._reconnect_attempt) {
auto_reconnect = true;
2020-03-30 11:44:18 +00:00
this.log.log(server_log.Type.CONNECTION_FAILED, {});
2019-04-04 19:47:52 +00:00
break;
}
2019-11-23 22:41:51 +00:00
if(data)
log.error(LogCategory.CLIENT, tr("Could not connect to remote host! Extra data: %o"), data);
else
log.error(LogCategory.CLIENT, tr("Could not connect to remote host!"), data);
2019-04-04 19:47:52 +00:00
if(native_client) {
createErrorModal(
tr("Could not connect"),
tr("Could not connect to remote host (Connection refused)")
).open();
} else {
const error_message_format =
"Could not connect to remote host (Connection refused)\n" +
"If you're sure that the remote host is up, than you may not allow unsigned certificates.\n" +
"Click {0} to accept the remote certificate";
this._certificate_modal = createErrorModal(
tr("Could not connect"),
2020-03-31 13:19:53 +00:00
formatMessage(/* @tr-ignore */ tr(error_message_format), this.generate_ssl_certificate_accept())
2019-04-04 19:47:52 +00:00
);
this._certificate_modal.close_listener.push(() => this._certificate_modal = undefined);
this._certificate_modal.open();
}
this.sound.play(Sound.CONNECTION_REFUSED);
break;
case DisconnectReason.HANDSHAKE_FAILED:
//TODO sound
2019-08-30 21:06:39 +00:00
log.error(LogCategory.CLIENT, tr("Failed to process handshake: %o"), data);
2019-04-04 19:47:52 +00:00
createErrorModal(
tr("Could not connect"),
tr("Failed to process handshake: ") + data as string
).open();
break;
case DisconnectReason.HANDSHAKE_TEAMSPEAK_REQUIRED:
createErrorModal(
tr("Target server is a TeamSpeak server"),
2020-03-30 11:44:18 +00:00
formatMessage(tr("The target server is a TeamSpeak 3 server!{:br:}Only TeamSpeak 3 based identities are able to connect.{:br:}Please select another profile or change the identify type."))
).open();
this.sound.play(Sound.CONNECTION_DISCONNECTED);
auto_reconnect = false;
break;
2019-04-15 13:33:51 +00:00
case DisconnectReason.IDENTITY_TOO_LOW:
createErrorModal(
tr("Identity level is too low"),
2020-03-30 11:44:18 +00:00
formatMessage(tr("You've been disconnected, because your Identity level is too low.{:br:}You need at least a level of {0}"), data["extra_message"])
2019-04-15 13:33:51 +00:00
).open();
this.sound.play(Sound.CONNECTION_DISCONNECTED);
auto_reconnect = false;
break;
2019-04-04 19:47:52 +00:00
case DisconnectReason.CONNECTION_CLOSED:
2019-08-30 21:06:39 +00:00
log.error(LogCategory.CLIENT, tr("Lost connection to remote server!"));
2019-10-19 15:13:40 +00:00
if(!this._reconnect_attempt) {
createErrorModal(
tr("Connection closed"),
tr("The connection was closed by remote host")
).open();
}
2019-04-04 19:47:52 +00:00
this.sound.play(Sound.CONNECTION_DISCONNECTED);
auto_reconnect = true;
break;
case DisconnectReason.CONNECTION_PING_TIMEOUT:
2019-08-30 21:06:39 +00:00
log.error(LogCategory.CLIENT, tr("Connection ping timeout"));
2019-04-04 19:47:52 +00:00
this.sound.play(Sound.CONNECTION_DISCONNECTED_TIMEOUT);
createErrorModal(
tr("Connection lost"),
tr("Lost connection to remote host (Ping timeout)<br>Even possible?")
).open();
break;
case DisconnectReason.SERVER_CLOSED:
2020-03-30 11:44:18 +00:00
this.log.log(server_log.Type.SERVER_CLOSED, {message: data.reasonmsg});
2019-10-19 15:13:40 +00:00
2019-04-04 19:47:52 +00:00
createErrorModal(
tr("Server closed"),
"The server is closed.<br>" + //TODO tr
"Reason: " + data.reasonmsg
).open();
this.sound.play(Sound.CONNECTION_DISCONNECTED);
auto_reconnect = true;
break;
case DisconnectReason.SERVER_REQUIRES_PASSWORD:
2020-03-30 11:44:18 +00:00
this.log.log(server_log.Type.SERVER_REQUIRES_PASSWORD, {});
2019-08-21 08:00:01 +00:00
2019-04-04 19:47:52 +00:00
createInputModal(tr("Server password"), tr("Enter server password:"), password => password.length != 0, password => {
if(!(typeof password === "string")) return;
2019-08-21 08:00:01 +00:00
const profile = this.serverConnection.handshake_handler().profile;
const cprops = this.reconnect_properties(profile);
cprops.password = {password: password as string, hashed: false};
2019-08-21 08:00:01 +00:00
connection_log.update_address_info({
port: this.channelTree.server.remote_address.port,
hostname: this.channelTree.server.remote_address.host
}, {
flag_password: true
} as any);
this.startConnection(this.channelTree.server.remote_address.host + ":" + this.channelTree.server.remote_address.port, profile, false, cprops);
2019-04-04 19:47:52 +00:00
}).open();
break;
case DisconnectReason.CLIENT_KICKED:
2019-10-24 16:37:12 +00:00
const have_invoker = typeof(data["invokerid"]) !== "undefined" && parseInt(data["invokerid"]) !== 0;
const modal = createErrorModal(
2019-10-19 15:13:40 +00:00
tr("You've been kicked"),
2020-03-30 11:44:18 +00:00
formatMessage(
2019-10-24 16:37:12 +00:00
have_invoker ? tr("You've been kicked from the server by {0}:{:br:}{1}") : tr("You've been kicked from the server:{:br:}{1}"),
have_invoker ?
htmltags.generate_client_object({ client_id: parseInt(data["invokerid"]), client_unique_id: data["invokeruid"], client_name: data["invokername"]}) :
2019-10-24 16:22:26 +00:00
"",
2019-10-24 16:37:12 +00:00
data["extra_message"] || data["reasonmsg"] || ""
2019-10-24 16:22:26 +00:00
)
2019-10-24 16:37:12 +00:00
);
modal.htmlTag.find(".modal-body").addClass("modal-disconnect-kick");
modal.open();
this.sound.play(Sound.SERVER_KICKED);
auto_reconnect = false;
break;
case DisconnectReason.HANDSHAKE_BANNED:
//Reason message already printed because of the command error handling
this.sound.play(Sound.CONNECTION_BANNED);
2019-04-04 19:47:52 +00:00
break;
case DisconnectReason.CLIENT_BANNED:
2020-03-30 11:44:18 +00:00
this.log.log(server_log.Type.SERVER_BANNED, {
2019-08-21 08:00:01 +00:00
invoker: {
client_name: data["invokername"],
client_id: parseInt(data["invokerid"]),
client_unique_id: data["invokeruid"]
},
message: data["reasonmsg"],
time: parseInt(data["time"])
});
this.sound.play(Sound.CONNECTION_BANNED);
2019-04-04 19:47:52 +00:00
break;
default:
2019-08-30 21:06:39 +00:00
log.error(LogCategory.CLIENT, tr("Got uncaught disconnect!"));
log.error(LogCategory.CLIENT, tr("Type: %o Data: %o"), type, data);
2019-04-04 19:47:52 +00:00
break;
}
2019-08-21 08:00:01 +00:00
this.channelTree.unregisterClient(this._local_client); /* if we dont unregister our client here the client will be destroyed */
2019-04-04 19:47:52 +00:00
this.channelTree.reset();
if(this.serverConnection)
this.serverConnection.disconnect();
2019-08-21 08:00:01 +00:00
this.side_bar.private_conversations().clear_client_ids();
this.hostbanner.update();
2019-04-04 19:47:52 +00:00
if(auto_reconnect) {
if(!this.serverConnection) {
2019-08-30 21:06:39 +00:00
log.info(LogCategory.NETWORKING, tr("Allowed to auto reconnect but cant reconnect because we dont have any information left..."));
2019-04-04 19:47:52 +00:00
return;
}
2020-03-30 11:44:18 +00:00
this.log.log(server_log.Type.RECONNECT_SCHEDULED, {timeout: 5000});
2019-04-04 19:47:52 +00:00
2019-08-30 21:06:39 +00:00
log.info(LogCategory.NETWORKING, tr("Allowed to auto reconnect. Reconnecting in 5000ms"));
2019-04-15 13:33:51 +00:00
const server_address = this.serverConnection.remote_address();
const profile = this.serverConnection.handshake_handler().profile;
2019-04-04 19:47:52 +00:00
this._reconnect_timer = setTimeout(() => {
this._reconnect_timer = undefined;
2020-03-30 11:44:18 +00:00
this.log.log(server_log.Type.RECONNECT_EXECUTE, {});
log.info(LogCategory.NETWORKING, tr("Reconnecting..."));
2019-10-19 15:13:40 +00:00
this.startConnection(server_address.host + ":" + server_address.port, profile, false, Object.assign(this.reconnect_properties(profile), {auto_reconnect_attempt: true}));
2019-04-04 19:47:52 +00:00
}, 5000);
}
}
2019-08-21 08:00:01 +00:00
cancel_reconnect(log_event: boolean) {
2019-04-04 19:47:52 +00:00
if(this._reconnect_timer) {
2020-03-30 11:44:18 +00:00
if(log_event) this.log.log(server_log.Type.RECONNECT_CANCELED, {});
2019-04-04 19:47:52 +00:00
clearTimeout(this._reconnect_timer);
this._reconnect_timer = undefined;
}
}
private on_connection_state_changed(old_state: ConnectionState, new_state: ConnectionState) {
this.event_registry.fire("notify_connection_state_changed", {
old_state: old_state,
new_state: new_state
});
2019-04-04 19:47:52 +00:00
}
2020-01-30 23:54:00 +00:00
private _last_record_error_popup: number;
2019-04-04 19:47:52 +00:00
update_voice_status(targetChannel?: ChannelEntry) {
//TODO: Simplify this
2019-08-21 08:00:01 +00:00
if(!this._local_client) return; /* we've been destroyed */
2019-04-04 19:47:52 +00:00
targetChannel = targetChannel || this.getClient().currentChannel();
const vconnection = this.serverConnection.voice_connection();
2019-10-13 19:33:07 +00:00
const basic_voice_support = this.serverConnection.support_voice() && vconnection.connected() && targetChannel;
2019-04-04 19:47:52 +00:00
const support_record = basic_voice_support && (!targetChannel || vconnection.encoding_supported(targetChannel.properties.channel_codec));
const support_playback = basic_voice_support && (!targetChannel || vconnection.decoding_supported(targetChannel.properties.channel_codec));
const property_update = {
client_input_muted: this.client_status.input_muted,
client_output_muted: this.client_status.output_muted
};
2019-09-12 21:59:35 +00:00
if(support_record && basic_voice_support)
vconnection.set_encoder_codec(targetChannel.properties.channel_codec);
if(!this.serverConnection.support_voice() || !this.serverConnection.connected() || !vconnection.connected()) {
2019-04-04 19:47:52 +00:00
property_update["client_input_hardware"] = false;
property_update["client_output_hardware"] = false;
this.client_status.input_hardware = true; /* IDK if we have input hardware or not, but it dosn't matter at all so */
/* no icons are shown so no update at all */
2019-04-04 19:47:52 +00:00
} else {
const audio_source = vconnection.voice_recorder();
const recording_supported = typeof(audio_source) !== "undefined" && audio_source.record_supported && (!targetChannel || vconnection.encoding_supported(targetChannel.properties.channel_codec));
2019-04-04 19:47:52 +00:00
const playback_supported = !targetChannel || vconnection.decoding_supported(targetChannel.properties.channel_codec);
property_update["client_input_hardware"] = recording_supported;
property_update["client_output_hardware"] = playback_supported;
this.client_status.input_hardware = recording_supported;
/* update icons */
2019-04-04 19:47:52 +00:00
const client_properties = this.getClient().properties;
for(const key of Object.keys(property_update)) {
if(client_properties[key] === property_update[key])
delete property_update[key];
}
if(Object.keys(property_update).length > 0) {
this.serverConnection.send_command("clientupdate", property_update).catch(error => {
log.warn(LogCategory.GENERAL, tr("Failed to update client audio hardware properties. Error: %o"), error);
2020-03-30 11:44:18 +00:00
this.log.log(server_log.Type.ERROR_CUSTOM, {message: tr("Failed to update audio hardware properties.")});
2019-04-04 19:47:52 +00:00
/* Update these properties anyways (for case the server fails to handle the command) */
const updates = [];
for(const key of Object.keys(property_update))
updates.push({key: key, value: (property_update[key]) + ""});
this.getClient().updateVariables(...updates);
});
}
}
2019-04-04 19:47:52 +00:00
if(targetChannel && basic_voice_support) {
2019-04-15 13:33:51 +00:00
const encoding_supported = vconnection && vconnection.encoding_supported(targetChannel.properties.channel_codec);
const decoding_supported = vconnection && vconnection.decoding_supported(targetChannel.properties.channel_codec);
2019-04-04 19:47:52 +00:00
if(this.client_status.channel_codec_decoding_supported !== decoding_supported || this.client_status.channel_codec_encoding_supported !== encoding_supported) {
this.client_status.channel_codec_decoding_supported = decoding_supported;
this.client_status.channel_codec_encoding_supported = encoding_supported;
let message;
if(!encoding_supported && !decoding_supported)
message = tr("This channel has an unsupported codec.<br>You cant speak or listen to anybody within this channel!");
else if(!encoding_supported)
message = tr("This channel has an unsupported codec.<br>You cant speak within this channel!");
else if(!decoding_supported)
message = tr("This channel has an unsupported codec.<br>You listen to anybody within this channel!"); /* implies speaking does not work as well */
if(message)
createErrorModal(tr("Channel codec unsupported"), message).open();
}
}
this.client_status = this.client_status || {} as any;
this.client_status.sound_record_supported = support_record;
this.client_status.sound_playback_supported = support_playback;
if(vconnection && vconnection.voice_recorder() && vconnection.voice_recorder().record_supported) {
const active = !this.client_status.input_muted && !this.client_status.output_muted;
/* No need to start the microphone when we're not even connected */
2020-02-22 13:30:17 +00:00
const input = vconnection.voice_recorder().input;
if(input) {
if(active && this.serverConnection.connected()) {
2020-03-30 11:44:18 +00:00
if(input.current_state() === InputState.PAUSED) {
2020-02-22 13:30:17 +00:00
input.start().then(result => {
2020-03-30 11:44:18 +00:00
if(result != InputStartResult.EOK)
2020-02-22 13:30:17 +00:00
throw result;
}).catch(error => {
log.warn(LogCategory.VOICE, tr("Failed to start microphone input (%s)."), error);
if(Date.now() - (this._last_record_error_popup || 0) > 10 * 1000) {
this._last_record_error_popup = Date.now();
2020-03-30 11:44:18 +00:00
createErrorModal(tr("Failed to start recording"), formatMessage(tr("Microphone start failed.{:br:}Error: {}"), error)).open();
2020-02-22 13:30:17 +00:00
}
});
}
} else {
input.stop();
2019-08-21 08:00:01 +00:00
}
}
}
2019-04-04 19:47:52 +00:00
//TODO: Only trigger events for stuff which has been updated
this.event_registry.fire("notify_state_updated", {
state: "microphone"
});
this.event_registry.fire("notify_state_updated", {
state: "speaker"
});
top_menu.update_state(); //TODO: Top-Menu should register their listener
2019-04-04 19:47:52 +00:00
}
sync_status_with_server() {
if(this.serverConnection.connected())
this.serverConnection.send_command("clientupdate", {
client_input_muted: this.client_status.input_muted,
client_output_muted: this.client_status.output_muted,
client_away: typeof(this.client_status.away) === "string" || this.client_status.away,
client_away_message: typeof(this.client_status.away) === "string" ? this.client_status.away : "",
client_input_hardware: this.client_status.sound_record_supported && this.client_status.input_hardware,
client_output_hardware: this.client_status.sound_playback_supported
}).catch(error => {
log.warn(LogCategory.GENERAL, tr("Failed to sync handler state with server. Error: %o"), error);
2020-03-30 11:44:18 +00:00
this.log.log(server_log.Type.ERROR_CUSTOM, {message: tr("Failed to sync handler state with server.")});
2019-04-04 19:47:52 +00:00
});
}
resize_elements() {
this.channelTree.handle_resized();
this.invoke_resized_on_activate = false;
}
acquire_recorder(voice_recoder: RecorderProfile, update_control_bar: boolean) {
/* TODO: If the voice connection hasn't been set upped cache the target recorder */
2019-04-04 19:47:52 +00:00
const vconnection = this.serverConnection.voice_connection();
(vconnection ? vconnection.acquire_voice_recorder(voice_recoder) : Promise.resolve()).catch(error => {
2019-08-30 21:06:39 +00:00
log.warn(LogCategory.VOICE, tr("Failed to acquire recorder (%o)"), error);
}).then(() => {
this.update_voice_status(undefined);
});
2019-04-04 19:47:52 +00:00
}
getVoiceRecorder() :RecorderProfile | undefined { return this.serverConnection?.voice_connection()?.voice_recorder(); }
2020-03-30 11:44:18 +00:00
reconnect_properties(profile?: ConnectionProfile) : ConnectParameters {
const name = (this.getClient() ? this.getClient().clientNickName() : "") ||
(this.serverConnection && this.serverConnection.handshake_handler() ? this.serverConnection.handshake_handler().parameters.nickname : "") ||
2020-03-30 11:44:18 +00:00
StaticSettings.instance.static(Settings.KEY_CONNECT_USERNAME, profile ? profile.default_username : undefined) ||
"Another TeaSpeak user";
const channel = (this.getClient() && this.getClient().currentChannel() ? this.getClient().currentChannel().channelId : 0) ||
(this.serverConnection && this.serverConnection.handshake_handler() ? (this.serverConnection.handshake_handler().parameters.channel || {} as any).target : "");
const channel_password = (this.getClient() && this.getClient().currentChannel() ? this.getClient().currentChannel().cached_password() : "") ||
(this.serverConnection && this.serverConnection.handshake_handler() ? (this.serverConnection.handshake_handler().parameters.channel || {} as any).password : "");
return {
channel: channel ? {target: "/" + channel, password: channel_password} : undefined,
nickname: name,
password: this.serverConnection && this.serverConnection.handshake_handler() ? this.serverConnection.handshake_handler().parameters.password : undefined
}
}
2019-08-21 08:00:01 +00:00
update_avatar() {
2020-03-30 11:44:18 +00:00
spawnAvatarUpload(data => {
2019-08-21 08:00:01 +00:00
if(typeof(data) === "undefined")
return;
if(data === null) {
2019-08-30 21:06:39 +00:00
log.info(LogCategory.CLIENT, tr("Deleting existing avatar"));
2019-08-21 08:00:01 +00:00
this.serverConnection.send_command('ftdeletefile', {
name: "/avatar_", /* delete own avatar */
path: "",
cid: 0
}).then(() => {
createInfoModal(tr("Avatar deleted"), tr("Avatar successfully deleted")).open();
}).catch(error => {
2019-08-30 21:06:39 +00:00
log.error(LogCategory.GENERAL, tr("Failed to reset avatar flag: %o"), error);
2019-08-21 08:00:01 +00:00
let message;
if(error instanceof CommandResult)
2020-03-30 11:44:18 +00:00
message = formatMessage(tr("Failed to delete avatar.{:br:}Error: {0}"), error.extra_message || error.message);
2019-08-21 08:00:01 +00:00
if(!message)
2020-03-30 11:44:18 +00:00
message = formatMessage(tr("Failed to delete avatar.{:br:}Lookup the console for more details"));
2019-08-21 08:00:01 +00:00
createErrorModal(tr("Failed to delete avatar"), message).open();
return;
});
} else {
2019-08-30 21:06:39 +00:00
log.info(LogCategory.CLIENT, tr("Uploading new avatar"));
2019-08-21 08:00:01 +00:00
(async () => {
2020-03-30 11:44:18 +00:00
let key: UploadKey;
2019-08-21 08:00:01 +00:00
try {
key = await this.fileManager.upload_file({
size: data.byteLength,
path: '',
name: '/avatar',
overwrite: true,
channel: undefined,
channel_password: undefined
});
} catch(error) {
2019-08-30 21:06:39 +00:00
log.error(LogCategory.GENERAL, tr("Failed to initialize avatar upload: %o"), error);
2019-08-21 08:00:01 +00:00
let message;
if(error instanceof CommandResult) {
//TODO: Resolve permission name
//i_client_max_avatar_filesize
if(error.id == ErrorID.PERMISSION_ERROR) {
2020-03-30 11:44:18 +00:00
message = formatMessage(tr("Failed to initialize avatar upload.{:br:}Missing permission {0}"), error["failed_permid"]);
2019-08-21 08:00:01 +00:00
} else {
2020-03-30 11:44:18 +00:00
message = formatMessage(tr("Failed to initialize avatar upload.{:br:}Error: {0}"), error.extra_message || error.message);
2019-08-21 08:00:01 +00:00
}
}
if(!message)
2020-03-30 11:44:18 +00:00
message = formatMessage(tr("Failed to initialize avatar upload.{:br:}Lookup the console for more details"));
2019-08-21 08:00:01 +00:00
createErrorModal(tr("Failed to upload avatar"), message).open();
return;
}
try {
2020-03-30 11:44:18 +00:00
await spawn_upload_transfer(key).put_data(data);
2019-08-21 08:00:01 +00:00
} catch(error) {
2019-08-30 21:06:39 +00:00
log.error(LogCategory.GENERAL, tr("Failed to upload avatar: %o"), error);
2019-08-21 08:00:01 +00:00
let message;
if(typeof(error) === "string")
2020-03-30 11:44:18 +00:00
message = formatMessage(tr("Failed to upload avatar.{:br:}Error: {0}"), error);
2019-08-21 08:00:01 +00:00
if(!message)
2020-03-30 11:44:18 +00:00
message = formatMessage(tr("Failed to initialize avatar upload.{:br:}Lookup the console for more details"));
2019-08-21 08:00:01 +00:00
createErrorModal(tr("Failed to upload avatar"), message).open();
return;
}
try {
await this.serverConnection.send_command('clientupdate', {
client_flag_avatar: guid()
});
} catch(error) {
2019-08-30 21:06:39 +00:00
log.error(LogCategory.GENERAL, tr("Failed to update avatar flag: %o"), error);
2019-08-21 08:00:01 +00:00
let message;
if(error instanceof CommandResult)
2020-03-30 11:44:18 +00:00
message = formatMessage(tr("Failed to update avatar flag.{:br:}Error: {0}"), error.extra_message || error.message);
2019-08-21 08:00:01 +00:00
if(!message)
2020-03-30 11:44:18 +00:00
message = formatMessage(tr("Failed to update avatar flag.{:br:}Lookup the console for more details"));
2019-08-21 08:00:01 +00:00
createErrorModal(tr("Failed to set avatar"), message).open();
return;
}
createInfoModal(tr("Avatar successfully uploaded"), tr("Your avatar has been uploaded successfully!")).open();
})();
}
});
}
destroy() {
this.event_registry.unregister_handler(this);
2019-08-21 08:00:01 +00:00
this.cancel_reconnect(true);
this.tag_connection_handler && this.tag_connection_handler.remove();
this.tag_connection_handler = undefined;
this.hostbanner && this.hostbanner.destroy();
this.hostbanner = undefined;
this._local_client && this._local_client.destroy();
this._local_client = undefined;
this.channelTree && this.channelTree.destroy();
this.channelTree = undefined;
this.side_bar && this.side_bar.destroy();
this.side_bar = undefined;
this.log && this.log.destroy();
this.log = undefined;
this.permissions && this.permissions.destroy();
this.permissions = undefined;
this.groups && this.groups.destroy();
this.groups = undefined;
this.fileManager && this.fileManager.destroy();
this.fileManager = undefined;
this.settings && this.settings.destroy();
this.settings = undefined;
if(this.serverConnection) {
this.serverConnection.onconnectionstatechanged = undefined;
connection.destroy_server_connection(this.serverConnection);
}
this.serverConnection = undefined;
this.sound = undefined;
this._local_client = undefined;
}
/* state changing methods */
setMicrophoneMuted(muted: boolean) {
if(this.client_status.input_muted === muted) return;
this.client_status.input_muted = muted;
this.sound.play(muted ? Sound.MICROPHONE_MUTED : Sound.MICROPHONE_ACTIVATED);
this.update_voice_status();
}
isMicrophoneMuted() { return this.client_status.input_muted; }
/*
* Returns whatever the client is able to talk or not. Reasons for returning true could be:
* - Channel codec isn't supported
* - No recorder has been acquired
* - Voice bridge hasn't been set upped yet
*/
isMicrophoneDisabled() { return !this.client_status.input_hardware; }
setSpeakerMuted(muted: boolean) {
if(this.client_status.output_muted === muted) return;
if(muted) this.sound.play(Sound.SOUND_MUTED); /* play the sound *before* we're setting the muted state */
this.client_status.output_muted = muted;
if(!muted) this.sound.play(Sound.SOUND_ACTIVATED); /* play the sound *after* we're setting we've unmuted the sound */
this.update_voice_status();
}
isSpeakerMuted() { return this.client_status.output_muted; }
/*
* Returns whatever the client is able to playback sound (voice). Reasons for returning true could be:
* - Channel codec isn't supported
* - Voice bridge hasn't been set upped yet
*/
//TODO: This currently returns false
isSpeakerDisabled() { return false; }
setSubscribeToAllChannels(flag: boolean) {
if(this.client_status.channel_subscribe_all === flag) return;
this.client_status.channel_subscribe_all = flag;
if(flag)
this.channelTree.subscribe_all_channels();
else
this.channelTree.unsubscribe_all_channels();
this.event_registry.fire("notify_state_updated", { state: "subscribe" });
}
isSubscribeToAllChannels() { return this.client_status.channel_subscribe_all; }
setAway(state: boolean | string) {
this.setAway_(state, true);
}
private setAway_(state: boolean | string, play_sound: boolean) {
if(this.client_status.away === state)
return;
const was_away = this.isAway();
const will_away = typeof state === "boolean" ? state : true;
if(was_away != will_away && play_sound)
this.sound.play(will_away ? Sound.AWAY_ACTIVATED : Sound.AWAY_DEACTIVATED);
this.client_status.away = state;
this.serverConnection.send_command("clientupdate", {
client_away: typeof(this.client_status.away) === "string" || this.client_status.away,
client_away_message: typeof(this.client_status.away) === "string" ? this.client_status.away : "",
}).catch(error => {
log.warn(LogCategory.GENERAL, tr("Failed to update away status. Error: %o"), error);
this.log.log(server_log.Type.ERROR_CUSTOM, {message: tr("Failed to update away status.")});
});
this.event_registry.fire("notify_state_updated", {
state: "away"
});
}
isAway() : boolean { return typeof this.client_status.away !== "boolean" || this.client_status.away; }
setQueriesShown(flag: boolean) {
if(this.client_status.queries_visible === flag) return;
this.client_status.queries_visible = flag;
this.channelTree.toggle_server_queries(flag);
this.event_registry.fire("notify_state_updated", {
state: "query"
});
}
areQueriesShown() {
return this.client_status.queries_visible;
}
}
export type ConnectionStateUpdateType = "microphone" | "speaker" | "away" | "subscribe" | "query";
export interface ConnectionEvents {
notify_state_updated: {
state: ConnectionStateUpdateType;
}
notify_connection_state_changed: {
old_state: ConnectionState,
new_state: ConnectionState
}
2019-04-04 19:47:52 +00:00
}