diff --git a/ChangeLog.md b/ChangeLog.md index 196bddca..d812f6bb 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -1,4 +1,7 @@ # Changelog: +* **06.10.19** + - Added the possibility to connect within an already running TeaWeb instance + * **30.10.19** - Removed old `files.php` script and replaced it with a more modern `file.ts` script to generate the environment - Some small UI fixed diff --git a/shared/js/BrowserIPC.ts b/shared/js/BrowserIPC.ts index f813ffd6..977088cb 100644 --- a/shared/js/BrowserIPC.ts +++ b/shared/js/BrowserIPC.ts @@ -28,8 +28,8 @@ namespace bipc { export interface ChannelMessage { channel_id: string; - key: string; - message: any; + type: string; + data: any; } export interface ProcessQueryResponse { @@ -63,7 +63,7 @@ namespace bipc { abstract send_message(type: string, data: any, target?: string); protected handle_message(message: BroadcastMessage) { - log.trace(LogCategory.IPC, tr("Received message %o"), message); + //log.trace(LogCategory.IPC, tr("Received message %o"), message); if(message.receiver === BasicIPCHandler.BROADCAST_UNIQUE_ID) { if(message.type == "process-query") { @@ -86,7 +86,8 @@ namespace bipc { log.warn(LogCategory.IPC, tr("Received a query response for an unknown request.")); } return; - } else if(message.type == "certificate-accept-callback") { + } + else if(message.type == "certificate-accept-callback") { const data: CertificateAcceptCallback = message.data; if(!this._cert_accept_callbacks[data.request_id]) { log.warn(LogCategory.IPC, tr("Received certificate accept callback for an unknown request ID.")); @@ -99,26 +100,28 @@ namespace bipc { } as CertificateAcceptSucceeded, message.sender); return; - } else if(message.type == "certificate-accept-succeeded") { + } + else if(message.type == "certificate-accept-succeeded") { if(!this._cert_accept_succeeded[message.sender]) { log.warn(LogCategory.IPC, tr("Received certificate accept succeeded, but haven't a callback.")); return; } this._cert_accept_succeeded[message.sender](); return; - } else if(message.type === "channel") { - const data: ChannelMessage = message.data; + } + } + if(message.type === "channel") { + const data: ChannelMessage = message.data; - let channel_invoked = false; - for(const channel of this._channels) - if(channel.channel_id === data.channel_id && (typeof(channel.target_id) === "undefined" || channel.target_id === message.sender)) { - if(channel.message_handler) - channel.message_handler(message.sender, data); - channel_invoked = true; - } - if(!channel_invoked) { - console.warn(tr("Received channel message for unknown channel (%s)"), data.channel_id); + let channel_invoked = false; + for(const channel of this._channels) + if(channel.channel_id === data.channel_id && (typeof(channel.target_id) === "undefined" || channel.target_id === message.sender)) { + if(channel.message_handler) + channel.message_handler(message.sender, message.receiver === BasicIPCHandler.BROADCAST_UNIQUE_ID, data); + channel_invoked = true; } + if(!channel_invoked) { + console.warn(tr("Received channel message for unknown channel (%s)"), data.channel_id); } } } @@ -128,15 +131,17 @@ namespace bipc { target_id: target_id, channel_id: channel_id || uuidv4(), message_handler: undefined, - send_message: (key: string, message: any) => { - if(!channel.target_id) - throw "channel has no target!"; + send_message: (type: string, data: any, target?: string) => { + if(typeof target !== "undefined") { + if(typeof channel.target_id === "string" && target != channel.target_id) + throw "target id does not match channel target"; + } this.send_message("channel", { - key: key, - message: message, + type: type, + data: data, channel_id: channel.channel_id - } as ChannelMessage, channel.target_id) + } as ChannelMessage, target || channel.target_id || BasicIPCHandler.BROADCAST_UNIQUE_ID); } }; @@ -198,8 +203,8 @@ namespace bipc { readonly channel_id: string; target_id?: string; - message_handler: (remote_id: string, message: ChannelMessage) => any; - send_message(key: string, message: any); + message_handler: (remote_id: string, broadcast: boolean, message: ChannelMessage) => any; + send_message(type: string, message: any, target?: string); } class BroadcastChannelIPC extends BasicIPCHandler { @@ -252,232 +257,460 @@ namespace bipc { } } - export interface MethodProxyInvokeData { - method_name: string; - arguments: any[]; - promise_id: string; - } - export interface MethodProxyResultData { - promise_id: string; - result: any; - success: boolean; - } - export interface MethodProxyCallback { - promise: Promise; - promise_id: string; - - resolve: (object: any) => any; - reject: (object: any) => any; - } - - export type MethodProxyConnectParameters = { - channel_id: string; - client_id: string; - } - export abstract class MethodProxy { - readonly ipc_handler: BasicIPCHandler; - private _ipc_channel: Channel; - private _ipc_parameters: MethodProxyConnectParameters; - - private readonly _local: boolean; - private readonly _slave: boolean; - - private _connected: boolean; - private _proxied_methods: {[key: string]:() => Promise} = {}; - private _proxied_callbacks: {[key: string]:MethodProxyCallback} = {}; - - protected constructor(ipc_handler: BasicIPCHandler, connect_params?: MethodProxyConnectParameters) { - this.ipc_handler = ipc_handler; - this._ipc_parameters = connect_params; - this._connected = false; - this._slave = typeof(connect_params) !== "undefined"; - this._local = typeof(connect_params) !== "undefined" && connect_params.channel_id === "local" && connect_params.client_id === "local"; + export namespace connect { + export interface ConnectOffer { + request_id: string; + data: ConnectRequestData; } - protected setup() { - if(this._local) { - this._connected = true; - this.on_connected(); - } else { - if(this._slave) - this._ipc_channel = this.ipc_handler.create_channel(this._ipc_parameters.client_id, this._ipc_parameters.channel_id); - else - this._ipc_channel = this.ipc_handler.create_channel(); - - this._ipc_channel.message_handler = this._handle_message.bind(this); - if(this._slave) - this._ipc_channel.send_message("initialize", {}); - } + export interface ConnectOfferAnswer { + request_id: string; + accepted: boolean; } - protected finalize() { - if(!this._local) { - if(this._connected) - this._ipc_channel.send_message("finalize", {}); - - this.ipc_handler.delete_channel(this._ipc_channel); - this._ipc_channel = undefined; - } - for(const promise of Object.values(this._proxied_callbacks)) - promise.reject("disconnected"); - this._proxied_callbacks = {}; - - this._connected = false; - this.on_disconnected(); + export interface ConnectExecute { + request_id: string; } - protected register_method(method: (...args: any[]) => Promise | string) { - let method_name: string; - if(typeof method === "function") { - log.debug(LogCategory.IPC, tr("Registering method proxy for %s"), method.name); - method_name = method.name; - } else { - log.debug(LogCategory.IPC, tr("Registering method proxy for %s"), method); - method_name = method; + export interface ConnectExecuted { + request_id: string; + succeeded: boolean; + message?: string; + } + + /* The connect process: + * 1. Broadcast an offer + * 2. Wait 50ms for all offer responses or until the first one respond with "ok" + * 3. Select (if possible) on accepted offer and execute the connect + */ + export class ConnectHandler { + private static readonly CHANNEL_NAME = "connect"; + + readonly ipc_handler: BasicIPCHandler; + private ipc_channel: Channel; + + public callback_available: (data: ConnectRequestData) => boolean = () => false; + public callback_execute: (data: ConnectRequestData) => boolean | string = () => false; + + + private _pending_connect_offers: { + id: string; + data: ConnectRequestData; + timeout: number; + + remote_handler: string; + }[] = []; + + private _pending_connects_requests: { + id: string; + + data: ConnectRequestData; + timeout: number; + + callback_success: () => any; + callback_failed: (message: string) => any; + callback_avail: () => Promise; + + remote_handler?: string; + }[] = []; + + constructor(ipc_handler: BasicIPCHandler) { + this.ipc_handler = ipc_handler; } - if(!this[method_name]) - throw "method is missing in current object"; + public setup() { + this.ipc_channel = this.ipc_handler.create_channel(undefined, ConnectHandler.CHANNEL_NAME); + this.ipc_channel.message_handler = this.on_message.bind(this); + } - this._proxied_methods[method_name] = this[method_name]; - if(!this._local) { - this[method_name] = (...args: any[]) => { - if(!this._connected) - return Promise.reject("not connected"); + private on_message(sender: string, broadcast: boolean, message: ChannelMessage) { + if(broadcast) { + if(message.type == "offer") { + const data = message.data as ConnectOffer; - const proxy_callback = { - promise_id: uuidv4() - } as MethodProxyCallback; - this._proxied_callbacks[proxy_callback.promise_id] = proxy_callback; - proxy_callback.promise = new Promise((resolve, reject) => { - proxy_callback.resolve = resolve; - proxy_callback.reject = reject; - }); + const response = { + accepted: this.callback_available(data.data), + request_id: data.request_id + } as ConnectOfferAnswer; - this._ipc_channel.send_message("invoke", { - promise_id: proxy_callback.promise_id, - arguments: [...args], - method_name: method_name - } as MethodProxyInvokeData); - return proxy_callback.promise; + if(response.accepted) { + log.debug(LogCategory.IPC, tr("Received new connect offer from %s: %s"), sender, data.request_id); + + const ld = { + remote_handler: sender, + data: data.data, + id: data.request_id, + timeout: 0 + }; + this._pending_connect_offers.push(ld); + ld.timeout = setTimeout(() => { + log.debug(LogCategory.IPC, tr("Dropping connect request %s, because we never received an execute."), ld.id); + this._pending_connect_offers.remove(ld); + }, 120 * 1000) as any; + } + this.ipc_channel.send_message("offer-answer", response, sender); + } + } else { + if(message.type == "offer-answer") { + const data = message.data as ConnectOfferAnswer; + const request = this._pending_connects_requests.find(e => e.id === data.request_id); + if(!request) { + log.warn(LogCategory.IPC, tr("Received connect offer answer with unknown request id (%s)."), data.request_id); + return; + } + if(!data.accepted) { + log.debug(LogCategory.IPC, tr("Client %s rejected the connect offer (%s)."), sender, request.id); + return; + } + if(request.remote_handler) { + log.debug(LogCategory.IPC, tr("Client %s accepted the connect offer (%s), but offer has already been accepted."), sender, request.id); + return; + } + + log.debug(LogCategory.IPC, tr("Client %s accepted the connect offer (%s). Request local acceptance."), sender, request.id); + request.remote_handler = sender; + clearTimeout(request.timeout); + + request.callback_avail().then(flag => { + if(!flag) { + request.callback_failed("local avail rejected"); + return; + } + + log.debug(LogCategory.IPC, tr("Executing connect with client %s"), request.remote_handler); + this.ipc_channel.send_message("execute", { + request_id: request.id + } as ConnectExecute, request.remote_handler); + request.timeout = setTimeout(() => { + request.callback_failed("connect execute timeout"); + }, 1000) as any; + }).catch(error => { + log.error(LogCategory.IPC, tr("Local avail callback caused an error: %o"), error); + request.callback_failed(tr("local avail callback caused an error")); + }); + + } + else if(message.type == "executed") { + const data = message.data as ConnectExecuted; + const request = this._pending_connects_requests.find(e => e.id === data.request_id); + if(!request) { + log.warn(LogCategory.IPC, tr("Received connect executed with unknown request id (%s)."), data.request_id); + return; + } + + if(request.remote_handler != sender) { + log.warn(LogCategory.IPC, tr("Received connect executed for request %s, but from wrong client: %s (expected %s)"), data.request_id, sender, request.remote_handler); + return; + } + + log.debug(LogCategory.IPC, tr("Received connect executed response from client %s for request %s. Succeeded: %o (%s)"), sender, data.request_id, data.succeeded, data.message); + clearTimeout(request.timeout); + if(data.succeeded) + request.callback_success(); + else + request.callback_failed(data.message); + } + else if(message.type == "execute") { + const data = message.data as ConnectExecute; + const request = this._pending_connect_offers.find(e => e.id === data.request_id); + if(!request) { + log.warn(LogCategory.IPC, tr("Received connect execute with unknown request id (%s)."), data.request_id); + return; + } + + if(request.remote_handler != sender) { + log.warn(LogCategory.IPC, tr("Received connect execute for request %s, but from wrong client: %s (expected %s)"), data.request_id, sender, request.remote_handler); + return; + } + clearTimeout(request.timeout); + this._pending_connect_offers.remove(request); + + log.debug(LogCategory.IPC, tr("Executing connect for %s"), data.request_id); + const cr = this.callback_execute(request.data); + + const response = { + request_id: data.request_id, + + succeeded: typeof(cr) !== "string" && cr, + message: typeof(cr) === "string" ? cr : "", + } as ConnectExecuted; + this.ipc_channel.send_message("executed", response, request.remote_handler); + } } } - } - private _handle_message(remote_id: string, message: ChannelMessage) { - if(message.key === "finalize") { - this._handle_finalize(); - } else if(message.key === "initialize") { - this._handle_remote_callback(remote_id); - } else if(message.key === "invoke") { - this._handle_invoke(message.message); - } else if(message.key === "result") { - this._handle_result(message.message); + post_connect_request(data: ConnectRequestData, callback_avail: () => Promise) : Promise { + return new Promise((resolve, reject) => { + const pd = { + data: data, + id: uuidv4(), + timeout: 0, + + callback_success: () => { + this._pending_connects_requests.remove(pd); + clearTimeout(pd.timeout); + resolve(); + }, + + callback_failed: error => { + this._pending_connects_requests.remove(pd); + clearTimeout(pd.timeout); + reject(error); + }, + + callback_avail: callback_avail, + }; + this._pending_connects_requests.push(pd); + + this.ipc_channel.send_message("offer", { + request_id: pd.id, + data: pd.data + } as ConnectOffer); + pd.timeout = setTimeout(() => { + pd.callback_failed("received no response to offer"); + }, 50) as any; + }) } } + } - private _handle_finalize() { - this.on_disconnected(); - this.finalize(); - this._connected = false; + export namespace mproxy { + export interface MethodProxyInvokeData { + method_name: string; + arguments: any[]; + promise_id: string; + } + export interface MethodProxyResultData { + promise_id: string; + result: any; + success: boolean; + } + export interface MethodProxyCallback { + promise: Promise; + promise_id: string; + + resolve: (object: any) => any; + reject: (object: any) => any; } - private _handle_remote_callback(remote_id: string) { - if(!this._ipc_channel.target_id) { - if(this._slave) - throw "initialize wrong state!"; - - this._ipc_channel.target_id = remote_id; /* now we're able to send messages */ - this.on_connected(); - this._ipc_channel.send_message("initialize", true); - } else { - if(!this._slave) - throw "initialize wrong state!"; - - this.on_connected(); - } - this._connected = true; + export type MethodProxyConnectParameters = { + channel_id: string; + client_id: string; } + export abstract class MethodProxy { + readonly ipc_handler: BasicIPCHandler; + private _ipc_channel: Channel; + private _ipc_parameters: MethodProxyConnectParameters; - private _send_result(promise_id: string, success: boolean, message: any) { - this._ipc_channel.send_message("result", { - promise_id: promise_id, - result: message, - success: success - } as MethodProxyResultData); - } + private readonly _local: boolean; + private readonly _slave: boolean; - private _handle_invoke(data: MethodProxyInvokeData) { - if(this._proxied_methods[data.method_name]) - throw "we could not invoke a local proxied method!"; + private _connected: boolean; + private _proxied_methods: {[key: string]:() => Promise} = {}; + private _proxied_callbacks: {[key: string]:MethodProxyCallback} = {}; - if(!this[data.method_name]) { - this._send_result(data.promise_id, false, "missing method"); - return; + protected constructor(ipc_handler: BasicIPCHandler, connect_params?: MethodProxyConnectParameters) { + this.ipc_handler = ipc_handler; + this._ipc_parameters = connect_params; + this._connected = false; + this._slave = typeof(connect_params) !== "undefined"; + this._local = typeof(connect_params) !== "undefined" && connect_params.channel_id === "local" && connect_params.client_id === "local"; } - try { - log.info(LogCategory.IPC, tr("Invoking method %s with arguments: %o"), data.method_name, data.arguments); + protected setup() { + if(this._local) { + this._connected = true; + this.on_connected(); + } else { + if(this._slave) + this._ipc_channel = this.ipc_handler.create_channel(this._ipc_parameters.client_id, this._ipc_parameters.channel_id); + else + this._ipc_channel = this.ipc_handler.create_channel(); - const promise = this[data.method_name](...data.arguments); - promise.then(result => { - log.info(LogCategory.IPC, tr("Result: %o"), result); - this._send_result(data.promise_id, true, result); - }).catch(error => { + this._ipc_channel.message_handler = this._handle_message.bind(this); + if(this._slave) + this._ipc_channel.send_message("initialize", {}); + } + } + + protected finalize() { + if(!this._local) { + if(this._connected) + this._ipc_channel.send_message("finalize", {}); + + this.ipc_handler.delete_channel(this._ipc_channel); + this._ipc_channel = undefined; + } + for(const promise of Object.values(this._proxied_callbacks)) + promise.reject("disconnected"); + this._proxied_callbacks = {}; + + this._connected = false; + this.on_disconnected(); + } + + protected register_method(method: (...args: any[]) => Promise | string) { + let method_name: string; + if(typeof method === "function") { + log.debug(LogCategory.IPC, tr("Registering method proxy for %s"), method.name); + method_name = method.name; + } else { + log.debug(LogCategory.IPC, tr("Registering method proxy for %s"), method); + method_name = method; + } + + if(!this[method_name]) + throw "method is missing in current object"; + + this._proxied_methods[method_name] = this[method_name]; + if(!this._local) { + this[method_name] = (...args: any[]) => { + if(!this._connected) + return Promise.reject("not connected"); + + const proxy_callback = { + promise_id: uuidv4() + } as MethodProxyCallback; + this._proxied_callbacks[proxy_callback.promise_id] = proxy_callback; + proxy_callback.promise = new Promise((resolve, reject) => { + proxy_callback.resolve = resolve; + proxy_callback.reject = reject; + }); + + this._ipc_channel.send_message("invoke", { + promise_id: proxy_callback.promise_id, + arguments: [...args], + method_name: method_name + } as MethodProxyInvokeData); + return proxy_callback.promise; + } + } + } + + private _handle_message(remote_id: string, boradcast: boolean, message: ChannelMessage) { + if(message.type === "finalize") { + this._handle_finalize(); + } else if(message.type === "initialize") { + this._handle_remote_callback(remote_id); + } else if(message.type === "invoke") { + this._handle_invoke(message.data); + } else if(message.type === "result") { + this._handle_result(message.data); + } + } + + private _handle_finalize() { + this.on_disconnected(); + this.finalize(); + this._connected = false; + } + + private _handle_remote_callback(remote_id: string) { + if(!this._ipc_channel.target_id) { + if(this._slave) + throw "initialize wrong state!"; + + this._ipc_channel.target_id = remote_id; /* now we're able to send messages */ + this.on_connected(); + this._ipc_channel.send_message("initialize", true); + } else { + if(!this._slave) + throw "initialize wrong state!"; + + this.on_connected(); + } + this._connected = true; + } + + private _send_result(promise_id: string, success: boolean, message: any) { + this._ipc_channel.send_message("result", { + promise_id: promise_id, + result: message, + success: success + } as MethodProxyResultData); + } + + private _handle_invoke(data: MethodProxyInvokeData) { + if(this._proxied_methods[data.method_name]) + throw "we could not invoke a local proxied method!"; + + if(!this[data.method_name]) { + this._send_result(data.promise_id, false, "missing method"); + return; + } + + try { + log.info(LogCategory.IPC, tr("Invoking method %s with arguments: %o"), data.method_name, data.arguments); + + const promise = this[data.method_name](...data.arguments); + promise.then(result => { + log.info(LogCategory.IPC, tr("Result: %o"), result); + this._send_result(data.promise_id, true, result); + }).catch(error => { + this._send_result(data.promise_id, false, error); + }); + } catch(error) { this._send_result(data.promise_id, false, error); - }); - } catch(error) { - this._send_result(data.promise_id, false, error); - return; + return; + } } - } - private _handle_result(data: MethodProxyResultData) { - if(!this._proxied_callbacks[data.promise_id]) { - console.warn(tr("Received proxy method result for unknown promise")); - return; + private _handle_result(data: MethodProxyResultData) { + if(!this._proxied_callbacks[data.promise_id]) { + console.warn(tr("Received proxy method result for unknown promise")); + return; + } + const callback = this._proxied_callbacks[data.promise_id]; + delete this._proxied_callbacks[data.promise_id]; + + if(data.success) + callback.resolve(data.result); + else + callback.reject(data.result); } - const callback = this._proxied_callbacks[data.promise_id]; - delete this._proxied_callbacks[data.promise_id]; - if(data.success) - callback.resolve(data.result); - else - callback.reject(data.result); + generate_connect_parameters() : MethodProxyConnectParameters { + if(this._slave) + throw "only masters can generate connect parameters!"; + if(!this._ipc_channel) + throw "please call setup() before"; + + return { + channel_id: this._ipc_channel.channel_id, + client_id: this.ipc_handler.get_local_address() + }; + } + + is_slave() { return this._local || this._slave; } /* the popout modal */ + is_master() { return this._local || !this._slave; } /* the host (teaweb application) */ + + protected abstract on_connected(); + protected abstract on_disconnected(); } - - generate_connect_parameters() : MethodProxyConnectParameters { - if(this._slave) - throw "only masters can generate connect parameters!"; - if(!this._ipc_channel) - throw "please call setup() before"; - - return { - channel_id: this._ipc_channel.channel_id, - client_id: this.ipc_handler.get_local_address() - }; - } - - is_slave() { return this._local || this._slave; } /* the popout modal */ - is_master() { return this._local || !this._slave; } /* the host (teaweb application) */ - - protected abstract on_connected(); - protected abstract on_disconnected(); } let handler: BasicIPCHandler; + let connect_handler: connect.ConnectHandler; + export function setup() { if(!supported()) return; - /* TODO: test for support */ + handler = new BroadcastChannelIPC(); handler.setup(); + + connect_handler = new connect.ConnectHandler(handler); + connect_handler.setup(); } export function get_handler() { return handler; } + export function get_connect_handler() { + return connect_handler; + } + export function supported() { /* ios does not support this */ return typeof(window.BroadcastChannel) !== "undefined"; diff --git a/shared/js/main.ts b/shared/js/main.ts index 3c01125f..8071c1ea 100644 --- a/shared/js/main.ts +++ b/shared/js/main.ts @@ -8,6 +8,8 @@ /// /// +import spawnYesNo = Modals.spawnYesNo; + const js_render = window.jsrender || $; const native_client = window.require !== undefined; @@ -288,34 +290,41 @@ interface Window { } */ -function execute_default_connect() { - if(settings.static(Settings.KEY_FLAG_CONNECT_DEFAULT, false) && settings.static(Settings.KEY_CONNECT_ADDRESS, "")) { - const profile_uuid = settings.static(Settings.KEY_CONNECT_PROFILE, (profiles.default_profile() || {id: 'default'}).id); - const profile = profiles.find_profile(profile_uuid) || profiles.default_profile(); - const address = settings.static(Settings.KEY_CONNECT_ADDRESS, ""); - const username = settings.static(Settings.KEY_CONNECT_USERNAME, "Another TeaSpeak user"); +type ConnectRequestData = { + address: string; - const password = settings.static(Settings.KEY_CONNECT_PASSWORD, ""); - const password_hashed = settings.static(Settings.KEY_FLAG_CONNECT_PASSWORD, false); + profile?: string; + username?: string; + password?: { + value: string; + hashed: boolean; + }; +} +function handle_connect_request(properties: ConnectRequestData, connection: ConnectionHandler) { + const profile_uuid = properties.profile || (profiles.default_profile() || {id: 'default'}).id; + const profile = profiles.find_profile(profile_uuid) || profiles.default_profile(); + const username = properties.username || profile.connect_username(); - if(profile && profile.valid()) { - const connection = server_connections.active_connection_handler() || server_connections.spawn_server_connection_handler(); - connection.startConnection(address, profile, true, { - nickname: username, - password: password.length > 0 ? { - password: password, - hashed: password_hashed - } : undefined - }); - } else { - Modals.spawnConnectModal({},{ - url: address, - enforce: true - }, { - profile: profile, - enforce: true - }); - } + const password = properties.password ? properties.password.value : ""; + const password_hashed = properties.password ? properties.password.hashed : false; + + if(profile && profile.valid()) { + connection.startConnection(properties.address, profile, true, { + nickname: username, + password: password.length > 0 ? { + password: password, + hashed: password_hashed + } : undefined + }); + server_connections.set_active_connection_handler(connection); + } else { + Modals.spawnConnectModal({},{ + url: properties.address, + enforce: true + }, { + profile: profile, + enforce: true + }); } } @@ -416,7 +425,6 @@ function main() { }; /* schedule it a bit later then the main because the main function is still within the loader */ - setTimeout(execute_default_connect, 5); setTimeout(() => { const connection = server_connections.active_connection_handler(); /* @@ -499,6 +507,74 @@ const task_teaweb_starter: loader.Task = { priority: 10 }; +const task_connect_handler: loader.Task = { + name: "Connect handler", + function: async () => { + const address = settings.static(Settings.KEY_CONNECT_ADDRESS, ""); + const chandler = bipc.get_connect_handler(); + if(settings.static(Settings.KEY_FLAG_CONNECT_DEFAULT, false) && address) { + const connect_data = { + address: address, + + profile: settings.static(Settings.KEY_CONNECT_PROFILE, ""), + username: settings.static(Settings.KEY_CONNECT_USERNAME, ""), + + password: { + value: settings.static(Settings.KEY_CONNECT_PASSWORD, ""), + hashed: settings.static(Settings.KEY_FLAG_CONNECT_PASSWORD, false) + } + }; + + if(chandler) { + try { + await chandler.post_connect_request(connect_data, () => new Promise((resolve, reject) => { + spawnYesNo(tr("Another TeaWeb instance is already running"), tra("Another TeaWeb instance is already running.{:br:}Would you like to connect there?"), response => { + resolve(response); + }, { + closeable: false + }).open(); + })); + log.info(LogCategory.CLIENT, tr("Executed connect successfully in another browser window. Closing this window")); + + const message = + "You're connecting to {0} within the other TeaWeb instance.{:br:}" + + "You could now close this page."; + createInfoModal( + tr("Connecting successfully within other instance"), + MessageHelper.formatMessage(tr(message), connect_data.address), + { + closeable: false, + footer: undefined + } + ).open(); + return; + } catch(error) { + log.info(LogCategory.CLIENT, tr("Failed to execute connect within other TeaWeb instance. Using this one. Error: %o"), error); + } + } + + loader.register_task(loader.Stage.LOADED, { + priority: 0, + function: async () => handle_connect_request(connect_data, server_connections.active_connection_handler() || server_connections.spawn_server_connection_handler()), + name: tr("default url connect") + }); + } + if(chandler) { + /* no instance avail, so lets make us avail */ + chandler.callback_available = data => { + return !settings.static_global(Settings.KEY_DISABLE_MULTI_SESSION); + }; + + chandler.callback_execute = data => { + handle_connect_request(data, server_connections.spawn_server_connection_handler()); + return true; + } + } + loader.register_task(loader.Stage.LOADED, task_teaweb_starter); + }, + priority: 10 +}; + const task_certificate_callback: loader.Task = { name: "certificate accept tester", function: async () => { @@ -546,7 +622,7 @@ const task_certificate_callback: loader.Task = { log.info(LogCategory.IPC, tr("We're not used to accept certificated. Booting app.")); } - loader.register_task(loader.Stage.LOADED, task_teaweb_starter); + loader.register_task(loader.Stage.LOADED, task_connect_handler); }, priority: 10 }; @@ -574,8 +650,9 @@ loader.register_task(loader.Stage.JAVASCRIPT_INITIALIZING, { if(app.is_web()) { loader.register_task(loader.Stage.LOADED, task_certificate_callback); - } else + } else { loader.register_task(loader.Stage.LOADED, task_teaweb_starter); + } } catch (ex) { if(ex instanceof Error || typeof(ex.stack) !== "undefined") console.error((tr || (msg => msg))("Critical error stack trace: %o"), ex.stack); diff --git a/shared/js/ui/modal/ModalYesNo.ts b/shared/js/ui/modal/ModalYesNo.ts index 91e015aa..4562d4ac 100644 --- a/shared/js/ui/modal/ModalYesNo.ts +++ b/shared/js/ui/modal/ModalYesNo.ts @@ -3,7 +3,9 @@ namespace Modals { export function spawnYesNo(header: BodyCreator, body: BodyCreator, callback: (_: boolean) => any, properties?: { text_yes?: string, - text_no?: string + text_no?: string, + + closeable?: boolean; }) { properties = properties || {}; @@ -16,6 +18,7 @@ namespace Modals { props.header = header; props.template_properties.question = ModalFunctions.jqueriefy(body); + props.closeable = typeof(properties.closeable) !== "boolean" || properties.closeable; const modal = createModal(props); let submited = false; const button_yes = modal.htmlTag.find(".button-yes");