diff --git a/shared/js/BrowserIPC.ts b/shared/js/BrowserIPC.ts index a371cba7..b7271f6e 100644 --- a/shared/js/BrowserIPC.ts +++ b/shared/js/BrowserIPC.ts @@ -1,8 +1,8 @@ -export interface Window { +interface Window { BroadcastChannel: BroadcastChannel; } -export namespace bipc { +namespace bipc { export interface BroadcastMessage { timestamp: number; receiver: string; diff --git a/shared/js/ConnectionHandler.ts b/shared/js/ConnectionHandler.ts index d1256c5f..85c3b8af 100644 --- a/shared/js/ConnectionHandler.ts +++ b/shared/js/ConnectionHandler.ts @@ -1,6 +1,6 @@ /// /// -/// +/// /// /// /// @@ -8,17 +8,7 @@ /// /// -import {ChannelTree} from "./channel-tree/view"; -import {LocalClientEntry} from "./channel-tree/client"; -import {ServerAddress} from "./channel-tree/server"; -import {ChannelEntry} from "./channel-tree/channel"; -import {AbstractServerConnection} from "./connection/ConnectionBase"; -import {PermissionManager} from "./permission/PermissionManager"; -import {GroupManager} from "./permission/GroupManager"; -import {ServerSettings} from "./settings"; -import {Hostbanner} from "./ui/frames/hostbanner"; - -export enum DisconnectReason { +enum DisconnectReason { HANDLER_DESTROYED, REQUESTED, DNS_FAILED, @@ -38,7 +28,7 @@ export enum DisconnectReason { UNKNOWN } -export enum ConnectionState { +enum ConnectionState { UNCONNECTED, CONNECTING, INITIALISING, @@ -46,7 +36,7 @@ export enum ConnectionState { DISCONNECTING } -export enum ViewReasonId { +enum ViewReasonId { VREASON_USER_ACTION = 0, VREASON_MOVED = 1, VREASON_SYSTEM = 2, @@ -61,7 +51,7 @@ export enum ViewReasonId { VREASON_SERVER_SHUTDOWN = 11 } -export interface VoiceStatus { +interface VoiceStatus { input_hardware: boolean; input_muted: boolean; output_muted: boolean; @@ -78,7 +68,7 @@ export interface VoiceStatus { queries_visible: boolean; } -export interface ConnectParameters { +interface ConnectParameters { nickname?: string; channel?: { target: string | number; @@ -89,10 +79,10 @@ export interface ConnectParameters { auto_reconnect_attempt?: boolean; } -export class ConnectionHandler { +class ConnectionHandler { channelTree: ChannelTree; - serverConnection: AbstractServerConnection; + serverConnection: connection.AbstractServerConnection; fileManager: FileManager; diff --git a/shared/js/FileManager.ts b/shared/js/FileManager.ts index b20e369a..e20cc336 100644 --- a/shared/js/FileManager.ts +++ b/shared/js/FileManager.ts @@ -1,26 +1,21 @@ -import {ChannelEntry} from "./channel-tree/channel"; -import {AbstractCommandHandler, ServerCommand} from "./connection/ConnectionBase"; -import {ConnectionHandler} from "./ConnectionHandler"; -import {CommandResult} from "./connection/ServerConnectionDeclaration"; -import {log, LogCategory} from "./log"; -import {ClientEntry} from "./channel-tree/client"; -import {hex} from "./crypto/hex"; +/// +/// -export class FileEntry { +class FileEntry { name: string; datetime: number; type: number; size: number; } -export class FileListRequest { +class FileListRequest { path: string; entries: FileEntry[]; callback: (entries: FileEntry[]) => void; } -export namespace transfer { +namespace transfer { export interface TransferKey { client_transfer_id: number; server_transfer_id: number; @@ -157,7 +152,7 @@ class RequestFileUpload implements transfer.UploadTransfer { } } -class FileManager extends AbstractCommandHandler { +class FileManager extends connection.AbstractCommandHandler { handle: ConnectionHandler; icons: IconManager; avatars: AvatarManager; @@ -196,7 +191,7 @@ class FileManager extends AbstractCommandHandler { this.avatars = undefined; } - handle_command(command: ServerCommand): boolean { + handle_command(command: connection.ServerCommand): boolean { switch (command.command) { case "notifyfilelist": this.notifyFileList(command.arguments); diff --git a/shared/js/MessageFormatter.ts b/shared/js/MessageFormatter.ts index 3ef9906c..66bb2d7f 100644 --- a/shared/js/MessageFormatter.ts +++ b/shared/js/MessageFormatter.ts @@ -1,10 +1,4 @@ -import {Settings, settings} from "./settings"; -import {contextmenu} from "./ui/elements/context_menu"; -import {image_preview} from "./ui/frames/image_preview"; -import {guid} from "./crypto/uid"; - -declare const xbbcode; -export namespace messages.formatter { +namespace messages.formatter { export namespace bbcode { const sanitizer_escaped = (key: string) => "[-- sescaped: " + key + " --]"; const sanitizer_escaped_regex = /\[-- sescaped: ([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}) --]/; diff --git a/shared/js/PPTListener.ts b/shared/js/PPTListener.ts index 939963a8..b0d32f5d 100644 --- a/shared/js/PPTListener.ts +++ b/shared/js/PPTListener.ts @@ -1,4 +1,4 @@ -export enum KeyCode { +enum KeyCode { KEY_CANCEL = 3, KEY_HELP = 6, KEY_BACK_SPACE = 8, @@ -118,7 +118,7 @@ export enum KeyCode { KEY_META = 224 } -export namespace ppt { +namespace ppt { export enum EventType { KEY_PRESS, KEY_RELEASE, diff --git a/shared/js/bookmarks.ts b/shared/js/bookmarks.ts index 60a1268a..cb12ca5e 100644 --- a/shared/js/bookmarks.ts +++ b/shared/js/bookmarks.ts @@ -1,6 +1,14 @@ -import {profiles} from "./profiles/ConnectionProfile"; +namespace bookmarks { + function guid() { + function s4() { + return Math + .floor((1 + Math.random()) * 0x10000) + .toString(16) + .substring(1); + } + return s4() + s4() + '-' + s4() + '-' + s4() + '-' + s4() + '-' + s4() + s4() + s4(); + } -export namespace bookmarks { export const boorkmak_connect = (mark: Bookmark, new_tab?: boolean) => { const profile = profiles.find_profile(mark.connect_profile) || profiles.default_profile(); if(profile.valid()) { diff --git a/shared/js/connection/CommandHandler.ts b/shared/js/connection/CommandHandler.ts index e2a72bb1..f5e4bfed 100644 --- a/shared/js/connection/CommandHandler.ts +++ b/shared/js/connection/CommandHandler.ts @@ -1,466 +1,309 @@ -import {ConnectionHandler, DisconnectReason, ViewReasonId} from "../ConnectionHandler"; -import { - AbstractCommandHandler, - AbstractCommandHandlerBoss, - AbstractServerConnection, - CommandOptions, ServerCommand -} from "./ConnectionBase"; -import {CommandResult, ErrorID} from "./ServerConnectionDeclaration"; -import {Sound} from "../sound/Sounds"; -import {log, LogCategory} from "../log"; -import {MessageHelper} from "../ui/frames/chat"; -import { - ClientConnectionInfo, - ClientEntry, - ClientType, - LocalClientEntry, - MusicClientEntry, SongInfo -} from "../channel-tree/client"; -import {ChannelEntry} from "../channel-tree/channel"; -import {chat as pchat} from "../ui/frames/side/private_conversations"; -import {Modals} from "../ui/modal/ModalPoke"; -import {chat} from "../ui/frames/side/conversations"; -import Conversation = chat.channel.Conversation; -import {createErrorModal, createInfoModal, createInputModal, createModal} from "../ui/elements/modal"; -import {server_connections} from "../ui/frames/connection_handlers"; -import {server} from "../ui/frames/server_log"; +/// -export class ServerConnectionCommandBoss extends AbstractCommandHandlerBoss { - constructor(connection: AbstractServerConnection) { - super(connection); - } -} +namespace connection { + import Conversation = chat.channel.Conversation; + import MusicInfo = chat.MusicInfo; -export class ConnectionCommandHandler extends AbstractCommandHandler { - readonly connection: AbstractServerConnection; - readonly connection_handler: ConnectionHandler; - - constructor(connection: AbstractServerConnection) { - super(connection); - this.connection_handler = connection.client; - - this["error"] = this.handleCommandResult; - this["channellist"] = this.handleCommandChannelList; - this["channellistfinished"] = this.handleCommandChannelListFinished; - this["notifychannelcreated"] = this.handleCommandChannelCreate; - this["notifychanneldeleted"] = this.handleCommandChannelDelete; - this["notifychannelhide"] = this.handleCommandChannelHide; - this["notifychannelshow"] = this.handleCommandChannelShow; - - this["notifyserverconnectioninfo"] = this.handleNotifyServerConnectionInfo; - this["notifyconnectioninfo"] = this.handleNotifyConnectionInfo; - - this["notifycliententerview"] = this.handleCommandClientEnterView; - this["notifyclientleftview"] = this.handleCommandClientLeftView; - this["notifyclientmoved"] = this.handleNotifyClientMoved; - this["initserver"] = this.handleCommandServerInit; - this["notifychannelmoved"] = this.handleNotifyChannelMoved; - this["notifychanneledited"] = this.handleNotifyChannelEdited; - this["notifytextmessage"] = this.handleNotifyTextMessage; - this["notifyclientchatcomposing"] = this.notifyClientChatComposing; - this["notifyclientchatclosed"] = this.handleNotifyClientChatClosed; - this["notifyclientupdated"] = this.handleNotifyClientUpdated; - this["notifyserveredited"] = this.handleNotifyServerEdited; - this["notifyserverupdated"] = this.handleNotifyServerUpdated; - - this["notifyclientpoke"] = this.handleNotifyClientPoke; - - this["notifymusicplayerinfo"] = this.handleNotifyMusicPlayerInfo; - - this["notifyservergroupclientadded"] = this.handleNotifyServerGroupClientAdd; - this["notifyservergroupclientdeleted"] = this.handleNotifyServerGroupClientRemove; - this["notifyclientchannelgroupchanged"] = this.handleNotifyClientChannelGroupChanged; - - this["notifychannelsubscribed"] = this.handleNotifyChannelSubscribed; - this["notifychannelunsubscribed"] = this.handleNotifyChannelUnsubscribed; - - this["notifyconversationhistory"] = this.handleNotifyConversationHistory; - this["notifyconversationmessagedelete"] = this.handleNotifyConversationMessageDelete; - - this["notifymusicstatusupdate"] = this.handleNotifyMusicStatusUpdate; - this["notifymusicplayersongchange"] = this.handleMusicPlayerSongChange; - - this["notifyplaylistsongadd"] = this.handleNotifyPlaylistSongAdd; - this["notifyplaylistsongremove"] = this.handleNotifyPlaylistSongRemove; - this["notifyplaylistsongreorder"] = this.handleNotifyPlaylistSongReorder; - this["notifyplaylistsongloaded"] = this.handleNotifyPlaylistSongLoaded; + export class ServerConnectionCommandBoss extends AbstractCommandHandlerBoss { + constructor(connection: AbstractServerConnection) { + super(connection); + } } - private loggable_invoker(unique_id, client_id, name) : server.base.Client | undefined { - const id = parseInt(client_id); - if(typeof(client_id) === "undefined" || Number.isNaN(id)) - return undefined; + export class ConnectionCommandHandler extends AbstractCommandHandler { + readonly connection: AbstractServerConnection; + readonly connection_handler: ConnectionHandler; + + constructor(connection: AbstractServerConnection) { + super(connection); + this.connection_handler = connection.client; + + this["error"] = this.handleCommandResult; + this["channellist"] = this.handleCommandChannelList; + this["channellistfinished"] = this.handleCommandChannelListFinished; + this["notifychannelcreated"] = this.handleCommandChannelCreate; + this["notifychanneldeleted"] = this.handleCommandChannelDelete; + this["notifychannelhide"] = this.handleCommandChannelHide; + this["notifychannelshow"] = this.handleCommandChannelShow; + + this["notifyserverconnectioninfo"] = this.handleNotifyServerConnectionInfo; + this["notifyconnectioninfo"] = this.handleNotifyConnectionInfo; + + this["notifycliententerview"] = this.handleCommandClientEnterView; + this["notifyclientleftview"] = this.handleCommandClientLeftView; + this["notifyclientmoved"] = this.handleNotifyClientMoved; + this["initserver"] = this.handleCommandServerInit; + this["notifychannelmoved"] = this.handleNotifyChannelMoved; + this["notifychanneledited"] = this.handleNotifyChannelEdited; + this["notifytextmessage"] = this.handleNotifyTextMessage; + this["notifyclientchatcomposing"] = this.notifyClientChatComposing; + this["notifyclientchatclosed"] = this.handleNotifyClientChatClosed; + this["notifyclientupdated"] = this.handleNotifyClientUpdated; + this["notifyserveredited"] = this.handleNotifyServerEdited; + this["notifyserverupdated"] = this.handleNotifyServerUpdated; + + this["notifyclientpoke"] = this.handleNotifyClientPoke; + + this["notifymusicplayerinfo"] = this.handleNotifyMusicPlayerInfo; + + this["notifyservergroupclientadded"] = this.handleNotifyServerGroupClientAdd; + this["notifyservergroupclientdeleted"] = this.handleNotifyServerGroupClientRemove; + this["notifyclientchannelgroupchanged"] = this.handleNotifyClientChannelGroupChanged; + + this["notifychannelsubscribed"] = this.handleNotifyChannelSubscribed; + this["notifychannelunsubscribed"] = this.handleNotifyChannelUnsubscribed; + + this["notifyconversationhistory"] = this.handleNotifyConversationHistory; + this["notifyconversationmessagedelete"] = this.handleNotifyConversationMessageDelete; + + this["notifymusicstatusupdate"] = this.handleNotifyMusicStatusUpdate; + this["notifymusicplayersongchange"] = this.handleMusicPlayerSongChange; + + this["notifyplaylistsongadd"] = this.handleNotifyPlaylistSongAdd; + this["notifyplaylistsongremove"] = this.handleNotifyPlaylistSongRemove; + this["notifyplaylistsongreorder"] = this.handleNotifyPlaylistSongReorder; + this["notifyplaylistsongloaded"] = this.handleNotifyPlaylistSongLoaded; + } + + private loggable_invoker(unique_id, client_id, name) : log.server.base.Client | undefined { + const id = parseInt(client_id); + if(typeof(client_id) === "undefined" || Number.isNaN(id)) + return undefined; + + if(id == 0) + return { + client_id: 0, + client_unique_id: this.connection_handler.channelTree.server.properties.virtualserver_unique_identifier, + client_name: this.connection_handler.channelTree.server.properties.virtualserver_name, + }; - if(id == 0) return { - client_id: 0, - client_unique_id: this.connection_handler.channelTree.server.properties.virtualserver_unique_identifier, - client_name: this.connection_handler.channelTree.server.properties.virtualserver_name, + client_unique_id: unique_id, + client_name: name, + client_id: client_id }; + } - return { - client_unique_id: unique_id, - client_name: name, - client_id: client_id - }; - } + proxy_command_promise(promise: Promise, options: connection.CommandOptions) { + if(!options.process_result) + return promise; - proxy_command_promise(promise: Promise, options: CommandOptions) { - if(!options.process_result) - return promise; - - return promise.catch(ex => { - if(options.process_result) { - if(ex instanceof CommandResult) { - let res = ex; - if(!res.success) { - if(res.id == ErrorID.PERMISSION_ERROR) { //Permission error - const permission = this.connection_handler.permissions.resolveInfo(res.json["failed_permid"] as number); - res.message = tr("Insufficient client permissions. Failed on permission ") + (permission ? permission.name : "unknown"); - this.connection_handler.log.log(server.Type.ERROR_PERMISSION, { - permission: this.connection_handler.permissions.resolveInfo(res.json["failed_permid"] as number) - }); - this.connection_handler.sound.play(Sound.ERROR_INSUFFICIENT_PERMISSIONS); - } else if(res.id != ErrorID.EMPTY_RESULT) { - this.connection_handler.log.log(server.Type.ERROR_CUSTOM, { - message: res.extra_message.length == 0 ? res.message : res.extra_message - }); + return promise.catch(ex => { + if(options.process_result) { + if(ex instanceof CommandResult) { + let res = ex; + if(!res.success) { + if(res.id == ErrorID.PERMISSION_ERROR) { //Permission error + const permission = this.connection_handler.permissions.resolveInfo(res.json["failed_permid"] as number); + res.message = tr("Insufficient client permissions. Failed on permission ") + (permission ? permission.name : "unknown"); + this.connection_handler.log.log(log.server.Type.ERROR_PERMISSION, { + permission: this.connection_handler.permissions.resolveInfo(res.json["failed_permid"] as number) + }); + this.connection_handler.sound.play(Sound.ERROR_INSUFFICIENT_PERMISSIONS); + } else if(res.id != ErrorID.EMPTY_RESULT) { + this.connection_handler.log.log(log.server.Type.ERROR_CUSTOM, { + message: res.extra_message.length == 0 ? res.message : res.extra_message + }); + } } + } else if(typeof(ex) === "string") { + this.connection_handler.log.log(log.server.Type.CONNECTION_COMMAND_ERROR, {error: ex}); + } else { + log.error(LogCategory.NETWORKING, tr("Invalid promise result type: %s. Result: %o"), typeof (ex), ex); } - } else if(typeof(ex) === "string") { - this.connection_handler.log.log(server.Type.CONNECTION_COMMAND_ERROR, {error: ex}); - } else { - log.error(LogCategory.NETWORKING, tr("Invalid promise result type: %s. Result: %o"), typeof (ex), ex); } - } - return Promise.reject(ex); - }); - } - - handle_command(command: ServerCommand) : boolean { - if(this[command.command]) { - this[command.command](command.arguments); - return true; - } - - return false; - } - - set_handler(command: string, handler: any) { - this[command] = handler; - } - - unset_handler(command: string, handler?: any) { - if(handler && this[command] != handler) return; - this[command] = undefined; - } - - handleCommandResult(json) { - json = json[0]; //Only one bulk - - let code : string = json["return_code"]; - if(!code || code.length == 0) { - log.warn(LogCategory.NETWORKING, tr("Invalid return code! (%o)"), json); - return; - } - let retListeners = this.connection["_retListener"]; - - for(let e of retListeners) { - if(e.code != code) continue; - retListeners.remove(e); - let result = new CommandResult(json); - if(result.success) - e.resolve(result); - else - e.reject(result); - break; - } - } - - handleCommandServerInit(json){ - //We could setup the voice channel - if(this.connection.support_voice()) { - log.debug(LogCategory.NETWORKING, tr("Setting up voice")); - } else { - log.debug(LogCategory.NETWORKING, tr("Skipping voice setup (No voice bridge available)")); - } - - - json = json[0]; //Only one bulk - - this.connection_handler.channelTree.registerClient(this.connection_handler.getClient()); - this.connection.client.side_bar.channel_conversations().reset(); - this.connection.client.clientId = parseInt(json["aclid"]); - this.connection.client.getClient().updateVariables( {key: "client_nickname", value: json["acn"]}); - - let updates: { - key: string, - value: string - }[] = []; - for(let key in json) { - if(key === "aclid") continue; - if(key === "acn") continue; - - updates.push({key: key, value: json[key]}); - } - this.connection.client.channelTree.server.updateVariables(false, ...updates); - - const properties = this.connection.client.channelTree.server.properties; - /* host message */ - if(properties.virtualserver_hostmessage_mode > 0) { - if(properties.virtualserver_hostmessage_mode == 1) { - /* show in log */ - this.connection_handler.log.log(server.Type.SERVER_HOST_MESSAGE, { - message: properties.virtualserver_hostmessage - }); - } else { - /* create modal/create modal and quit */ - createModal({ - header: tr("Host message"), - body: MessageHelper.bbcode_chat(properties.virtualserver_hostmessage), - footer: undefined - }).open(); - - if(properties.virtualserver_hostmessage_mode == 3) { - /* first let the client initialize his stuff */ - setTimeout(() => { - this.connection_handler.log.log(server.Type.SERVER_HOST_MESSAGE_DISCONNECT, { - message: properties.virtualserver_welcomemessage - }); - - this.connection.disconnect("host message disconnect"); - this.connection_handler.handleDisconnect(DisconnectReason.SERVER_HOSTMESSAGE); - this.connection_handler.sound.play(Sound.CONNECTION_DISCONNECTED); - }, 100); - } - } - } - - /* welcome message */ - if(properties.virtualserver_welcomemessage) { - this.connection_handler.log.log(server.Type.SERVER_WELCOME_MESSAGE, { - message: properties.virtualserver_welcomemessage + return Promise.reject(ex); }); } - /* priviledge key */ - if(properties.virtualserver_ask_for_privilegekey) { - createInputModal(tr("Use a privilege key"), tr("This is a newly created server for which administrator privileges have not yet been claimed.
Please enter the \"privilege key\" that was automatically generated when this server was created to gain administrator permissions."), message => message.length > 0, result => { - if(!result) return; - const scon = server_connections.active_connection_handler(); - - if(scon.serverConnection.connected) - scon.serverConnection.send_command("tokenuse", { - token: result - }).then(() => { - createInfoModal(tr("Use privilege key"), tr("Privilege key successfully used!")).open(); - }).catch(error => { - createErrorModal(tr("Use privilege key"), MessageHelper.formatMessage(tr("Failed to use privilege key: {}"), error instanceof CommandResult ? error.message : error)).open(); - }); - }, { field_placeholder: 'Enter Privilege Key' }).open(); - } - - this.connection_handler.log.log(server.Type.CONNECTION_CONNECTED, { - own_client: this.connection_handler.getClient().log_data() - }); - this.connection_handler.sound.play(Sound.CONNECTION_CONNECTED); - this.connection.client.onConnected(); - } - - handleNotifyServerConnectionInfo(json) { - json = json[0]; - - /* everything is a number, so lets parse it */ - for(const key of Object.keys(json)) - json[key] = parseFloat(json[key]); - - this.connection_handler.channelTree.server.set_connection_info(json); - } - - handleNotifyConnectionInfo(json) { - json = json[0]; - - const object = new ClientConnectionInfo(); - /* everything is a number (except ip), so lets parse it */ - for(const key of Object.keys(json)) { - if(key === "connection_client_ip") - object[key] = json[key]; - else - object[key] = parseFloat(json[key]); - } - - const client = this.connection_handler.channelTree.findClient(parseInt(json["clid"])); - if(!client) { - log.warn(LogCategory.NETWORKING, tr("Received client connection info for unknown client (%o)"), json["clid"]); - return; - } - - client.set_connection_info(object); - } - - private createChannelFromJson(json, ignoreOrder: boolean = false) { - let tree = this.connection.client.channelTree; - - let channel = new ChannelEntry(parseInt(json["cid"]), json["channel_name"], tree.findChannel(json["cpid"])); - tree.insertChannel(channel); - if(json["channel_order"] !== "0") { - let prev = tree.findChannel(json["channel_order"]); - if(!prev && json["channel_order"] != 0) { - if(!ignoreOrder) { - log.error(LogCategory.NETWORKING, tr("Invalid channel order id!")); - return; - } + handle_command(command: ServerCommand) : boolean { + if(this[command.command]) { + this[command.command](command.arguments); + return true; } - let parent = tree.findChannel(json["cpid"]); - if(!parent && json["cpid"] != 0) { - log.error(LogCategory.NETWORKING, tr("Invalid channel parent")); + return false; + } + + set_handler(command: string, handler: any) { + this[command] = handler; + } + + unset_handler(command: string, handler?: any) { + if(handler && this[command] != handler) return; + this[command] = undefined; + } + + handleCommandResult(json) { + json = json[0]; //Only one bulk + + let code : string = json["return_code"]; + if(!code || code.length == 0) { + log.warn(LogCategory.NETWORKING, tr("Invalid return code! (%o)"), json); return; } - tree.moveChannel(channel, prev, parent); //TODO test if channel exists! - } - if(ignoreOrder) { - for(let ch of tree.channels) { - if(ch.properties.channel_order == channel.channelId) { - tree.moveChannel(ch, channel, channel.parent); //Corrent the order :) - } + let retListeners = this.connection["_retListener"]; + + for(let e of retListeners) { + if(e.code != code) continue; + retListeners.remove(e); + let result = new CommandResult(json); + if(result.success) + e.resolve(result); + else + e.reject(result); + break; } } - let updates: { - key: string, - value: string - }[] = []; - for(let key in json) { - if(key === "cid") continue; - if(key === "cpid") continue; - if(key === "invokerid") continue; - if(key === "invokername") continue; - if(key === "invokeruid") continue; - if(key === "reasonid") continue; - - updates.push({key: key, value: json[key]}); - } - channel.updateVariables(...updates); - } - - handleCommandChannelList(json) { - this.connection.client.channelTree.hide_channel_tree(); /* dont perform channel inserts on the dom to prevent style recalculations */ - log.debug(LogCategory.NETWORKING, tr("Got %d new channels"), json.length); - for(let index = 0; index < json.length; index++) - this.createChannelFromJson(json[index], true); - } - - - handleCommandChannelListFinished(json) { - this.connection.client.channelTree.show_channel_tree(); - } - - handleCommandChannelCreate(json) { - this.createChannelFromJson(json[0]); - } - - handleCommandChannelShow(json) { - this.createChannelFromJson(json[0]); //TODO may chat? - } - - handleCommandChannelDelete(json) { - let tree = this.connection.client.channelTree; - const conversations = this.connection.client.side_bar.channel_conversations(); - - log.info(LogCategory.NETWORKING, tr("Got %d channel deletions"), json.length); - for(let index = 0; index < json.length; index++) { - conversations.delete_conversation(parseInt(json[index]["cid"])); - let channel = tree.findChannel(json[index]["cid"]); - if(!channel) { - log.error(LogCategory.NETWORKING, tr("Invalid channel onDelete (Unknown channel)")); - continue; - } - tree.deleteChannel(channel); - } - } - - handleCommandChannelHide(json) { - let tree = this.connection.client.channelTree; - const conversations = this.connection.client.side_bar.channel_conversations(); - - log.info(LogCategory.NETWORKING, tr("Got %d channel hides"), json.length); - for(let index = 0; index < json.length; index++) { - conversations.delete_conversation(parseInt(json[index]["cid"])); - let channel = tree.findChannel(json[index]["cid"]); - if(!channel) { - log.error(LogCategory.NETWORKING, tr("Invalid channel on hide (Unknown channel)")); - continue; - } - tree.deleteChannel(channel); - } - } - - handleCommandClientEnterView(json) { - let tree = this.connection.client.channelTree; - - let client: ClientEntry; - let channel = undefined; - let old_channel = undefined; - let reason_id, reason_msg; - - let invokerid, invokername, invokeruid; - - for(const entry of json) { - /* attempt to update properties if given */ - channel = typeof(entry["ctid"]) !== "undefined" ? tree.findChannel(parseInt(entry["ctid"])) : channel; - old_channel = typeof(entry["cfid"]) !== "undefined" ? tree.findChannel(parseInt(entry["cfid"])) : old_channel; - reason_id = typeof(entry["reasonid"]) !== "undefined" ? entry["reasonid"] : reason_id; - reason_msg = typeof(entry["reason_msg"]) !== "undefined" ? entry["reason_msg"] : reason_msg; - - invokerid = typeof(entry["invokerid"]) !== "undefined" ? parseInt(entry["invokerid"]) : invokerid; - invokername = typeof(entry["invokername"]) !== "undefined" ? entry["invokername"] : invokername; - invokeruid = typeof(entry["invokeruid"]) !== "undefined" ? entry["invokeruid"] : invokeruid; - - client = tree.findClient(parseInt(entry["clid"])); - - if(!client) { - if(parseInt(entry["client_type_exact"]) == ClientType.CLIENT_MUSIC) { - client = new MusicClientEntry(parseInt(entry["clid"]), entry["client_nickname"]); - } else { - client = new ClientEntry(parseInt(entry["clid"]), entry["client_nickname"]); - } - - client.properties.client_type = parseInt(entry["client_type"]); - client = tree.insertClient(client, channel); + handleCommandServerInit(json){ + //We could setup the voice channel + if(this.connection.support_voice()) { + log.debug(LogCategory.NETWORKING, tr("Setting up voice")); } else { - tree.moveClient(client, channel); + log.debug(LogCategory.NETWORKING, tr("Skipping voice setup (No voice bridge available)")); } - if(this.connection_handler.client_status.queries_visible || client.properties.client_type != ClientType.CLIENT_QUERY) { - const own_channel = this.connection.client.getClient().currentChannel(); - this.connection_handler.log.log(server.Type.CLIENT_VIEW_ENTER, { - channel_from: old_channel ? old_channel.log_data() : undefined, - channel_to: channel ? channel.log_data() : undefined, - client: client.log_data(), - invoker: this.loggable_invoker(invokeruid, invokerid, invokername), - message:reason_msg, - reason: parseInt(reason_id), - own_channel: channel == own_channel - }); - if(reason_id == ViewReasonId.VREASON_USER_ACTION) { - if(own_channel == channel) - if(old_channel) - this.connection_handler.sound.play(Sound.USER_ENTERED); - else - this.connection_handler.sound.play(Sound.USER_ENTERED_CONNECT); - } else if(reason_id == ViewReasonId.VREASON_MOVED) { - if(own_channel == channel) - this.connection_handler.sound.play(Sound.USER_ENTERED_MOVED); - } else if(reason_id == ViewReasonId.VREASON_CHANNEL_KICK) { - if(own_channel == channel) - this.connection_handler.sound.play(Sound.USER_ENTERED_KICKED); - } else if(reason_id == ViewReasonId.VREASON_SYSTEM) { + json = json[0]; //Only one bulk + this.connection_handler.channelTree.registerClient(this.connection_handler.getClient()); + this.connection.client.side_bar.channel_conversations().reset(); + this.connection.client.clientId = parseInt(json["aclid"]); + this.connection.client.getClient().updateVariables( {key: "client_nickname", value: json["acn"]}); + + let updates: { + key: string, + value: string + }[] = []; + for(let key in json) { + if(key === "aclid") continue; + if(key === "acn") continue; + + updates.push({key: key, value: json[key]}); + } + this.connection.client.channelTree.server.updateVariables(false, ...updates); + + const properties = this.connection.client.channelTree.server.properties; + /* host message */ + if(properties.virtualserver_hostmessage_mode > 0) { + if(properties.virtualserver_hostmessage_mode == 1) { + /* show in log */ + this.connection_handler.log.log(log.server.Type.SERVER_HOST_MESSAGE, { + message: properties.virtualserver_hostmessage + }); } else { - console.warn(tr("Unknown reasonid for %o"), reason_id); + /* create modal/create modal and quit */ + createModal({ + header: tr("Host message"), + body: MessageHelper.bbcode_chat(properties.virtualserver_hostmessage), + footer: undefined + }).open(); + + if(properties.virtualserver_hostmessage_mode == 3) { + /* first let the client initialize his stuff */ + setTimeout(() => { + this.connection_handler.log.log(log.server.Type.SERVER_HOST_MESSAGE_DISCONNECT, { + message: properties.virtualserver_welcomemessage + }); + + this.connection.disconnect("host message disconnect"); + this.connection_handler.handleDisconnect(DisconnectReason.SERVER_HOSTMESSAGE); + this.connection_handler.sound.play(Sound.CONNECTION_DISCONNECTED); + }, 100); + } + } + } + + /* welcome message */ + if(properties.virtualserver_welcomemessage) { + this.connection_handler.log.log(log.server.Type.SERVER_WELCOME_MESSAGE, { + message: properties.virtualserver_welcomemessage + }); + } + + /* priviledge key */ + if(properties.virtualserver_ask_for_privilegekey) { + createInputModal(tr("Use a privilege key"), tr("This is a newly created server for which administrator privileges have not yet been claimed.
Please enter the \"privilege key\" that was automatically generated when this server was created to gain administrator permissions."), message => message.length > 0, result => { + if(!result) return; + const scon = server_connections.active_connection_handler(); + + if(scon.serverConnection.connected) + scon.serverConnection.send_command("tokenuse", { + token: result + }).then(() => { + createInfoModal(tr("Use privilege key"), tr("Privilege key successfully used!")).open(); + }).catch(error => { + createErrorModal(tr("Use privilege key"), MessageHelper.formatMessage(tr("Failed to use privilege key: {}"), error instanceof CommandResult ? error.message : error)).open(); + }); + }, { field_placeholder: 'Enter Privilege Key' }).open(); + } + + this.connection_handler.log.log(log.server.Type.CONNECTION_CONNECTED, { + own_client: this.connection_handler.getClient().log_data() + }); + this.connection_handler.sound.play(Sound.CONNECTION_CONNECTED); + this.connection.client.onConnected(); + } + + handleNotifyServerConnectionInfo(json) { + json = json[0]; + + /* everything is a number, so lets parse it */ + for(const key of Object.keys(json)) + json[key] = parseFloat(json[key]); + + this.connection_handler.channelTree.server.set_connection_info(json); + } + + handleNotifyConnectionInfo(json) { + json = json[0]; + + const object = new ClientConnectionInfo(); + /* everything is a number (except ip), so lets parse it */ + for(const key of Object.keys(json)) { + if(key === "connection_client_ip") + object[key] = json[key]; + else + object[key] = parseFloat(json[key]); + } + + const client = this.connection_handler.channelTree.findClient(parseInt(json["clid"])); + if(!client) { + log.warn(LogCategory.NETWORKING, tr("Received client connection info for unknown client (%o)"), json["clid"]); + return; + } + + client.set_connection_info(object); + } + + private createChannelFromJson(json, ignoreOrder: boolean = false) { + let tree = this.connection.client.channelTree; + + let channel = new ChannelEntry(parseInt(json["cid"]), json["channel_name"], tree.findChannel(json["cpid"])); + tree.insertChannel(channel); + if(json["channel_order"] !== "0") { + let prev = tree.findChannel(json["channel_order"]); + if(!prev && json["channel_order"] != 0) { + if(!ignoreOrder) { + log.error(LogCategory.NETWORKING, tr("Invalid channel order id!")); + return; + } + } + + let parent = tree.findChannel(json["cpid"]); + if(!parent && json["cpid"] != 0) { + log.error(LogCategory.NETWORKING, tr("Invalid channel parent")); + return; + } + tree.moveChannel(channel, prev, parent); //TODO test if channel exists! + } + if(ignoreOrder) { + for(let ch of tree.channels) { + if(ch.properties.channel_order == channel.channelId) { + tree.moveChannel(ch, channel, channel.parent); //Corrent the order :) + } } } @@ -468,113 +311,159 @@ export class ConnectionCommandHandler extends AbstractCommandHandler { key: string, value: string }[] = []; - - for(let key in entry) { - if(key == "cfid") continue; - if(key == "ctid") continue; + for(let key in json) { + if(key === "cid") continue; + if(key === "cpid") continue; if(key === "invokerid") continue; if(key === "invokername") continue; if(key === "invokeruid") continue; if(key === "reasonid") continue; - updates.push({key: key, value: entry[key]}); + updates.push({key: key, value: json[key]}); } + channel.updateVariables(...updates); + } - client.updateVariables(...updates); + handleCommandChannelList(json) { + this.connection.client.channelTree.hide_channel_tree(); /* dont perform channel inserts on the dom to prevent style recalculations */ + log.debug(LogCategory.NETWORKING, tr("Got %d new channels"), json.length); + for(let index = 0; index < json.length; index++) + this.createChannelFromJson(json[index], true); + } - /* if its a new client join, or a system reason (like we joined) */ - if(!old_channel || reason_id == 2) { - /* client new join */ - const conversation_manager = this.connection_handler.side_bar.private_conversations(); - const conversation = conversation_manager.find_conversation({ - unique_id: client.properties.client_unique_identifier, - client_id: client.clientId(), - name: client.clientNickName() - }, { - create: false, - attach: true - }); - if(conversation) - client.flag_text_unread = conversation.is_unread(); - } - if(client instanceof LocalClientEntry) { - client.initializeListener(); - this.connection_handler.update_voice_status(); - this.connection_handler.side_bar.info_frame().update_channel_talk(); - const conversations = this.connection.client.side_bar.channel_conversations(); - conversations.set_current_channel(client.currentChannel().channelId); + handleCommandChannelListFinished(json) { + this.connection.client.channelTree.show_channel_tree(); + } + + handleCommandChannelCreate(json) { + this.createChannelFromJson(json[0]); + } + + handleCommandChannelShow(json) { + this.createChannelFromJson(json[0]); //TODO may chat? + } + + handleCommandChannelDelete(json) { + let tree = this.connection.client.channelTree; + const conversations = this.connection.client.side_bar.channel_conversations(); + + log.info(LogCategory.NETWORKING, tr("Got %d channel deletions"), json.length); + for(let index = 0; index < json.length; index++) { + conversations.delete_conversation(parseInt(json[index]["cid"])); + let channel = tree.findChannel(json[index]["cid"]); + if(!channel) { + log.error(LogCategory.NETWORKING, tr("Invalid channel onDelete (Unknown channel)")); + continue; + } + tree.deleteChannel(channel); } } - } - handleCommandClientLeftView(json) { - let reason_id = -1; - - for(const entry of json) { - reason_id = entry["reasonid"] || reason_id; + handleCommandChannelHide(json) { let tree = this.connection.client.channelTree; - let client = tree.findClient(entry["clid"]); - if(!client) { - log.error(LogCategory.NETWORKING, tr("Unknown client left!")); - return 0; - } - if(client == this.connection.client.getClient()) { - if(reason_id == ViewReasonId.VREASON_BAN) { - this.connection.client.handleDisconnect(DisconnectReason.CLIENT_BANNED, entry); - } else if(reason_id == ViewReasonId.VREASON_SERVER_KICK) { - this.connection.client.handleDisconnect(DisconnectReason.CLIENT_KICKED, entry); - } else if(reason_id == ViewReasonId.VREASON_SERVER_SHUTDOWN) { - this.connection.client.handleDisconnect(DisconnectReason.SERVER_CLOSED, entry); - } else if(reason_id == ViewReasonId.VREASON_SERVER_STOPPED) { - this.connection.client.handleDisconnect(DisconnectReason.SERVER_CLOSED, entry); - } else { - this.connection.client.handleDisconnect(DisconnectReason.UNKNOWN, entry); + const conversations = this.connection.client.side_bar.channel_conversations(); + + log.info(LogCategory.NETWORKING, tr("Got %d channel hides"), json.length); + for(let index = 0; index < json.length; index++) { + conversations.delete_conversation(parseInt(json[index]["cid"])); + let channel = tree.findChannel(json[index]["cid"]); + if(!channel) { + log.error(LogCategory.NETWORKING, tr("Invalid channel on hide (Unknown channel)")); + continue; } - this.connection_handler.side_bar.info_frame().update_channel_talk(); - return; + tree.deleteChannel(channel); } + } + handleCommandClientEnterView(json) { + let tree = this.connection.client.channelTree; - if(this.connection_handler.client_status.queries_visible || client.properties.client_type != ClientType.CLIENT_QUERY) { - const own_channel = this.connection.client.getClient().currentChannel(); - let channel_from = tree.findChannel(entry["cfid"]); - let channel_to = tree.findChannel(entry["ctid"]); + let client: ClientEntry; + let channel = undefined; + let old_channel = undefined; + let reason_id, reason_msg; - const is_own_channel = channel_from == own_channel; - this.connection_handler.log.log(server.Type.CLIENT_VIEW_LEAVE, { - channel_from: channel_from ? channel_from.log_data() : undefined, - channel_to: channel_to ? channel_to.log_data() : undefined, - client: client.log_data(), - invoker: this.loggable_invoker(entry["invokeruid"], entry["invokerid"], entry["invokername"]), - message: entry["reasonmsg"], - reason: parseInt(entry["reasonid"]), - ban_time: parseInt(entry["bantime"]), - own_channel: is_own_channel - }); + let invokerid, invokername, invokeruid; - if(is_own_channel) { - if(reason_id == ViewReasonId.VREASON_USER_ACTION) { - this.connection_handler.sound.play(Sound.USER_LEFT); - } else if(reason_id == ViewReasonId.VREASON_SERVER_LEFT) { - this.connection_handler.sound.play(Sound.USER_LEFT_DISCONNECT); - } else if(reason_id == ViewReasonId.VREASON_SERVER_KICK) { - this.connection_handler.sound.play(Sound.USER_LEFT_KICKED_SERVER); - } else if(reason_id == ViewReasonId.VREASON_CHANNEL_KICK) { - this.connection_handler.sound.play(Sound.USER_LEFT_KICKED_CHANNEL); - } else if(reason_id == ViewReasonId.VREASON_BAN) { - this.connection_handler.sound.play(Sound.USER_LEFT_BANNED); - } else if(reason_id == ViewReasonId.VREASON_TIMEOUT) { - this.connection_handler.sound.play(Sound.USER_LEFT_TIMEOUT); - } else if(reason_id == ViewReasonId.VREASON_MOVED) { - this.connection_handler.sound.play(Sound.USER_LEFT_MOVED); + for(const entry of json) { + /* attempt to update properties if given */ + channel = typeof(entry["ctid"]) !== "undefined" ? tree.findChannel(parseInt(entry["ctid"])) : channel; + old_channel = typeof(entry["cfid"]) !== "undefined" ? tree.findChannel(parseInt(entry["cfid"])) : old_channel; + reason_id = typeof(entry["reasonid"]) !== "undefined" ? entry["reasonid"] : reason_id; + reason_msg = typeof(entry["reason_msg"]) !== "undefined" ? entry["reason_msg"] : reason_msg; + + invokerid = typeof(entry["invokerid"]) !== "undefined" ? parseInt(entry["invokerid"]) : invokerid; + invokername = typeof(entry["invokername"]) !== "undefined" ? entry["invokername"] : invokername; + invokeruid = typeof(entry["invokeruid"]) !== "undefined" ? entry["invokeruid"] : invokeruid; + + client = tree.findClient(parseInt(entry["clid"])); + + if(!client) { + if(parseInt(entry["client_type_exact"]) == ClientType.CLIENT_MUSIC) { + client = new MusicClientEntry(parseInt(entry["clid"]), entry["client_nickname"]); } else { - log.error(LogCategory.NETWORKING, tr("Unknown client left reason %d!"), reason_id); + client = new ClientEntry(parseInt(entry["clid"]), entry["client_nickname"]); + } + + client.properties.client_type = parseInt(entry["client_type"]); + client = tree.insertClient(client, channel); + } else { + tree.moveClient(client, channel); + } + + if(this.connection_handler.client_status.queries_visible || client.properties.client_type != ClientType.CLIENT_QUERY) { + const own_channel = this.connection.client.getClient().currentChannel(); + this.connection_handler.log.log(log.server.Type.CLIENT_VIEW_ENTER, { + channel_from: old_channel ? old_channel.log_data() : undefined, + channel_to: channel ? channel.log_data() : undefined, + client: client.log_data(), + invoker: this.loggable_invoker(invokeruid, invokerid, invokername), + message:reason_msg, + reason: parseInt(reason_id), + own_channel: channel == own_channel + }); + + if(reason_id == ViewReasonId.VREASON_USER_ACTION) { + if(own_channel == channel) + if(old_channel) + this.connection_handler.sound.play(Sound.USER_ENTERED); + else + this.connection_handler.sound.play(Sound.USER_ENTERED_CONNECT); + } else if(reason_id == ViewReasonId.VREASON_MOVED) { + if(own_channel == channel) + this.connection_handler.sound.play(Sound.USER_ENTERED_MOVED); + } else if(reason_id == ViewReasonId.VREASON_CHANNEL_KICK) { + if(own_channel == channel) + this.connection_handler.sound.play(Sound.USER_ENTERED_KICKED); + } else if(reason_id == ViewReasonId.VREASON_SYSTEM) { + + } else { + console.warn(tr("Unknown reasonid for %o"), reason_id); } } - if(!channel_to) { - /* client left the server */ + let updates: { + key: string, + value: string + }[] = []; + + for(let key in entry) { + if(key == "cfid") continue; + if(key == "ctid") continue; + if(key === "invokerid") continue; + if(key === "invokername") continue; + if(key === "invokeruid") continue; + if(key === "reasonid") continue; + + updates.push({key: key, value: entry[key]}); + } + + client.updateVariables(...updates); + + /* if its a new client join, or a system reason (like we joined) */ + if(!old_channel || reason_id == 2) { + /* client new join */ const conversation_manager = this.connection_handler.side_bar.private_conversations(); const conversation = conversation_manager.find_conversation({ unique_id: client.properties.client_unique_identifier, @@ -582,599 +471,690 @@ export class ConnectionCommandHandler extends AbstractCommandHandler { name: client.clientNickName() }, { create: false, - attach: false + attach: true }); - if(conversation) { - conversation.set_state(pchat.PrivateConversationState.DISCONNECTED); + if(conversation) + client.flag_text_unread = conversation.is_unread(); + } + + if(client instanceof LocalClientEntry) { + client.initializeListener(); + this.connection_handler.update_voice_status(); + this.connection_handler.side_bar.info_frame().update_channel_talk(); + const conversations = this.connection.client.side_bar.channel_conversations(); + conversations.set_current_channel(client.currentChannel().channelId); + } + } + } + + handleCommandClientLeftView(json) { + let reason_id = -1; + + for(const entry of json) { + reason_id = entry["reasonid"] || reason_id; + let tree = this.connection.client.channelTree; + let client = tree.findClient(entry["clid"]); + if(!client) { + log.error(LogCategory.NETWORKING, tr("Unknown client left!")); + return 0; + } + if(client == this.connection.client.getClient()) { + if(reason_id == ViewReasonId.VREASON_BAN) { + this.connection.client.handleDisconnect(DisconnectReason.CLIENT_BANNED, entry); + } else if(reason_id == ViewReasonId.VREASON_SERVER_KICK) { + this.connection.client.handleDisconnect(DisconnectReason.CLIENT_KICKED, entry); + } else if(reason_id == ViewReasonId.VREASON_SERVER_SHUTDOWN) { + this.connection.client.handleDisconnect(DisconnectReason.SERVER_CLOSED, entry); + } else if(reason_id == ViewReasonId.VREASON_SERVER_STOPPED) { + this.connection.client.handleDisconnect(DisconnectReason.SERVER_CLOSED, entry); + } else { + this.connection.client.handleDisconnect(DisconnectReason.UNKNOWN, entry); + } + this.connection_handler.side_bar.info_frame().update_channel_talk(); + return; + } + + + if(this.connection_handler.client_status.queries_visible || client.properties.client_type != ClientType.CLIENT_QUERY) { + const own_channel = this.connection.client.getClient().currentChannel(); + let channel_from = tree.findChannel(entry["cfid"]); + let channel_to = tree.findChannel(entry["ctid"]); + + const is_own_channel = channel_from == own_channel; + this.connection_handler.log.log(log.server.Type.CLIENT_VIEW_LEAVE, { + channel_from: channel_from ? channel_from.log_data() : undefined, + channel_to: channel_to ? channel_to.log_data() : undefined, + client: client.log_data(), + invoker: this.loggable_invoker(entry["invokeruid"], entry["invokerid"], entry["invokername"]), + message: entry["reasonmsg"], + reason: parseInt(entry["reasonid"]), + ban_time: parseInt(entry["bantime"]), + own_channel: is_own_channel + }); + + if(is_own_channel) { + if(reason_id == ViewReasonId.VREASON_USER_ACTION) { + this.connection_handler.sound.play(Sound.USER_LEFT); + } else if(reason_id == ViewReasonId.VREASON_SERVER_LEFT) { + this.connection_handler.sound.play(Sound.USER_LEFT_DISCONNECT); + } else if(reason_id == ViewReasonId.VREASON_SERVER_KICK) { + this.connection_handler.sound.play(Sound.USER_LEFT_KICKED_SERVER); + } else if(reason_id == ViewReasonId.VREASON_CHANNEL_KICK) { + this.connection_handler.sound.play(Sound.USER_LEFT_KICKED_CHANNEL); + } else if(reason_id == ViewReasonId.VREASON_BAN) { + this.connection_handler.sound.play(Sound.USER_LEFT_BANNED); + } else if(reason_id == ViewReasonId.VREASON_TIMEOUT) { + this.connection_handler.sound.play(Sound.USER_LEFT_TIMEOUT); + } else if(reason_id == ViewReasonId.VREASON_MOVED) { + this.connection_handler.sound.play(Sound.USER_LEFT_MOVED); + } else { + log.error(LogCategory.NETWORKING, tr("Unknown client left reason %d!"), reason_id); + } + } + + if(!channel_to) { + /* client left the server */ + const conversation_manager = this.connection_handler.side_bar.private_conversations(); + const conversation = conversation_manager.find_conversation({ + unique_id: client.properties.client_unique_identifier, + client_id: client.clientId(), + name: client.clientNickName() + }, { + create: false, + attach: false + }); + if(conversation) { + conversation.set_state(chat.PrivateConversationState.DISCONNECTED); + } } } + + tree.deleteClient(client); + } + } + + handleNotifyClientMoved(json) { + json = json[0]; //Only one bulk + let tree = this.connection.client.channelTree; + let client = tree.findClient(json["clid"]); + let self = client instanceof LocalClientEntry; + + let channel_to = tree.findChannel(parseInt(json["ctid"])); + let channel_from = tree.findChannel(parseInt(json["cfid"])); + + if(!client) { + log.error(LogCategory.NETWORKING, tr("Unknown client move (Client)!")); + return 0; } - tree.deleteClient(client); - } - } - - handleNotifyClientMoved(json) { - json = json[0]; //Only one bulk - let tree = this.connection.client.channelTree; - let client = tree.findClient(json["clid"]); - let self = client instanceof LocalClientEntry; - - let channel_to = tree.findChannel(parseInt(json["ctid"])); - let channel_from = tree.findChannel(parseInt(json["cfid"])); - - if(!client) { - log.error(LogCategory.NETWORKING, tr("Unknown client move (Client)!")); - return 0; - } - - if(!channel_to) { - log.error(LogCategory.NETWORKING, tr("Unknown client move (Channel to)!")); - return 0; - } - - if(!self) { - if(!channel_from) { - log.error(LogCategory.NETWORKING, tr("Unknown client move (Channel from)!")); - channel_from = client.currentChannel(); - } else if(channel_from != client.currentChannel()) { - log.error(LogCategory.NETWORKING, - tr("Client move from invalid source channel! Local client registered in channel %d but server send %d."), - client.currentChannel().channelId, channel_from.channelId - ); + if(!channel_to) { + log.error(LogCategory.NETWORKING, tr("Unknown client move (Channel to)!")); + return 0; } - } else { - channel_from = client.currentChannel(); - } - tree.moveClient(client, channel_to); - - if(self) { - this.connection_handler.update_voice_status(channel_to); - - for(const entry of client.channelTree.clientsByChannel(channel_from)) { - if(entry !== client && entry.get_audio_handle()) { - entry.get_audio_handle().abort_replay(); - entry.speaking = false; + if(!self) { + if(!channel_from) { + log.error(LogCategory.NETWORKING, tr("Unknown client move (Channel from)!")); + channel_from = client.currentChannel(); + } else if(channel_from != client.currentChannel()) { + log.error(LogCategory.NETWORKING, + tr("Client move from invalid source channel! Local client registered in channel %d but server send %d."), + client.currentChannel().channelId, channel_from.channelId + ); } + } else { + channel_from = client.currentChannel(); } - const side_bar = this.connection_handler.side_bar; - side_bar.info_frame().update_channel_talk(); + tree.moveClient(client, channel_to); - const conversation_to = side_bar.channel_conversations().conversation(channel_to.channelId, false); - if(conversation_to) - conversation_to.update_private_state(); - - if(channel_from) { - const conversation_from = side_bar.channel_conversations().conversation(channel_from.channelId, false); - if(conversation_from) - conversation_from.update_private_state(); - } - - side_bar.channel_conversations().update_chat_box(); - } - - const own_channel = this.connection.client.getClient().currentChannel(); - this.connection_handler.log.log(server.Type.CLIENT_VIEW_MOVE, { - channel_from: channel_from ? { - channel_id: channel_from.channelId, - channel_name: channel_from.channelName() - } : undefined, - channel_from_own: channel_from == own_channel, - - channel_to: channel_to ? { - channel_id: channel_to.channelId, - channel_name: channel_to.channelName() - } : undefined, - channel_to_own: channel_to == own_channel, - - client: { - client_id: client.clientId(), - client_name: client.clientNickName(), - client_unique_id: client.properties.client_unique_identifier - }, - client_own: self, - - invoker: this.loggable_invoker(json["invokeruid"], json["invokerid"], json["invokername"]), - - message: json["reasonmsg"], - reason: parseInt(json["reasonid"]), - }); - if(json["reasonid"] == ViewReasonId.VREASON_MOVED) { - if(self) - this.connection_handler.sound.play(Sound.USER_MOVED_SELF); - else if(own_channel == channel_to) - this.connection_handler.sound.play(Sound.USER_ENTERED_MOVED); - else if(own_channel == channel_from) - this.connection_handler.sound.play(Sound.USER_LEFT_MOVED); - } else if(json["reasonid"] == ViewReasonId.VREASON_USER_ACTION) { - if(self) {} //If we do an action we wait for the error response - else if(own_channel == channel_to) - this.connection_handler.sound.play(Sound.USER_ENTERED); - else if(own_channel == channel_from) - this.connection_handler.sound.play(Sound.USER_LEFT); - } else if(json["reasonid"] == ViewReasonId.VREASON_CHANNEL_KICK) { if(self) { - this.connection_handler.sound.play(Sound.CHANNEL_KICKED); - } else if(own_channel == channel_to) - this.connection_handler.sound.play(Sound.USER_ENTERED_KICKED); - else if(own_channel == channel_from) - this.connection_handler.sound.play(Sound.USER_LEFT_KICKED_CHANNEL); - } else { - console.warn(tr("Unknown reason id %o"), json["reasonid"]); - } - } + this.connection_handler.update_voice_status(channel_to); - handleNotifyChannelMoved(json) { - json = json[0]; //Only one bulk + for(const entry of client.channelTree.clientsByChannel(channel_from)) { + if(entry !== client && entry.get_audio_handle()) { + entry.get_audio_handle().abort_replay(); + entry.speaking = false; + } + } - let tree = this.connection.client.channelTree; - let channel = tree.findChannel(json["cid"]); - if(!channel) { - log.error(LogCategory.NETWORKING, tr("Unknown channel move (Channel)!")); - return 0; - } + const side_bar = this.connection_handler.side_bar; + side_bar.info_frame().update_channel_talk(); - let prev = tree.findChannel(json["order"]); - if(!prev && json["order"] != 0) { - log.error(LogCategory.NETWORKING, tr("Unknown channel move (prev)!")); - return 0; - } + const conversation_to = side_bar.channel_conversations().conversation(channel_to.channelId, false); + if(conversation_to) + conversation_to.update_private_state(); - let parent = tree.findChannel(json["cpid"]); - if(!parent && json["cpid"] != 0) { - log.error(LogCategory.NETWORKING, tr("Unknown channel move (parent)!")); - return 0; - } + if(channel_from) { + const conversation_from = side_bar.channel_conversations().conversation(channel_from.channelId, false); + if(conversation_from) + conversation_from.update_private_state(); + } - tree.moveChannel(channel, prev, parent); - } - - handleNotifyChannelEdited(json) { - json = json[0]; //Only one bulk - - let tree = this.connection.client.channelTree; - let channel = tree.findChannel(json["cid"]); - if(!channel) { - log.error(LogCategory.NETWORKING, tr("Unknown channel edit (Channel)!")); - return 0; - } - - let updates: { - key: string, - value: string - }[] = []; - for(let key in json) { - if(key === "cid") continue; - if(key === "invokerid") continue; - if(key === "invokername") continue; - if(key === "invokeruid") continue; - if(key === "reasonid") continue; - updates.push({key: key, value: json[key]}); - } - channel.updateVariables(...updates); - - if(this.connection_handler.getClient().currentChannel() === channel) { - //TODO: Playback sound that your channel has been edited - this.connection_handler.update_voice_status(); - } - } - - handleNotifyTextMessage(json) { - json = json[0]; //Only one bulk - - let mode = json["targetmode"]; - if(mode == 1){ - //json["invokerid"], json["invokername"], json["invokeruid"] - const target_client_id = parseInt(json["target"]); - const target_own = target_client_id === this.connection.client.getClientId(); - - if(target_own && target_client_id === json["invokerid"]) { - log.error(LogCategory.NETWORKING, tr("Received conversation message from invalid client id. Data: %o"), json); - return; + side_bar.channel_conversations().update_chat_box(); } + const own_channel = this.connection.client.getClient().currentChannel(); + this.connection_handler.log.log(log.server.Type.CLIENT_VIEW_MOVE, { + channel_from: channel_from ? { + channel_id: channel_from.channelId, + channel_name: channel_from.channelName() + } : undefined, + channel_from_own: channel_from == own_channel, + + channel_to: channel_to ? { + channel_id: channel_to.channelId, + channel_name: channel_to.channelName() + } : undefined, + channel_to_own: channel_to == own_channel, + + client: { + client_id: client.clientId(), + client_name: client.clientNickName(), + client_unique_id: client.properties.client_unique_identifier + }, + client_own: self, + + invoker: this.loggable_invoker(json["invokeruid"], json["invokerid"], json["invokername"]), + + message: json["reasonmsg"], + reason: parseInt(json["reasonid"]), + }); + if(json["reasonid"] == ViewReasonId.VREASON_MOVED) { + if(self) + this.connection_handler.sound.play(Sound.USER_MOVED_SELF); + else if(own_channel == channel_to) + this.connection_handler.sound.play(Sound.USER_ENTERED_MOVED); + else if(own_channel == channel_from) + this.connection_handler.sound.play(Sound.USER_LEFT_MOVED); + } else if(json["reasonid"] == ViewReasonId.VREASON_USER_ACTION) { + if(self) {} //If we do an action we wait for the error response + else if(own_channel == channel_to) + this.connection_handler.sound.play(Sound.USER_ENTERED); + else if(own_channel == channel_from) + this.connection_handler.sound.play(Sound.USER_LEFT); + } else if(json["reasonid"] == ViewReasonId.VREASON_CHANNEL_KICK) { + if(self) { + this.connection_handler.sound.play(Sound.CHANNEL_KICKED); + } else if(own_channel == channel_to) + this.connection_handler.sound.play(Sound.USER_ENTERED_KICKED); + else if(own_channel == channel_from) + this.connection_handler.sound.play(Sound.USER_LEFT_KICKED_CHANNEL); + } else { + console.warn(tr("Unknown reason id %o"), json["reasonid"]); + } + } + + handleNotifyChannelMoved(json) { + json = json[0]; //Only one bulk + + let tree = this.connection.client.channelTree; + let channel = tree.findChannel(json["cid"]); + if(!channel) { + log.error(LogCategory.NETWORKING, tr("Unknown channel move (Channel)!")); + return 0; + } + + let prev = tree.findChannel(json["order"]); + if(!prev && json["order"] != 0) { + log.error(LogCategory.NETWORKING, tr("Unknown channel move (prev)!")); + return 0; + } + + let parent = tree.findChannel(json["cpid"]); + if(!parent && json["cpid"] != 0) { + log.error(LogCategory.NETWORKING, tr("Unknown channel move (parent)!")); + return 0; + } + + tree.moveChannel(channel, prev, parent); + } + + handleNotifyChannelEdited(json) { + json = json[0]; //Only one bulk + + let tree = this.connection.client.channelTree; + let channel = tree.findChannel(json["cid"]); + if(!channel) { + log.error(LogCategory.NETWORKING, tr("Unknown channel edit (Channel)!")); + return 0; + } + + let updates: { + key: string, + value: string + }[] = []; + for(let key in json) { + if(key === "cid") continue; + if(key === "invokerid") continue; + if(key === "invokername") continue; + if(key === "invokeruid") continue; + if(key === "reasonid") continue; + updates.push({key: key, value: json[key]}); + } + channel.updateVariables(...updates); + + if(this.connection_handler.getClient().currentChannel() === channel) { + //TODO: Playback sound that your channel has been edited + this.connection_handler.update_voice_status(); + } + } + + handleNotifyTextMessage(json) { + json = json[0]; //Only one bulk + + let mode = json["targetmode"]; + if(mode == 1){ + //json["invokerid"], json["invokername"], json["invokeruid"] + const target_client_id = parseInt(json["target"]); + const target_own = target_client_id === this.connection.client.getClientId(); + + if(target_own && target_client_id === json["invokerid"]) { + log.error(LogCategory.NETWORKING, tr("Received conversation message from invalid client id. Data: %o", json)); + return; + } + + const conversation_manager = this.connection_handler.side_bar.private_conversations(); + const conversation = conversation_manager.find_conversation({ + client_id: target_own ? parseInt(json["invokerid"]) : target_client_id, + unique_id: target_own ? json["invokeruid"] : undefined, + name: target_own ? json["invokername"] : undefined + }, { + create: target_own, + attach: target_own + }); + if(!conversation) { + log.error(LogCategory.NETWORKING, tr("Received conversation message for unknown conversation! (%s)"), target_own ? tr("Remote message") : tr("Own message")); + return; + } + + conversation.append_message(json["msg"], { + type: target_own ? "partner" : "self", + name: json["invokername"], + unique_id: json["invokeruid"], + client_id: parseInt(json["invokerid"]) + }); + + if(target_own) { + this.connection_handler.sound.play(Sound.MESSAGE_RECEIVED, {default_volume: .5}); + const client = this.connection_handler.channelTree.findClient(parseInt(json["invokerid"])); + if(client) /* the client itself might be invisible */ + client.flag_text_unread = conversation.is_unread(); + } else { + this.connection_handler.sound.play(Sound.MESSAGE_SEND, {default_volume: .5}); + } + } else if(mode == 2) { + const invoker = this.connection_handler.channelTree.findClient(parseInt(json["invokerid"])); + const own_channel_id = this.connection.client.getClient().currentChannel().channelId; + const channel_id = typeof(json["cid"]) !== "undefined" ? parseInt(json["cid"]) : own_channel_id; + const channel = this.connection_handler.channelTree.findChannel(channel_id) || this.connection_handler.getClient().currentChannel(); + + if(json["invokerid"] == this.connection.client.clientId) + this.connection_handler.sound.play(Sound.MESSAGE_SEND, {default_volume: .5}); + else if(channel_id == own_channel_id) { + this.connection_handler.sound.play(Sound.MESSAGE_RECEIVED, {default_volume: .5}); + } + + const conversations = this.connection_handler.side_bar.channel_conversations(); + const conversation = conversations.conversation(channel_id); + conversation.register_new_message({ + sender_database_id: invoker ? invoker.properties.client_database_id : 0, + sender_name: json["invokername"], + sender_unique_id: json["invokeruid"], + + timestamp: typeof(json["timestamp"]) === "undefined" ? Date.now() : parseInt(json["timestamp"]), + message: json["msg"] + }); + if(conversation.is_unread() && channel) + channel.flag_text_unread = true; + } else if(mode == 3) { + this.connection_handler.log.log(log.server.Type.GLOBAL_MESSAGE, { + message: json["msg"], + sender: { + client_unique_id: json["invokeruid"], + client_name: json["invokername"], + client_id: parseInt(json["invokerid"]) + } + }); + + const invoker = this.connection_handler.channelTree.findClient(parseInt(json["invokerid"])); + const conversations = this.connection_handler.side_bar.channel_conversations(); + const conversation = conversations.conversation(0); + conversation.register_new_message({ + sender_database_id: invoker ? invoker.properties.client_database_id : 0, + sender_name: json["invokername"], + sender_unique_id: json["invokeruid"], + + timestamp: typeof(json["timestamp"]) === "undefined" ? Date.now() : parseInt(json["timestamp"]), + message: json["msg"] + }); + this.connection_handler.channelTree.server.flag_text_unread = conversation.is_unread(); + } + } + + notifyClientChatComposing(json) { + json = json[0]; + const conversation_manager = this.connection_handler.side_bar.private_conversations(); const conversation = conversation_manager.find_conversation({ - client_id: target_own ? parseInt(json["invokerid"]) : target_client_id, - unique_id: target_own ? json["invokeruid"] : undefined, - name: target_own ? json["invokername"] : undefined + client_id: parseInt(json["clid"]), + unique_id: json["cluid"], + name: undefined }, { - create: target_own, - attach: target_own + create: false, + attach: false + }); + if(!conversation) + return; + + conversation.trigger_typing(); + } + + handleNotifyClientChatClosed(json) { + json = json[0]; //Only one bulk + + //Chat partner has closed the conversation + + //clid: "6" + //cluid: "YoWmG+dRGKD+Rxb7SPLAM5+B9tY=" + + const conversation_manager = this.connection_handler.side_bar.private_conversations(); + const conversation = conversation_manager.find_conversation({ + client_id: parseInt(json["clid"]), + unique_id: json["cluid"], + name: undefined + }, { + create: false, + attach: false }); if(!conversation) { - log.error(LogCategory.NETWORKING, tr("Received conversation message for unknown conversation! (%s)"), target_own ? tr("Remote message") : tr("Own message")); + log.warn(LogCategory.GENERAL, tr("Received chat close for client, but we haven't a chat open.")); + return; + } + conversation.set_state(chat.PrivateConversationState.CLOSED); + } + + handleNotifyClientUpdated(json) { + json = json[0]; //Only one bulk + + let client = this.connection.client.channelTree.findClient(json["clid"]); + if(!client) { + log.error(LogCategory.NETWORKING, tr("Tried to update an non existing client")); return; } - conversation.append_message(json["msg"], { - type: target_own ? "partner" : "self", + let updates: { + key: string, + value: string + }[] = []; + for(let key in json) { + if(key == "clid") continue; + updates.push({key: key, value: json[key]}); + } + client.updateVariables(...updates); + } + + handleNotifyServerEdited(json) { + json = json[0]; + + let updates: { + key: string, + value: string + }[] = []; + for(let key in json) { + if(key === "invokerid") continue; + if(key === "invokername") continue; + if(key === "invokeruid") continue; + if(key === "reasonid") continue; + + updates.push({key: key, value: json[key]}); + } + this.connection.client.channelTree.server.updateVariables(false, ...updates); + } + + handleNotifyServerUpdated(json) { + json = json[0]; + + let updates: { + key: string, + value: string + }[] = []; + for(let key in json) { + if(key === "invokerid") continue; + if(key === "invokername") continue; + if(key === "invokeruid") continue; + if(key === "reasonid") continue; + + updates.push({key: key, value: json[key]}); + } + this.connection.client.channelTree.server.updateVariables(true, ...updates); + } + + handleNotifyMusicPlayerInfo(json) { + json = json[0]; + + let bot = this.connection.client.channelTree.find_client_by_dbid(json["bot_id"]); + if(!bot || !(bot instanceof MusicClientEntry)) { + log.warn(LogCategory.CLIENT, tr("Got music player info for unknown or invalid bot! (ID: %i, Entry: %o)"), json["bot_id"], bot); + return; + } + + bot.handlePlayerInfo(json); + } + + handleNotifyClientPoke(json) { + json = json[0]; + Modals.spawnPoke(this.connection_handler, { + id: parseInt(json["invokerid"]), name: json["invokername"], - unique_id: json["invokeruid"], - client_id: parseInt(json["invokerid"]) - }); + unique_id: json["invokeruid"] + }, json["msg"]); - if(target_own) { - this.connection_handler.sound.play(Sound.MESSAGE_RECEIVED, {default_volume: .5}); - const client = this.connection_handler.channelTree.findClient(parseInt(json["invokerid"])); - if(client) /* the client itself might be invisible */ - client.flag_text_unread = conversation.is_unread(); + this.connection_handler.sound.play(Sound.USER_POKED_SELF); + } + + //TODO server chat message + handleNotifyServerGroupClientAdd(json) { + json = json[0]; + + const self = this.connection.client.getClient(); + if(json["clid"] == self.clientId()) + this.connection_handler.sound.play(Sound.GROUP_SERVER_ASSIGNED_SELF); + } + + //TODO server chat message + handleNotifyServerGroupClientRemove(json) { + json = json[0]; + + const self = this.connection.client.getClient(); + if(json["clid"] == self.clientId()) { + this.connection_handler.sound.play(Sound.GROUP_SERVER_REVOKED_SELF); } else { - this.connection_handler.sound.play(Sound.MESSAGE_SEND, {default_volume: .5}); } - } else if(mode == 2) { - const invoker = this.connection_handler.channelTree.findClient(parseInt(json["invokerid"])); - const own_channel_id = this.connection.client.getClient().currentChannel().channelId; - const channel_id = typeof(json["cid"]) !== "undefined" ? parseInt(json["cid"]) : own_channel_id; - const channel = this.connection_handler.channelTree.findChannel(channel_id) || this.connection_handler.getClient().currentChannel(); + } - if(json["invokerid"] == this.connection.client.clientId) - this.connection_handler.sound.play(Sound.MESSAGE_SEND, {default_volume: .5}); - else if(channel_id == own_channel_id) { - this.connection_handler.sound.play(Sound.MESSAGE_RECEIVED, {default_volume: .5}); + //TODO server chat message + handleNotifyClientChannelGroupChanged(json) { + json = json[0]; + + const self = this.connection.client.getClient(); + if(json["clid"] == self.clientId()) { + this.connection_handler.sound.play(Sound.GROUP_CHANNEL_CHANGED_SELF); + } + } + + handleNotifyChannelSubscribed(json) { + for(const entry of json) { + const channel = this.connection.client.channelTree.findChannel(entry["cid"]); + if(!channel) { + console.warn(tr("Received channel subscribed for not visible channel (cid: %d)"), entry['cid']); + continue; + } + + channel.flag_subscribed = true; + } + } + + handleNotifyChannelUnsubscribed(json) { + for(const entry of json) { + const channel = this.connection.client.channelTree.findChannel(entry["cid"]); + if(!channel) { + console.warn(tr("Received channel unsubscribed for not visible channel (cid: %d)"), entry['cid']); + continue; + } + + channel.flag_subscribed = false; + for(const client of channel.clients(false)) + this.connection.client.channelTree.deleteClient(client); + } + } + + handleNotifyConversationHistory(json: any[]) { + const conversations = this.connection.client.side_bar.channel_conversations(); + const conversation = conversations.conversation(parseInt(json[0]["cid"])); + if(!conversation) { + log.warn(LogCategory.NETWORKING, tr("Received conversation history for invalid or unknown conversation (%o)"), json[0]["cid"]); + return; } - const conversations = this.connection_handler.side_bar.channel_conversations(); - const conversation = conversations.conversation(channel_id); - conversation.register_new_message({ - sender_database_id: invoker ? invoker.properties.client_database_id : 0, - sender_name: json["invokername"], - sender_unique_id: json["invokeruid"], + for(const entry of json) { + conversation.register_new_message({ + message: entry["msg"], + sender_unique_id: entry["sender_unique_id"], + sender_name: entry["sender_name"], + timestamp: parseInt(entry["timestamp"]), + sender_database_id: parseInt(entry["sender_database_id"]) + }, false); + } - timestamp: typeof(json["timestamp"]) === "undefined" ? Date.now() : parseInt(json["timestamp"]), - message: json["msg"] + /* now update the boxes */ + /* No update needed because the command which triggers this notify should update the chat box on success + conversation.fix_scroll(true); + conversation.handle.update_chat_box(); + */ + } + + handleNotifyConversationMessageDelete(json: any[]) { + let conversation: Conversation; + const conversations = this.connection.client.side_bar.channel_conversations(); + for(const entry of json) { + if(typeof(entry["cid"]) !== "undefined") + conversation = conversations.conversation(parseInt(entry["cid"]), false); + if(!conversation) + continue; + + conversation.delete_messages(parseInt(entry["timestamp_begin"]), parseInt(entry["timestamp_end"]), parseInt(entry["cldbid"]), parseInt(entry["limit"])); + } + } + + handleNotifyMusicStatusUpdate(json: any[]) { + json = json[0]; + + const bot_id = parseInt(json["bot_id"]); + const client = this.connection.client.channelTree.find_client_by_dbid(bot_id); + if(!client) { + log.warn(LogCategory.CLIENT, tr("Received music bot status update for unknown bot (%d)"), bot_id); + return; + } + + client.events.fire("music_status_update", { + player_replay_index: parseInt(json["player_replay_index"]), + player_buffered_index: parseInt(json["player_buffered_index"]) }); - if(conversation.is_unread() && channel) - channel.flag_text_unread = true; - } else if(mode == 3) { - this.connection_handler.log.log(server.Type.GLOBAL_MESSAGE, { - message: json["msg"], - sender: { - client_unique_id: json["invokeruid"], - client_name: json["invokername"], - client_id: parseInt(json["invokerid"]) + } + + handleMusicPlayerSongChange(json: any[]) { + json = json[0]; + + const bot_id = parseInt(json["bot_id"]); + const client = this.connection.client.channelTree.find_client_by_dbid(bot_id); + if(!client) { + log.warn(LogCategory.CLIENT, tr("Received music bot status update for unknown bot (%d)"), bot_id); + return; + } + + const song_id = parseInt(json["song_id"]); + let song: SongInfo; + if(song_id) { + song = new SongInfo(); + JSON.map_to(song, json); + } + + client.events.fire("music_song_change", { + song: song + }); + } + + handleNotifyPlaylistSongAdd(json: any[]) { + json = json[0]; + + const playlist_id = parseInt(json["playlist_id"]); + const client = this.connection.client.channelTree.clients.find(e => e instanceof MusicClientEntry && e.properties.client_playlist_id === playlist_id); + if(!client) { + log.warn(LogCategory.CLIENT, tr("Received playlist song add event, but we've no music bot for the playlist (%d)"), playlist_id); + return; + } + + client.events.fire("playlist_song_add", { + song: { + song_id: parseInt(json["song_id"]), + song_invoker: json["song_invoker"], + song_previous_song_id: parseInt(json["song_previous_song_id"]), + song_url: json["song_url"], + song_url_loader: json["song_url_loader"], + + song_loaded: json["song_loaded"] == true || json["song_loaded"] == "1", + song_metadata: json["song_metadata"] } }); + } - const invoker = this.connection_handler.channelTree.findClient(parseInt(json["invokerid"])); - const conversations = this.connection_handler.side_bar.channel_conversations(); - const conversation = conversations.conversation(0); - conversation.register_new_message({ - sender_database_id: invoker ? invoker.properties.client_database_id : 0, - sender_name: json["invokername"], - sender_unique_id: json["invokeruid"], + handleNotifyPlaylistSongRemove(json: any[]) { + json = json[0]; - timestamp: typeof(json["timestamp"]) === "undefined" ? Date.now() : parseInt(json["timestamp"]), - message: json["msg"] + const playlist_id = parseInt(json["playlist_id"]); + const client = this.connection.client.channelTree.clients.find(e => e instanceof MusicClientEntry && e.properties.client_playlist_id === playlist_id); + if(!client) { + log.warn(LogCategory.CLIENT, tr("Received playlist song remove event, but we've no music bot for the playlist (%d)"), playlist_id); + return; + } + + const song_id = parseInt(json["song_id"]); + client.events.fire("playlist_song_remove", { song_id: song_id }); + } + + handleNotifyPlaylistSongReorder(json: any[]) { + json = json[0]; + + const playlist_id = parseInt(json["playlist_id"]); + const client = this.connection.client.channelTree.clients.find(e => e instanceof MusicClientEntry && e.properties.client_playlist_id === playlist_id); + if(!client) { + log.warn(LogCategory.CLIENT, tr("Received playlist song reorder event, but we've no music bot for the playlist (%d)"), playlist_id); + return; + } + + const song_id = parseInt(json["song_id"]); + const previous_song_id = parseInt(json["song_previous_song_id"]); + client.events.fire("playlist_song_reorder", { song_id: song_id, previous_song_id: previous_song_id }); + } + + handleNotifyPlaylistSongLoaded(json: any[]) { + json = json[0]; + + const playlist_id = parseInt(json["playlist_id"]); + const client = this.connection.client.channelTree.clients.find(e => e instanceof MusicClientEntry && e.properties.client_playlist_id === playlist_id); + if(!client) { + log.warn(LogCategory.CLIENT, tr("Received playlist song loaded event, but we've no music bot for the playlist (%d)"), playlist_id); + return; + } + + const song_id = parseInt(json["song_id"]); + client.events.fire("playlist_song_loaded", { + song_id: song_id, + success: json["success"] == 1, + error_msg: json["load_error_msg"], + metadata: json["song_metadata"] }); - this.connection_handler.channelTree.server.flag_text_unread = conversation.is_unread(); } } - - notifyClientChatComposing(json) { - json = json[0]; - - const conversation_manager = this.connection_handler.side_bar.private_conversations(); - const conversation = conversation_manager.find_conversation({ - client_id: parseInt(json["clid"]), - unique_id: json["cluid"], - name: undefined - }, { - create: false, - attach: false - }); - if(!conversation) - return; - - conversation.trigger_typing(); - } - - handleNotifyClientChatClosed(json) { - json = json[0]; //Only one bulk - - //Chat partner has closed the conversation - - //clid: "6" - //cluid: "YoWmG+dRGKD+Rxb7SPLAM5+B9tY=" - - const conversation_manager = this.connection_handler.side_bar.private_conversations(); - const conversation = conversation_manager.find_conversation({ - client_id: parseInt(json["clid"]), - unique_id: json["cluid"], - name: undefined - }, { - create: false, - attach: false - }); - if(!conversation) { - log.warn(LogCategory.GENERAL, tr("Received chat close for client, but we haven't a chat open.")); - return; - } - conversation.set_state(pchat.PrivateConversationState.CLOSED); - } - - handleNotifyClientUpdated(json) { - json = json[0]; //Only one bulk - - let client = this.connection.client.channelTree.findClient(json["clid"]); - if(!client) { - log.error(LogCategory.NETWORKING, tr("Tried to update an non existing client")); - return; - } - - let updates: { - key: string, - value: string - }[] = []; - for(let key in json) { - if(key == "clid") continue; - updates.push({key: key, value: json[key]}); - } - client.updateVariables(...updates); - } - - handleNotifyServerEdited(json) { - json = json[0]; - - let updates: { - key: string, - value: string - }[] = []; - for(let key in json) { - if(key === "invokerid") continue; - if(key === "invokername") continue; - if(key === "invokeruid") continue; - if(key === "reasonid") continue; - - updates.push({key: key, value: json[key]}); - } - this.connection.client.channelTree.server.updateVariables(false, ...updates); - } - - handleNotifyServerUpdated(json) { - json = json[0]; - - let updates: { - key: string, - value: string - }[] = []; - for(let key in json) { - if(key === "invokerid") continue; - if(key === "invokername") continue; - if(key === "invokeruid") continue; - if(key === "reasonid") continue; - - updates.push({key: key, value: json[key]}); - } - this.connection.client.channelTree.server.updateVariables(true, ...updates); - } - - handleNotifyMusicPlayerInfo(json) { - json = json[0]; - - let bot = this.connection.client.channelTree.find_client_by_dbid(json["bot_id"]); - if(!bot || !(bot instanceof MusicClientEntry)) { - log.warn(LogCategory.CLIENT, tr("Got music player info for unknown or invalid bot! (ID: %i, Entry: %o)"), json["bot_id"], bot); - return; - } - - bot.handlePlayerInfo(json); - } - - handleNotifyClientPoke(json) { - json = json[0]; - Modals.spawnPoke(this.connection_handler, { - id: parseInt(json["invokerid"]), - name: json["invokername"], - unique_id: json["invokeruid"] - }, json["msg"]); - - this.connection_handler.sound.play(Sound.USER_POKED_SELF); - } - - //TODO server chat message - handleNotifyServerGroupClientAdd(json) { - json = json[0]; - - const self = this.connection.client.getClient(); - if(json["clid"] == self.clientId()) - this.connection_handler.sound.play(Sound.GROUP_SERVER_ASSIGNED_SELF); - } - - //TODO server chat message - handleNotifyServerGroupClientRemove(json) { - json = json[0]; - - const self = this.connection.client.getClient(); - if(json["clid"] == self.clientId()) { - this.connection_handler.sound.play(Sound.GROUP_SERVER_REVOKED_SELF); - } else { - } - } - - //TODO server chat message - handleNotifyClientChannelGroupChanged(json) { - json = json[0]; - - const self = this.connection.client.getClient(); - if(json["clid"] == self.clientId()) { - this.connection_handler.sound.play(Sound.GROUP_CHANNEL_CHANGED_SELF); - } - } - - handleNotifyChannelSubscribed(json) { - for(const entry of json) { - const channel = this.connection.client.channelTree.findChannel(entry["cid"]); - if(!channel) { - console.warn(tr("Received channel subscribed for not visible channel (cid: %d)"), entry['cid']); - continue; - } - - channel.flag_subscribed = true; - } - } - - handleNotifyChannelUnsubscribed(json) { - for(const entry of json) { - const channel = this.connection.client.channelTree.findChannel(entry["cid"]); - if(!channel) { - console.warn(tr("Received channel unsubscribed for not visible channel (cid: %d)"), entry['cid']); - continue; - } - - channel.flag_subscribed = false; - for(const client of channel.clients(false)) - this.connection.client.channelTree.deleteClient(client); - } - } - - handleNotifyConversationHistory(json: any[]) { - const conversations = this.connection.client.side_bar.channel_conversations(); - const conversation = conversations.conversation(parseInt(json[0]["cid"])); - if(!conversation) { - log.warn(LogCategory.NETWORKING, tr("Received conversation history for invalid or unknown conversation (%o)"), json[0]["cid"]); - return; - } - - for(const entry of json) { - conversation.register_new_message({ - message: entry["msg"], - sender_unique_id: entry["sender_unique_id"], - sender_name: entry["sender_name"], - timestamp: parseInt(entry["timestamp"]), - sender_database_id: parseInt(entry["sender_database_id"]) - }, false); - } - - /* now update the boxes */ - /* No update needed because the command which triggers this notify should update the chat box on success - conversation.fix_scroll(true); - conversation.handle.update_chat_box(); - */ - } - - handleNotifyConversationMessageDelete(json: any[]) { - let conversation: Conversation; - const conversations = this.connection.client.side_bar.channel_conversations(); - for(const entry of json) { - if(typeof(entry["cid"]) !== "undefined") - conversation = conversations.conversation(parseInt(entry["cid"]), false); - if(!conversation) - continue; - - conversation.delete_messages(parseInt(entry["timestamp_begin"]), parseInt(entry["timestamp_end"]), parseInt(entry["cldbid"]), parseInt(entry["limit"])); - } - } - - handleNotifyMusicStatusUpdate(json: any[]) { - json = json[0]; - - const bot_id = parseInt(json["bot_id"]); - const client = this.connection.client.channelTree.find_client_by_dbid(bot_id); - if(!client) { - log.warn(LogCategory.CLIENT, tr("Received music bot status update for unknown bot (%d)"), bot_id); - return; - } - - client.events.fire("music_status_update", { - player_replay_index: parseInt(json["player_replay_index"]), - player_buffered_index: parseInt(json["player_buffered_index"]) - }); - } - - handleMusicPlayerSongChange(json: any[]) { - json = json[0]; - - const bot_id = parseInt(json["bot_id"]); - const client = this.connection.client.channelTree.find_client_by_dbid(bot_id); - if(!client) { - log.warn(LogCategory.CLIENT, tr("Received music bot status update for unknown bot (%d)"), bot_id); - return; - } - - const song_id = parseInt(json["song_id"]); - let song: SongInfo; - if(song_id) { - song = new SongInfo(); - JSON.map_to(song, json); - } - - client.events.fire("music_song_change", { - song: song - }); - } - - handleNotifyPlaylistSongAdd(json: any[]) { - json = json[0]; - - const playlist_id = parseInt(json["playlist_id"]); - const client = this.connection.client.channelTree.clients.find(e => e instanceof MusicClientEntry && e.properties.client_playlist_id === playlist_id); - if(!client) { - log.warn(LogCategory.CLIENT, tr("Received playlist song add event, but we've no music bot for the playlist (%d)"), playlist_id); - return; - } - - client.events.fire("playlist_song_add", { - song: { - song_id: parseInt(json["song_id"]), - song_invoker: json["song_invoker"], - song_previous_song_id: parseInt(json["song_previous_song_id"]), - song_url: json["song_url"], - song_url_loader: json["song_url_loader"], - - song_loaded: json["song_loaded"] == true || json["song_loaded"] == "1", - song_metadata: json["song_metadata"] - } - }); - } - - handleNotifyPlaylistSongRemove(json: any[]) { - json = json[0]; - - const playlist_id = parseInt(json["playlist_id"]); - const client = this.connection.client.channelTree.clients.find(e => e instanceof MusicClientEntry && e.properties.client_playlist_id === playlist_id); - if(!client) { - log.warn(LogCategory.CLIENT, tr("Received playlist song remove event, but we've no music bot for the playlist (%d)"), playlist_id); - return; - } - - const song_id = parseInt(json["song_id"]); - client.events.fire("playlist_song_remove", { song_id: song_id }); - } - - handleNotifyPlaylistSongReorder(json: any[]) { - json = json[0]; - - const playlist_id = parseInt(json["playlist_id"]); - const client = this.connection.client.channelTree.clients.find(e => e instanceof MusicClientEntry && e.properties.client_playlist_id === playlist_id); - if(!client) { - log.warn(LogCategory.CLIENT, tr("Received playlist song reorder event, but we've no music bot for the playlist (%d)"), playlist_id); - return; - } - - const song_id = parseInt(json["song_id"]); - const previous_song_id = parseInt(json["song_previous_song_id"]); - client.events.fire("playlist_song_reorder", { song_id: song_id, previous_song_id: previous_song_id }); - } - - handleNotifyPlaylistSongLoaded(json: any[]) { - json = json[0]; - - const playlist_id = parseInt(json["playlist_id"]); - const client = this.connection.client.channelTree.clients.find(e => e instanceof MusicClientEntry && e.properties.client_playlist_id === playlist_id); - if(!client) { - log.warn(LogCategory.CLIENT, tr("Received playlist song loaded event, but we've no music bot for the playlist (%d)"), playlist_id); - return; - } - - const song_id = parseInt(json["song_id"]); - client.events.fire("playlist_song_loaded", { - song_id: song_id, - success: json["success"] == 1, - error_msg: json["load_error_msg"], - metadata: json["song_metadata"] - }); - } } \ No newline at end of file diff --git a/shared/js/connection/CommandHelper.ts b/shared/js/connection/CommandHelper.ts index 21203bb2..9eff210b 100644 --- a/shared/js/connection/CommandHelper.ts +++ b/shared/js/connection/CommandHelper.ts @@ -1,460 +1,448 @@ -import { - ClientNameInfo, - CommandResult, - ErrorID, - Playlist, PlaylistInfo, PlaylistSong, - QueryList, - QueryListEntry, ServerGroupClient -} from "./ServerConnectionDeclaration"; -import {ChannelEntry} from "../channel-tree/channel"; -import {ChatType} from "../ui/frames/chat"; -import {ClientEntry} from "../channel-tree/client"; -import {AbstractCommandHandler, ServerCommand, SingleCommandHandler} from "./ConnectionBase"; -import {log, LogCategory} from "../log"; +namespace connection { + export class CommandHelper extends AbstractCommandHandler { + private _who_am_i: any; + private _awaiters_unique_ids: {[unique_id: string]:((resolved: ClientNameInfo) => any)[]} = {}; + private _awaiters_unique_dbid: {[database_id: number]:((resolved: ClientNameInfo) => any)[]} = {}; -export class CommandHelper extends AbstractCommandHandler { - private _who_am_i: any; - private _awaiters_unique_ids: {[unique_id: string]:((resolved: ClientNameInfo) => any)[]} = {}; - private _awaiters_unique_dbid: {[database_id: number]:((resolved: ClientNameInfo) => any)[]} = {}; + constructor(connection) { + super(connection); - constructor(connection) { - super(connection); - - this.volatile_handler_boss = false; - this.ignore_consumed = true; - } - - initialize() { - this.connection.command_handler_boss().register_handler(this); - } - - destroy() { - if(this.connection) { - const hboss = this.connection.command_handler_boss(); - hboss && hboss.unregister_handler(this); - } - this._awaiters_unique_ids = undefined; - } - - handle_command(command: ServerCommand): boolean { - if(command.command == "notifyclientnamefromuid") - this.handle_notifyclientnamefromuid(command.arguments); - if(command.command == "notifyclientgetnamefromdbid") - this.handle_notifyclientgetnamefromdbid(command.arguments); - else - return false; - return true; - } - - joinChannel(channel: ChannelEntry, password?: string) : Promise { - return this.connection.send_command("clientmove", { - "clid": this.connection.client.getClientId(), - "cid": channel.getChannelId(), - "cpw": password || "" - }); - } - - sendMessage(message: string, type: ChatType, target?: ChannelEntry | ClientEntry) : Promise { - if(type == ChatType.SERVER) - return this.connection.send_command("sendtextmessage", {"targetmode": 3, "target": 0, "msg": message}); - else if(type == ChatType.CHANNEL) - return this.connection.send_command("sendtextmessage", {"targetmode": 2, "target": (target as ChannelEntry).getChannelId(), "msg": message}); - else if(type == ChatType.CLIENT) - return this.connection.send_command("sendtextmessage", {"targetmode": 1, "target": (target as ClientEntry).clientId(), "msg": message}); - } - - updateClient(key: string, value: string) : Promise { - let data = {}; - data[key] = value; - return this.connection.send_command("clientupdate", data); - } - - async info_from_uid(..._unique_ids: string[]) : Promise { - const response: ClientNameInfo[] = []; - const request = []; - const unique_ids = new Set(_unique_ids); - if(!unique_ids.size) return []; - - const unique_id_resolvers: {[unique_id: string]: (resolved: ClientNameInfo) => any} = {}; - - - for(const unique_id of unique_ids) { - request.push({'cluid': unique_id}); - (this._awaiters_unique_ids[unique_id] || (this._awaiters_unique_ids[unique_id] = [])) - .push(unique_id_resolvers[unique_id] = info => response.push(info)); + this.volatile_handler_boss = false; + this.ignore_consumed = true; } - try { - await this.connection.send_command("clientgetnamefromuid", request); - } catch(error) { - if(error instanceof CommandResult && error.id == ErrorID.EMPTY_RESULT) { - /* nothing */ - } else { - throw error; + initialize() { + this.connection.command_handler_boss().register_handler(this); + } + + destroy() { + if(this.connection) { + const hboss = this.connection.command_handler_boss(); + hboss && hboss.unregister_handler(this); } - } finally { - /* cleanup */ - for(const unique_id of Object.keys(unique_id_resolvers)) - (this._awaiters_unique_ids[unique_id] || []).remove(unique_id_resolvers[unique_id]); + this._awaiters_unique_ids = undefined; } - return response; - } - - private handle_notifyclientgetnamefromdbid(json: any[]) { - for(const entry of json) { - const info: ClientNameInfo = { - client_unique_id: entry["cluid"], - client_nickname: entry["clname"], - client_database_id: parseInt(entry["cldbid"]) - }; - - const functions = this._awaiters_unique_dbid[info.client_database_id] || []; - delete this._awaiters_unique_dbid[info.client_database_id]; - - for(const fn of functions) - fn(info); - } - } - - async info_from_cldbid(..._cldbid: number[]) : Promise { - const response: ClientNameInfo[] = []; - const request = []; - const unique_cldbid = new Set(_cldbid); - if(!unique_cldbid.size) return []; - - const unique_cldbid_resolvers: {[dbid: number]: (resolved: ClientNameInfo) => any} = {}; - - - for(const cldbid of unique_cldbid) { - request.push({'cldbid': cldbid}); - (this._awaiters_unique_dbid[cldbid] || (this._awaiters_unique_dbid[cldbid] = [])) - .push(unique_cldbid_resolvers[cldbid] = info => response.push(info)); + handle_command(command: connection.ServerCommand): boolean { + if(command.command == "notifyclientnamefromuid") + this.handle_notifyclientnamefromuid(command.arguments); + if(command.command == "notifyclientgetnamefromdbid") + this.handle_notifyclientgetnamefromdbid(command.arguments); + else + return false; + return true; } - try { - await this.connection.send_command("clientgetnamefromdbid", request); - } catch(error) { - if(error instanceof CommandResult && error.id == ErrorID.EMPTY_RESULT) { - /* nothing */ - } else { - throw error; - } - } finally { - /* cleanup */ - for(const cldbid of Object.keys(unique_cldbid_resolvers)) - (this._awaiters_unique_dbid[cldbid] || []).remove(unique_cldbid_resolvers[cldbid]); + joinChannel(channel: ChannelEntry, password?: string) : Promise { + return this.connection.send_command("clientmove", { + "clid": this.connection.client.getClientId(), + "cid": channel.getChannelId(), + "cpw": password || "" + }); } - return response; - } - - private handle_notifyclientnamefromuid(json: any[]) { - for(const entry of json) { - const info: ClientNameInfo = { - client_unique_id: entry["cluid"], - client_nickname: entry["clname"], - client_database_id: parseInt(entry["cldbid"]) - }; - - const functions = this._awaiters_unique_ids[entry["cluid"]] || []; - delete this._awaiters_unique_ids[entry["cluid"]]; - - for(const fn of functions) - fn(info); + sendMessage(message: string, type: ChatType, target?: ChannelEntry | ClientEntry) : Promise { + if(type == ChatType.SERVER) + return this.connection.send_command("sendtextmessage", {"targetmode": 3, "target": 0, "msg": message}); + else if(type == ChatType.CHANNEL) + return this.connection.send_command("sendtextmessage", {"targetmode": 2, "target": (target as ChannelEntry).getChannelId(), "msg": message}); + else if(type == ChatType.CLIENT) + return this.connection.send_command("sendtextmessage", {"targetmode": 1, "target": (target as ClientEntry).clientId(), "msg": message}); } - } - - request_query_list(server_id: number = undefined) : Promise { - return new Promise((resolve, reject) => { - const single_handler = { - command: "notifyquerylist", - function: command => { - const json = command.arguments; - - const result = {} as QueryList; - - result.flag_all = json[0]["flag_all"]; - result.flag_own = json[0]["flag_own"]; - result.queries = []; - - for(const entry of json) { - const rentry = {} as QueryListEntry; - rentry.bounded_server = parseInt(entry["client_bound_server"]); - rentry.username = entry["client_login_name"]; - rentry.unique_id = entry["client_unique_identifier"]; - - result.queries.push(rentry); - } - - resolve(result); - return true; - } - }; - this.handler_boss.register_single_handler(single_handler); + updateClient(key: string, value: string) : Promise { let data = {}; - if(server_id !== undefined) - data["server_id"] = server_id; + data[key] = value; + return this.connection.send_command("clientupdate", data); + } - this.connection.send_command("querylist", data).catch(error => { - this.handler_boss.remove_single_handler(single_handler); + async info_from_uid(..._unique_ids: string[]) : Promise { + const response: ClientNameInfo[] = []; + const request = []; + const unique_ids = new Set(_unique_ids); + if(!unique_ids.size) return []; - if(error instanceof CommandResult) { - if(error.id == ErrorID.EMPTY_RESULT) { - resolve(undefined); - return; - } - } - reject(error); - }); - }); - } + const unique_id_resolvers: {[unique_id: string]: (resolved: ClientNameInfo) => any} = {}; - request_playlist_list() : Promise { - return new Promise((resolve, reject) => { - const single_handler: SingleCommandHandler = { - command: "notifyplaylistlist", - function: command => { - const json = command.arguments; - const result: Playlist[] = []; - for(const entry of json) { - try { - result.push({ - playlist_id: parseInt(entry["playlist_id"]), - playlist_bot_id: parseInt(entry["playlist_bot_id"]), - playlist_title: entry["playlist_title"], - playlist_type: parseInt(entry["playlist_type"]), - playlist_owner_dbid: parseInt(entry["playlist_owner_dbid"]), - playlist_owner_name: entry["playlist_owner_name"], + for(const unique_id of unique_ids) { + request.push({'cluid': unique_id}); + (this._awaiters_unique_ids[unique_id] || (this._awaiters_unique_ids[unique_id] = [])) + .push(unique_id_resolvers[unique_id] = info => response.push(info)); + } - needed_power_modify: parseInt(entry["needed_power_modify"]), - needed_power_permission_modify: parseInt(entry["needed_power_permission_modify"]), - needed_power_delete: parseInt(entry["needed_power_delete"]), - needed_power_song_add: parseInt(entry["needed_power_song_add"]), - needed_power_song_move: parseInt(entry["needed_power_song_move"]), - needed_power_song_remove: parseInt(entry["needed_power_song_remove"]) - }); - } catch(error) { - log.error(LogCategory.NETWORKING, tr("Failed to parse playlist entry: %o"), error); - } - } - - resolve(result); - return true; - } - }; - this.handler_boss.register_single_handler(single_handler); - - this.connection.send_command("playlistlist").catch(error => { - this.handler_boss.remove_single_handler(single_handler); - - if(error instanceof CommandResult) { - if(error.id == ErrorID.EMPTY_RESULT) { - resolve([]); - return; - } - } - reject(error); - }) - }); - } - - request_playlist_songs(playlist_id: number) : Promise { - return new Promise((resolve, reject) => { - const single_handler: SingleCommandHandler = { - command: "notifyplaylistsonglist", - function: command => { - const json = command.arguments; - - if(json[0]["playlist_id"] != playlist_id) { - log.error(LogCategory.NETWORKING, tr("Received invalid notification for playlist songs")); - return false; - } - - const result: PlaylistSong[] = []; - - for(const entry of json) { - try { - result.push({ - song_id: parseInt(entry["song_id"]), - song_invoker: entry["song_invoker"], - song_previous_song_id: parseInt(entry["song_previous_song_id"]), - song_url: entry["song_url"], - song_url_loader: entry["song_url_loader"], - - song_loaded: entry["song_loaded"] == true || entry["song_loaded"] == "1", - song_metadata: entry["song_metadata"] - }); - } catch(error) { - log.error(LogCategory.NETWORKING, tr("Failed to parse playlist song entry: %o"), error); - } - } - - resolve(result); - return true; - } - }; - this.handler_boss.register_single_handler(single_handler); - - this.connection.send_command("playlistsonglist", {playlist_id: playlist_id}).catch(error => { - this.handler_boss.remove_single_handler(single_handler); - if(error instanceof CommandResult) { - if(error.id == ErrorID.EMPTY_RESULT) { - resolve([]); - return; - } - } - reject(error); - }) - }); - } - - request_playlist_client_list(playlist_id: number) : Promise { - return new Promise((resolve, reject) => { - const single_handler: SingleCommandHandler = { - command: "notifyplaylistclientlist", - function: command => { - const json = command.arguments; - - if(json[0]["playlist_id"] != playlist_id) { - log.error(LogCategory.NETWORKING, tr("Received invalid notification for playlist clients")); - return false; - } - - const result: number[] = []; - - for(const entry of json) - result.push(parseInt(entry["cldbid"])); - - resolve(result.filter(e => !isNaN(e))); - return true; - } - }; - this.handler_boss.register_single_handler(single_handler); - - this.connection.send_command("playlistclientlist", {playlist_id: playlist_id}).catch(error => { - this.handler_boss.remove_single_handler(single_handler); + try { + await this.connection.send_command("clientgetnamefromuid", request); + } catch(error) { if(error instanceof CommandResult && error.id == ErrorID.EMPTY_RESULT) { - resolve([]); - return; + /* nothing */ + } else { + throw error; } - reject(error); - }) - }); - } + } finally { + /* cleanup */ + for(const unique_id of Object.keys(unique_id_resolvers)) + (this._awaiters_unique_ids[unique_id] || []).remove(unique_id_resolvers[unique_id]); + } - async request_clients_by_server_group(group_id: number) : Promise { - //servergroupclientlist sgid=2 - //notifyservergroupclientlist sgid=6 cldbid=2 client_nickname=WolverinDEV client_unique_identifier=xxjnc14LmvTk+Lyrm8OOeo4tOqw= - return new Promise((resolve, reject) => { - const single_handler: SingleCommandHandler = { - command: "notifyservergroupclientlist", - function: command => { - if (command.arguments[0]["sgid"] != group_id) { - log.error(LogCategory.NETWORKING, tr("Received invalid notification for server group client list")); - return false; - } + return response; + } + + private handle_notifyclientgetnamefromdbid(json: any[]) { + for(const entry of json) { + const info: ClientNameInfo = { + client_unique_id: entry["cluid"], + client_nickname: entry["clname"], + client_database_id: parseInt(entry["cldbid"]) + }; + + const functions = this._awaiters_unique_dbid[info.client_database_id] || []; + delete this._awaiters_unique_dbid[info.client_database_id]; + + for(const fn of functions) + fn(info); + } + } + + async info_from_cldbid(..._cldbid: number[]) : Promise { + const response: ClientNameInfo[] = []; + const request = []; + const unique_cldbid = new Set(_cldbid); + if(!unique_cldbid.size) return []; + + const unique_cldbid_resolvers: {[dbid: number]: (resolved: ClientNameInfo) => any} = {}; + + + for(const cldbid of unique_cldbid) { + request.push({'cldbid': cldbid}); + (this._awaiters_unique_dbid[cldbid] || (this._awaiters_unique_dbid[cldbid] = [])) + .push(unique_cldbid_resolvers[cldbid] = info => response.push(info)); + } + + try { + await this.connection.send_command("clientgetnamefromdbid", request); + } catch(error) { + if(error instanceof CommandResult && error.id == ErrorID.EMPTY_RESULT) { + /* nothing */ + } else { + throw error; + } + } finally { + /* cleanup */ + for(const cldbid of Object.keys(unique_cldbid_resolvers)) + (this._awaiters_unique_dbid[cldbid] || []).remove(unique_cldbid_resolvers[cldbid]); + } + + return response; + } + + private handle_notifyclientnamefromuid(json: any[]) { + for(const entry of json) { + const info: ClientNameInfo = { + client_unique_id: entry["cluid"], + client_nickname: entry["clname"], + client_database_id: parseInt(entry["cldbid"]) + }; + + const functions = this._awaiters_unique_ids[entry["cluid"]] || []; + delete this._awaiters_unique_ids[entry["cluid"]]; + + for(const fn of functions) + fn(info); + } + } + + request_query_list(server_id: number = undefined) : Promise { + return new Promise((resolve, reject) => { + const single_handler = { + command: "notifyquerylist", + function: command => { + const json = command.arguments; + + const result = {} as QueryList; + + result.flag_all = json[0]["flag_all"]; + result.flag_own = json[0]["flag_own"]; + result.queries = []; + + for(const entry of json) { + const rentry = {} as QueryListEntry; + rentry.bounded_server = parseInt(entry["client_bound_server"]); + rentry.username = entry["client_login_name"]; + rentry.unique_id = entry["client_unique_identifier"]; + + result.queries.push(rentry); + } - try { - const result: ServerGroupClient[] = []; - for(const entry of command.arguments) - result.push({ - client_database_id: parseInt(entry["cldbid"]), - client_nickname: entry["client_nickname"], - client_unique_identifier: entry["client_unique_identifier"] - }); resolve(result); - } catch (error) { - log.error(LogCategory.NETWORKING, tr("Failed to parse server group client list: %o"), error); - reject("failed to parse info"); + return true; } + }; + this.handler_boss.register_single_handler(single_handler); - return true; - } - }; - this.handler_boss.register_single_handler(single_handler); + let data = {}; + if(server_id !== undefined) + data["server_id"] = server_id; - this.connection.send_command("servergroupclientlist", {sgid: group_id}).catch(error => { - this.handler_boss.remove_single_handler(single_handler); - reject(error); - }) - }); - } + this.connection.send_command("querylist", data).catch(error => { + this.handler_boss.remove_single_handler(single_handler); - request_playlist_info(playlist_id: number) : Promise { - return new Promise((resolve, reject) => { - const single_handler: SingleCommandHandler = { - command: "notifyplaylistinfo", - function: command => { - const json = command.arguments[0]; - if (json["playlist_id"] != playlist_id) { - log.error(LogCategory.NETWORKING, tr("Received invalid notification for playlist info")); + if(error instanceof CommandResult) { + if(error.id == ErrorID.EMPTY_RESULT) { + resolve(undefined); + return; + } + } + reject(error); + }); + }); + } + + request_playlist_list() : Promise { + return new Promise((resolve, reject) => { + const single_handler: SingleCommandHandler = { + command: "notifyplaylistlist", + function: command => { + const json = command.arguments; + const result: Playlist[] = []; + + for(const entry of json) { + try { + result.push({ + playlist_id: parseInt(entry["playlist_id"]), + playlist_bot_id: parseInt(entry["playlist_bot_id"]), + playlist_title: entry["playlist_title"], + playlist_type: parseInt(entry["playlist_type"]), + playlist_owner_dbid: parseInt(entry["playlist_owner_dbid"]), + playlist_owner_name: entry["playlist_owner_name"], + + needed_power_modify: parseInt(entry["needed_power_modify"]), + needed_power_permission_modify: parseInt(entry["needed_power_permission_modify"]), + needed_power_delete: parseInt(entry["needed_power_delete"]), + needed_power_song_add: parseInt(entry["needed_power_song_add"]), + needed_power_song_move: parseInt(entry["needed_power_song_move"]), + needed_power_song_remove: parseInt(entry["needed_power_song_remove"]) + }); + } catch(error) { + log.error(LogCategory.NETWORKING, tr("Failed to parse playlist entry: %o"), error); + } + } + + resolve(result); + return true; + } + }; + this.handler_boss.register_single_handler(single_handler); + + this.connection.send_command("playlistlist").catch(error => { + this.handler_boss.remove_single_handler(single_handler); + + if(error instanceof CommandResult) { + if(error.id == ErrorID.EMPTY_RESULT) { + resolve([]); + return; + } + } + reject(error); + }) + }); + } + + request_playlist_songs(playlist_id: number) : Promise { + return new Promise((resolve, reject) => { + const single_handler: SingleCommandHandler = { + command: "notifyplaylistsonglist", + function: command => { + const json = command.arguments; + + if(json[0]["playlist_id"] != playlist_id) { + log.error(LogCategory.NETWORKING, tr("Received invalid notification for playlist songs")); + return false; + } + + const result: PlaylistSong[] = []; + + for(const entry of json) { + try { + result.push({ + song_id: parseInt(entry["song_id"]), + song_invoker: entry["song_invoker"], + song_previous_song_id: parseInt(entry["song_previous_song_id"]), + song_url: entry["song_url"], + song_url_loader: entry["song_url_loader"], + + song_loaded: entry["song_loaded"] == true || entry["song_loaded"] == "1", + song_metadata: entry["song_metadata"] + }); + } catch(error) { + log.error(LogCategory.NETWORKING, tr("Failed to parse playlist song entry: %o"), error); + } + } + + resolve(result); + return true; + } + }; + this.handler_boss.register_single_handler(single_handler); + + this.connection.send_command("playlistsonglist", {playlist_id: playlist_id}).catch(error => { + this.handler_boss.remove_single_handler(single_handler); + if(error instanceof CommandResult) { + if(error.id == ErrorID.EMPTY_RESULT) { + resolve([]); + return; + } + } + reject(error); + }) + }); + } + + request_playlist_client_list(playlist_id: number) : Promise { + return new Promise((resolve, reject) => { + const single_handler: SingleCommandHandler = { + command: "notifyplaylistclientlist", + function: command => { + const json = command.arguments; + + if(json[0]["playlist_id"] != playlist_id) { + log.error(LogCategory.NETWORKING, tr("Received invalid notification for playlist clients")); + return false; + } + + const result: number[] = []; + + for(const entry of json) + result.push(parseInt(entry["cldbid"])); + + resolve(result.filter(e => !isNaN(e))); + return true; + } + }; + this.handler_boss.register_single_handler(single_handler); + + this.connection.send_command("playlistclientlist", {playlist_id: playlist_id}).catch(error => { + this.handler_boss.remove_single_handler(single_handler); + if(error instanceof CommandResult && error.id == ErrorID.EMPTY_RESULT) { + resolve([]); return; } - - try { - //resolve - resolve({ - playlist_id: parseInt(json["playlist_id"]), - playlist_title: json["playlist_title"], - playlist_description: json["playlist_description"], - playlist_type: parseInt(json["playlist_type"]), - - playlist_owner_dbid: parseInt(json["playlist_owner_dbid"]), - playlist_owner_name: json["playlist_owner_name"], - - playlist_flag_delete_played: json["playlist_flag_delete_played"] == true || json["playlist_flag_delete_played"] == "1", - playlist_flag_finished: json["playlist_flag_finished"] == true || json["playlist_flag_finished"] == "1", - playlist_replay_mode: parseInt(json["playlist_replay_mode"]), - playlist_current_song_id: parseInt(json["playlist_current_song_id"]), - - playlist_max_songs: parseInt(json["playlist_max_songs"]) - }); - } catch (error) { - log.error(LogCategory.NETWORKING, tr("Failed to parse playlist info: %o"), error); - reject("failed to parse info"); - } - - return true; - } - }; - this.handler_boss.register_single_handler(single_handler); - - this.connection.send_command("playlistinfo", {playlist_id: playlist_id}).catch(error => { - this.handler_boss.remove_single_handler(single_handler); - reject(error); - }) - }); - } - - /** - * @deprecated - * Its just a workaround for the query management. - * There is no garante that the whoami trick will work forever - */ - current_virtual_server_id() : Promise { - if(this._who_am_i) - return Promise.resolve(parseInt(this._who_am_i["virtualserver_id"])); - - return new Promise((resolve, reject) => { - const single_handler: SingleCommandHandler = { - function: command => { - if(command.command != "" && command.command.indexOf("=") == -1) - return false; - - this._who_am_i = command.arguments[0]; - resolve(parseInt(this._who_am_i["virtualserver_id"])); - return true; - } - }; - this.handler_boss.register_single_handler(single_handler); - - this.connection.send_command("whoami").catch(error => { - this.handler_boss.remove_single_handler(single_handler); - reject(error); + reject(error); + }) }); - }); + } + + async request_clients_by_server_group(group_id: number) : Promise { + //servergroupclientlist sgid=2 + //notifyservergroupclientlist sgid=6 cldbid=2 client_nickname=WolverinDEV client_unique_identifier=xxjnc14LmvTk+Lyrm8OOeo4tOqw= + return new Promise((resolve, reject) => { + const single_handler: SingleCommandHandler = { + command: "notifyservergroupclientlist", + function: command => { + if (command.arguments[0]["sgid"] != group_id) { + log.error(LogCategory.NETWORKING, tr("Received invalid notification for server group client list")); + return false; + } + + try { + const result: ServerGroupClient[] = []; + for(const entry of command.arguments) + result.push({ + client_database_id: parseInt(entry["cldbid"]), + client_nickname: entry["client_nickname"], + client_unique_identifier: entry["client_unique_identifier"] + }); + resolve(result); + } catch (error) { + log.error(LogCategory.NETWORKING, tr("Failed to parse server group client list: %o"), error); + reject("failed to parse info"); + } + + return true; + } + }; + this.handler_boss.register_single_handler(single_handler); + + this.connection.send_command("servergroupclientlist", {sgid: group_id}).catch(error => { + this.handler_boss.remove_single_handler(single_handler); + reject(error); + }) + }); + } + + request_playlist_info(playlist_id: number) : Promise { + return new Promise((resolve, reject) => { + const single_handler: SingleCommandHandler = { + command: "notifyplaylistinfo", + function: command => { + const json = command.arguments[0]; + if (json["playlist_id"] != playlist_id) { + log.error(LogCategory.NETWORKING, tr("Received invalid notification for playlist info")); + return; + } + + try { + //resolve + resolve({ + playlist_id: parseInt(json["playlist_id"]), + playlist_title: json["playlist_title"], + playlist_description: json["playlist_description"], + playlist_type: parseInt(json["playlist_type"]), + + playlist_owner_dbid: parseInt(json["playlist_owner_dbid"]), + playlist_owner_name: json["playlist_owner_name"], + + playlist_flag_delete_played: json["playlist_flag_delete_played"] == true || json["playlist_flag_delete_played"] == "1", + playlist_flag_finished: json["playlist_flag_finished"] == true || json["playlist_flag_finished"] == "1", + playlist_replay_mode: parseInt(json["playlist_replay_mode"]), + playlist_current_song_id: parseInt(json["playlist_current_song_id"]), + + playlist_max_songs: parseInt(json["playlist_max_songs"]) + }); + } catch (error) { + log.error(LogCategory.NETWORKING, tr("Failed to parse playlist info: %o"), error); + reject("failed to parse info"); + } + + return true; + } + }; + this.handler_boss.register_single_handler(single_handler); + + this.connection.send_command("playlistinfo", {playlist_id: playlist_id}).catch(error => { + this.handler_boss.remove_single_handler(single_handler); + reject(error); + }) + }); + } + + /** + * @deprecated + * Its just a workaround for the query management. + * There is no garante that the whoami trick will work forever + */ + current_virtual_server_id() : Promise { + if(this._who_am_i) + return Promise.resolve(parseInt(this._who_am_i["virtualserver_id"])); + + return new Promise((resolve, reject) => { + const single_handler: SingleCommandHandler = { + function: command => { + if(command.command != "" && command.command.indexOf("=") == -1) + return false; + + this._who_am_i = command.arguments[0]; + resolve(parseInt(this._who_am_i["virtualserver_id"])); + return true; + } + }; + this.handler_boss.register_single_handler(single_handler); + + this.connection.send_command("whoami").catch(error => { + this.handler_boss.remove_single_handler(single_handler); + reject(error); + }); + }); + } } } \ No newline at end of file diff --git a/shared/js/connection/ConnectionBase.ts b/shared/js/connection/ConnectionBase.ts index 6e490b17..cf975fdb 100644 --- a/shared/js/connection/ConnectionBase.ts +++ b/shared/js/connection/ConnectionBase.ts @@ -1,222 +1,216 @@ -import {ConnectionHandler, ConnectionState} from "../ConnectionHandler"; -import {ServerAddress} from "../channel-tree/server"; -import {CommandResult} from "./ServerConnectionDeclaration"; -import {RecorderProfile} from "../voice/RecorderProfile"; -import {CommandHelper} from "./CommandHelper"; -import {connection} from "./HandshakeHandler"; -import HandshakeHandler = connection.HandshakeHandler; +namespace connection { + export interface CommandOptions { + flagset?: string[]; /* default: [] */ + process_result?: boolean; /* default: true */ -export interface CommandOptions { - flagset?: string[]; /* default: [] */ - process_result?: boolean; /* default: true */ - - timeout?: number /* default: 1000 */; -} -export const CommandOptionDefaults: CommandOptions = { - flagset: [], - process_result: true, - timeout: 1000 -}; - -export type ConnectionStateListener = (old_state: ConnectionState, new_state: ConnectionState) => any; -export abstract class AbstractServerConnection { - readonly client: ConnectionHandler; - readonly command_helper: CommandHelper; - - protected constructor(client: ConnectionHandler) { - this.client = client; - - this.command_helper = new CommandHelper(this); + timeout?: number /* default: 1000 */; } - - /* resolved as soon a connection has been established. This does not means that the authentication had yet been done! */ - abstract connect(address: ServerAddress, handshake: HandshakeHandler, timeout?: number) : Promise; - - abstract connected() : boolean; - abstract disconnect(reason?: string) : Promise; - - abstract support_voice() : boolean; - abstract voice_connection() : voice.AbstractVoiceConnection | undefined; - - abstract command_handler_boss() : AbstractCommandHandlerBoss; - abstract send_command(command: string, data?: any | any[], options?: CommandOptions) : Promise; - - abstract get onconnectionstatechanged() : ConnectionStateListener; - abstract set onconnectionstatechanged(listener: ConnectionStateListener); - - abstract remote_address() : ServerAddress; /* only valid when connected */ - abstract handshake_handler() : HandshakeHandler; /* only valid when connected */ - - abstract ping() : { - native: number, - javascript?: number + export const CommandOptionDefaults: CommandOptions = { + flagset: [], + process_result: true, + timeout: 1000 }; -} -export namespace voice { - export enum PlayerState { - PREBUFFERING, - PLAYING, - BUFFERING, - STOPPING, - STOPPED + export type ConnectionStateListener = (old_state: ConnectionState, new_state: ConnectionState) => any; + export abstract class AbstractServerConnection { + readonly client: ConnectionHandler; + readonly command_helper: CommandHelper; + + protected constructor(client: ConnectionHandler) { + this.client = client; + + this.command_helper = new CommandHelper(this); + } + + /* resolved as soon a connection has been established. This does not means that the authentication had yet been done! */ + abstract connect(address: ServerAddress, handshake: HandshakeHandler, timeout?: number) : Promise; + + abstract connected() : boolean; + abstract disconnect(reason?: string) : Promise; + + abstract support_voice() : boolean; + abstract voice_connection() : voice.AbstractVoiceConnection | undefined; + + abstract command_handler_boss() : AbstractCommandHandlerBoss; + abstract send_command(command: string, data?: any | any[], options?: CommandOptions) : Promise; + + abstract get onconnectionstatechanged() : ConnectionStateListener; + abstract set onconnectionstatechanged(listener: ConnectionStateListener); + + abstract remote_address() : ServerAddress; /* only valid when connected */ + abstract handshake_handler() : HandshakeHandler; /* only valid when connected */ + + abstract ping() : { + native: number, + javascript?: number + }; } - export type LatencySettings = { - min_buffer: number; /* milliseconds */ - max_buffer: number; /* milliseconds */ + export namespace voice { + export enum PlayerState { + PREBUFFERING, + PLAYING, + BUFFERING, + STOPPING, + STOPPED + } + + export type LatencySettings = { + min_buffer: number; /* milliseconds */ + max_buffer: number; /* milliseconds */ + } + + export interface VoiceClient { + client_id: number; + + callback_playback: () => any; + callback_stopped: () => any; + + callback_state_changed: (new_state: PlayerState) => any; + + get_state() : PlayerState; + + get_volume() : number; + set_volume(volume: number) : void; + + abort_replay(); + + support_latency_settings() : boolean; + + reset_latency_settings(); + latency_settings(settings?: LatencySettings) : LatencySettings; + + support_flush() : boolean; + flush(); + } + + export abstract class AbstractVoiceConnection { + readonly connection: AbstractServerConnection; + + protected constructor(connection: AbstractServerConnection) { + this.connection = connection; + } + + abstract connected() : boolean; + abstract encoding_supported(codec: number) : boolean; + abstract decoding_supported(codec: number) : boolean; + + abstract register_client(client_id: number) : VoiceClient; + abstract available_clients() : VoiceClient[]; + abstract unregister_client(client: VoiceClient) : Promise; + + abstract voice_recorder() : RecorderProfile; + abstract acquire_voice_recorder(recorder: RecorderProfile | undefined) : Promise; + + abstract get_encoder_codec() : number; + abstract set_encoder_codec(codec: number); + } } - export interface VoiceClient { - client_id: number; - - callback_playback: () => any; - callback_stopped: () => any; - - callback_state_changed: (new_state: PlayerState) => any; - - get_state() : PlayerState; - - get_volume() : number; - set_volume(volume: number) : void; - - abort_replay(); - - support_latency_settings() : boolean; - - reset_latency_settings(); - latency_settings(settings?: LatencySettings) : LatencySettings; - - support_flush() : boolean; - flush(); + export class ServerCommand { + command: string; + arguments: any[]; } - export abstract class AbstractVoiceConnection { + export abstract class AbstractCommandHandler { readonly connection: AbstractServerConnection; + handler_boss: AbstractCommandHandlerBoss | undefined; + volatile_handler_boss: boolean = false; /* if true than the command handler could be registered twice to two or more handlers */ + + ignore_consumed: boolean = false; + protected constructor(connection: AbstractServerConnection) { this.connection = connection; } - abstract connected() : boolean; - abstract encoding_supported(codec: number) : boolean; - abstract decoding_supported(codec: number) : boolean; - - abstract register_client(client_id: number) : VoiceClient; - abstract available_clients() : VoiceClient[]; - abstract unregister_client(client: VoiceClient) : Promise; - - abstract voice_recorder() : RecorderProfile; - abstract acquire_voice_recorder(recorder: RecorderProfile | undefined) : Promise; - - abstract get_encoder_codec() : number; - abstract set_encoder_codec(codec: number); - } -} - -export class ServerCommand { - command: string; - arguments: any[]; -} - -export abstract class AbstractCommandHandler { - readonly connection: AbstractServerConnection; - - handler_boss: AbstractCommandHandlerBoss | undefined; - volatile_handler_boss: boolean = false; /* if true than the command handler could be registered twice to two or more handlers */ - - ignore_consumed: boolean = false; - - protected constructor(connection: AbstractServerConnection) { - this.connection = connection; + /** + * @return If the command should be consumed + */ + abstract handle_command(command: ServerCommand) : boolean; } - /** - * @return If the command should be consumed - */ - abstract handle_command(command: ServerCommand) : boolean; -} + export interface SingleCommandHandler { + name?: string; + command?: string; + timeout?: number; -export interface SingleCommandHandler { - name?: string; - command?: string; - timeout?: number; - - /* if the return is true then the command handler will be removed */ - function: (command: ServerCommand) => boolean; -} - -export abstract class AbstractCommandHandlerBoss { - readonly connection: AbstractServerConnection; - protected command_handlers: AbstractCommandHandler[] = []; - /* TODO: Timeout */ - protected single_command_handler: SingleCommandHandler[] = []; - - protected constructor(connection: AbstractServerConnection) { - this.connection = connection; + /* if the return is true then the command handler will be removed */ + function: (command: ServerCommand) => boolean; } - destroy() { - this.command_handlers = undefined; - this.single_command_handler = undefined; - } + export abstract class AbstractCommandHandlerBoss { + readonly connection: AbstractServerConnection; + protected command_handlers: AbstractCommandHandler[] = []; + /* TODO: Timeout */ + protected single_command_handler: SingleCommandHandler[] = []; - register_handler(handler: AbstractCommandHandler) { - if(!handler.volatile_handler_boss && handler.handler_boss) - throw "handler already registered"; - - this.command_handlers.remove(handler); /* just to be sure */ - this.command_handlers.push(handler); - handler.handler_boss = this; - } - - unregister_handler(handler: AbstractCommandHandler) { - if(!handler.volatile_handler_boss && handler.handler_boss !== this) { - console.warn(tr("Tried to unregister command handler which does not belong to the handler boss")); - return; + protected constructor(connection: AbstractServerConnection) { + this.connection = connection; } - this.command_handlers.remove(handler); - handler.handler_boss = undefined; - } + destroy() { + this.command_handlers = undefined; + this.single_command_handler = undefined; + } + register_handler(handler: AbstractCommandHandler) { + if(!handler.volatile_handler_boss && handler.handler_boss) + throw "handler already registered"; - register_single_handler(handler: SingleCommandHandler) { - this.single_command_handler.push(handler); - } + this.command_handlers.remove(handler); /* just to be sure */ + this.command_handlers.push(handler); + handler.handler_boss = this; + } - remove_single_handler(handler: SingleCommandHandler) { - this.single_command_handler.remove(handler); - } - - handlers() : AbstractCommandHandler[] { - return this.command_handlers; - } - - invoke_handle(command: ServerCommand) : boolean { - let flag_consumed = false; - - for(const handler of this.command_handlers) { - try { - if(!flag_consumed || handler.ignore_consumed) - flag_consumed = flag_consumed || handler.handle_command(command); - } catch(error) { - console.error(tr("Failed to invoke command handler. Invocation results in an exception: %o"), error); + unregister_handler(handler: AbstractCommandHandler) { + if(!handler.volatile_handler_boss && handler.handler_boss !== this) { + console.warn(tr("Tried to unregister command handler which does not belong to the handler boss")); + return; } + + this.command_handlers.remove(handler); + handler.handler_boss = undefined; } - for(const handler of [...this.single_command_handler]) { - if(handler.command && handler.command != command.command) - continue; - try { - if(handler.function(command)) - this.single_command_handler.remove(handler); - } catch(error) { - console.error(tr("Failed to invoke single command handler. Invocation results in an exception: %o"), error); + register_single_handler(handler: SingleCommandHandler) { + this.single_command_handler.push(handler); + } + + remove_single_handler(handler: SingleCommandHandler) { + this.single_command_handler.remove(handler); + } + + handlers() : AbstractCommandHandler[] { + return this.command_handlers; + } + + invoke_handle(command: ServerCommand) : boolean { + let flag_consumed = false; + + for(const handler of this.command_handlers) { + try { + if(!flag_consumed || handler.ignore_consumed) + flag_consumed = flag_consumed || handler.handle_command(command); + } catch(error) { + console.error(tr("Failed to invoke command handler. Invocation results in an exception: %o"), error); + } } - } - return flag_consumed; + for(const handler of [...this.single_command_handler]) { + if(handler.command && handler.command != command.command) + continue; + + try { + if(handler.function(command)) + this.single_command_handler.remove(handler); + } catch(error) { + console.error(tr("Failed to invoke single command handler. Invocation results in an exception: %o"), error); + } + } + + return flag_consumed; + } } } \ No newline at end of file diff --git a/shared/js/connection/HandshakeHandler.ts b/shared/js/connection/HandshakeHandler.ts index f4cb14df..de9c4910 100644 --- a/shared/js/connection/HandshakeHandler.ts +++ b/shared/js/connection/HandshakeHandler.ts @@ -1,15 +1,4 @@ -import {AbstractServerConnection} from "./ConnectionBase"; -import {ConnectParameters, DisconnectReason} from "../ConnectionHandler"; -import {profiles} from "../profiles/ConnectionProfile"; -import {profiles as iprofiles} from "../profiles/Identity"; -import {profiles as tiprofiles} from "../profiles/identities/TeamSpeakIdentity"; -import {native_client} from "../main"; -import {settings} from "../settings"; -import {CommandResult} from "./ServerConnectionDeclaration"; - -export namespace connection { - import identities = iprofiles.identities; - +namespace connection { export interface HandshakeIdentityHandler { connection: AbstractServerConnection; @@ -59,7 +48,7 @@ export namespace connection { on_teamspeak() { const type = this.profile.selected_type(); - if(type == identities.IdentitifyType.TEAMSPEAK) + if(type == profiles.identities.IdentitifyType.TEAMSPEAK) this.handshake_finished(); else { @@ -133,8 +122,8 @@ export namespace connection { } /* required to keep compatibility */ - if(this.profile.selected_type() === identities.IdentitifyType.TEAMSPEAK) { - data["client_key_offset"] = (this.profile.selected_identity() as tiprofiles.identities.TeaSpeakIdentity).hash_number; + if(this.profile.selected_type() === profiles.identities.IdentitifyType.TEAMSPEAK) { + data["client_key_offset"] = (this.profile.selected_identity() as profiles.identities.TeaSpeakIdentity).hash_number; } this.connection.send_command("clientinit", data).catch(error => { diff --git a/shared/js/connection/ServerConnectionDeclaration.ts b/shared/js/connection/ServerConnectionDeclaration.ts index 1161c107..c2a71825 100644 --- a/shared/js/connection/ServerConnectionDeclaration.ts +++ b/shared/js/connection/ServerConnectionDeclaration.ts @@ -1,6 +1,4 @@ -import {LaterPromise} from "../utils/helpers"; - -export enum ErrorID { +enum ErrorID { NOT_IMPLEMENTED = 0x2, COMMAND_NOT_FOUND = 0x100, @@ -17,7 +15,7 @@ export enum ErrorID { CONVERSATION_IS_PRIVATE = 0x2202 } -export class CommandResult { +class CommandResult { success: boolean; id: number; message: string; @@ -37,39 +35,39 @@ export class CommandResult { } } -export interface ClientNameInfo { +interface ClientNameInfo { //cluid=tYzKUryn\/\/Y8VBMf8PHUT6B1eiE= name=Exp clname=Exp cldbid=9 client_unique_id: string; client_nickname: string; client_database_id: number; } -export interface ClientNameFromUid { +interface ClientNameFromUid { promise: LaterPromise, keys: string[], response: ClientNameInfo[] } -export interface ServerGroupClient { +interface ServerGroupClient { client_nickname: string; client_unique_identifier: string; client_database_id: number; } -export interface QueryListEntry { +interface QueryListEntry { username: string; unique_id: string; bounded_server: number; } -export interface QueryList { +interface QueryList { flag_own: boolean; flag_all: boolean; queries: QueryListEntry[]; } -export interface Playlist { +interface Playlist { playlist_id: number; playlist_bot_id: number; playlist_title: string; @@ -85,7 +83,7 @@ export interface Playlist { needed_power_song_remove: number; } -export interface PlaylistInfo { +interface PlaylistInfo { playlist_id: number, playlist_title: string, playlist_description: string, @@ -102,7 +100,7 @@ export interface PlaylistInfo { playlist_max_songs: number } -export interface PlaylistSong { +interface PlaylistSong { song_id: number; song_previous_song_id: number; song_invoker: string; diff --git a/shared/js/crypto/asn1.ts b/shared/js/crypto/asn1.ts index a9bf9b44..8184a4aa 100644 --- a/shared/js/crypto/asn1.ts +++ b/shared/js/crypto/asn1.ts @@ -14,7 +14,7 @@ // ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF // OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. -export namespace asn1 { +namespace asn1 { declare class Int10 { constructor(value?: any); diff --git a/shared/js/crypto/crc32.ts b/shared/js/crypto/crc32.ts index 52e14979..8122a8a2 100644 --- a/shared/js/crypto/crc32.ts +++ b/shared/js/crypto/crc32.ts @@ -1,4 +1,4 @@ -export class Crc32 { +class Crc32 { private static readonly lookup = [ 0x00000000, 0x77073096, 0xEE0E612C, 0x990951BA, 0x076DC419, 0x706AF48F, 0xE963A535, 0x9E6495A3, diff --git a/shared/js/crypto/hex.ts b/shared/js/crypto/hex.ts index ca6e319e..9c700341 100644 --- a/shared/js/crypto/hex.ts +++ b/shared/js/crypto/hex.ts @@ -1,4 +1,4 @@ -export namespace hex { +namespace hex { export function encode(buffer) { let hexCodes = []; let view = new DataView(buffer); diff --git a/shared/js/crypto/sha.ts b/shared/js/crypto/sha.ts index d2057b5d..28998cff 100644 --- a/shared/js/crypto/sha.ts +++ b/shared/js/crypto/sha.ts @@ -10,7 +10,7 @@ interface Window { } */ -export namespace sha { +namespace sha { /* * [js-sha1]{@link https://github.com/emn178/js-sha1} * diff --git a/shared/js/crypto/uid.ts b/shared/js/crypto/uid.ts deleted file mode 100644 index 9ade46c7..00000000 --- a/shared/js/crypto/uid.ts +++ /dev/null @@ -1,8 +0,0 @@ -export function guid() { - function s4() { - return Math.floor((1 + Math.random()) * 0x10000) - .toString(16) - .substring(1); - } - return s4() + s4() + '-' + s4() + '-' + s4() + '-' + s4() + '-' + s4() + s4() + s4(); -} \ No newline at end of file diff --git a/shared/js/dns.ts b/shared/js/dns.ts index af3070e1..866b0daf 100644 --- a/shared/js/dns.ts +++ b/shared/js/dns.ts @@ -1,4 +1,4 @@ -export namespace dns { +namespace dns { export interface AddressTarget { target_ip: string; target_port?: number; diff --git a/shared/js/events.ts b/shared/js/events.ts index 9ce4d73f..348621a8 100644 --- a/shared/js/events.ts +++ b/shared/js/events.ts @@ -1,8 +1,4 @@ -import {guid} from "./crypto/uid"; -import {PlaylistSong} from "./connection/ServerConnectionDeclaration"; -import {MusicClientEntry, SongInfo} from "./channel-tree/client"; - -export namespace events { +namespace events { export interface EventConvert { as() : All[T]; } diff --git a/shared/js/i18n/country.ts b/shared/js/i18n/country.ts index 31c81dd8..ce152e30 100644 --- a/shared/js/i18n/country.ts +++ b/shared/js/i18n/country.ts @@ -1,4 +1,5 @@ -export namespace i18n { + +namespace i18n { interface CountryInfo { name: string; alpha_2: string; diff --git a/shared/js/i18n/localize.ts b/shared/js/i18n/localize.ts index 756930fe..1e469c39 100644 --- a/shared/js/i18n/localize.ts +++ b/shared/js/i18n/localize.ts @@ -1,10 +1,13 @@ -import {guid} from "../crypto/uid"; -import {log, LogCategory} from "../log"; -import {MessageHelper} from "../ui/frames/chat"; -import {StaticSettings} from "../settings"; -import {createErrorModal} from "../ui/elements/modal"; +function guid() { + function s4() { + return Math.floor((1 + Math.random()) * 0x10000) + .toString(16) + .substring(1); + } + return s4() + s4() + '-' + s4() + '-' + s4() + '-' + s4() + '-' + s4() + s4() + s4(); +} -export namespace i18n { +namespace i18n { export interface TranslationKey { message: string; line?: number; diff --git a/shared/js/log.ts b/shared/js/log.ts index d900686a..4ea9d119 100644 --- a/shared/js/log.ts +++ b/shared/js/log.ts @@ -1,8 +1,6 @@ //Used by CertAccept popup -import {settings} from "./settings"; - -export enum LogCategory { +enum LogCategory { CHANNEL, CHANNEL_PROPERTIES, /* separating channel and channel properties because on channel init logging is a big bottleneck */ CLIENT, @@ -21,7 +19,7 @@ export enum LogCategory { DNS } -export namespace log { +namespace log { export enum LogType { TRACE, DEBUG, diff --git a/shared/js/main.ts b/shared/js/main.ts index 354cc186..f2e910bc 100644 --- a/shared/js/main.ts +++ b/shared/js/main.ts @@ -1,18 +1,19 @@ +/// +/// +/// +/// +/// +/// +/// +/// +/// + import spawnYesNo = Modals.spawnYesNo; -import {ConnectionHandler} from "./ConnectionHandler"; -import {bipc} from "./BrowserIPC"; -import {log, LogCategory} from "./log"; -import {profiles} from "./profiles/ConnectionProfile"; -import {Modals} from "./ui/modal/ModalConnect"; -import {settings, Settings} from "./settings"; -import {i18n} from "./i18n/localize"; -import {createInfoModal} from "./ui/elements/modal"; -import {MessageHelper} from "./ui/frames/chat"; -export const js_render = window.jsrender || $; -export const native_client = window.require !== undefined; +const js_render = window.jsrender || $; +const native_client = window.require !== undefined; -export function getUserMediaFunctionPromise() : (constraints: MediaStreamConstraints) => Promise { +function getUserMediaFunctionPromise() : (constraints: MediaStreamConstraints) => Promise { if('mediaDevices' in navigator && 'getUserMedia' in navigator.mediaDevices) return constraints => navigator.mediaDevices.getUserMedia(constraints); @@ -23,12 +24,11 @@ export function getUserMediaFunctionPromise() : (constraints: MediaStreamConstra return constraints => new Promise((resolve, reject) => _callbacked_function(constraints, resolve, reject)); } -export interface Window { +interface Window { open_connected_question: () => Promise; } -export declare const nodeRequire: typeof require; -export function setup_close() { +function setup_close() { window.onbeforeunload = event => { if(profiles.requires_save()) profiles.save(); @@ -50,7 +50,7 @@ export function setup_close() { })); const exit = () => { - const {remote} = nodeRequire('electron'); + const {remote} = require('electron'); remote.getCurrentWindow().close(); }; @@ -80,8 +80,8 @@ export function setup_close() { }; } -export declare function moment(...arguments) : any; -export function setup_jsrender() : boolean { +declare function moment(...arguments) : any; +function setup_jsrender() : boolean { if(!js_render) { loader.critical_error("Missing jsrender extension!"); return false; @@ -115,7 +115,7 @@ export function setup_jsrender() : boolean { return true; } -export async function initialize() { +async function initialize() { Settings.initialize(); try { @@ -129,7 +129,7 @@ export async function initialize() { bipc.setup(); } -export async function initialize_app() { +async function initialize_app() { try { //Initialize main template const main = $("#tmpl_main").renderTag({ multi_session: !settings.static_global(Settings.KEY_DISABLE_MULTI_SESSION), @@ -180,7 +180,7 @@ export async function initialize_app() { setup_close(); } -export function str2ab8(str) { +function str2ab8(str) { const buf = new ArrayBuffer(str.length); const bufView = new Uint8Array(buf); for (let i = 0, strLen = str.length; i < strLen; i++) { @@ -190,7 +190,7 @@ export function str2ab8(str) { } /* FIXME Dont use atob, because it sucks for non UTF-8 tings */ -export function arrayBufferBase64(base64: string) { +function arrayBufferBase64(base64: string) { base64 = atob(base64); const buf = new ArrayBuffer(base64.length); const bufView = new Uint8Array(buf); @@ -200,7 +200,7 @@ export function arrayBufferBase64(base64: string) { return buf; } -export function base64_encode_ab(source: ArrayBufferLike) { +function base64_encode_ab(source: ArrayBufferLike) { const encodings = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; let base64 = ""; diff --git a/shared/js/permission/GroupManager.ts b/shared/js/permission/GroupManager.ts index 55e70ad0..c781f468 100644 --- a/shared/js/permission/GroupManager.ts +++ b/shared/js/permission/GroupManager.ts @@ -1,24 +1,17 @@ /// -import {LaterPromise} from "../utils/helpers"; -import {PermissionManager, PermissionValue} from "./PermissionManager"; -import {AbstractCommandHandler, ServerCommand} from "../connection/ConnectionBase"; -import {ConnectionHandler} from "../ConnectionHandler"; -import {log, LogCategory} from "../log"; -import {CommandResult} from "../connection/ServerConnectionDeclaration"; - -export enum GroupType { +enum GroupType { QUERY, TEMPLATE, NORMAL } -export enum GroupTarget { +enum GroupTarget { SERVER, CHANNEL } -export class GroupProperties { +class GroupProperties { iconid: number = 0; sortid: number = 0; @@ -26,12 +19,12 @@ export class GroupProperties { namemode: number = 0; } -export class GroupPermissionRequest { +class GroupPermissionRequest { group_id: number; promise: LaterPromise; } -export class Group { +class Group { properties: GroupProperties = new GroupProperties(); readonly handle: GroupManager; @@ -70,7 +63,7 @@ export class Group { } } -export class GroupManager extends AbstractCommandHandler { +class GroupManager extends connection.AbstractCommandHandler { readonly handle: ConnectionHandler; serverGroups: Group[] = []; @@ -90,7 +83,7 @@ export class GroupManager extends AbstractCommandHandler { this.channelGroups = undefined; } - handle_command(command: ServerCommand): boolean { + handle_command(command: connection.ServerCommand): boolean { switch (command.command) { case "notifyservergrouplist": case "notifychannelgrouplist": diff --git a/shared/js/permission/PermissionManager.ts b/shared/js/permission/PermissionManager.ts index 960ee70c..9b558d1c 100644 --- a/shared/js/permission/PermissionManager.ts +++ b/shared/js/permission/PermissionManager.ts @@ -2,14 +2,7 @@ /// /// -import {ConnectionHandler} from "../ConnectionHandler"; -import {log, LogCategory} from "../log"; -import {LaterPromise} from "../utils/helpers"; -import {AbstractCommandHandler, ServerCommand} from "../connection/ConnectionBase"; -import LogType = log.LogType; -import {CommandResult, ErrorID} from "../connection/ServerConnectionDeclaration"; - -export enum PermissionType { +enum PermissionType { B_SERVERINSTANCE_HELP_VIEW = "b_serverinstance_help_view", B_SERVERINSTANCE_VERSION_VIEW = "b_serverinstance_version_view", B_SERVERINSTANCE_INFO_VIEW = "b_serverinstance_info_view", @@ -359,7 +352,7 @@ export enum PermissionType { I_FT_QUOTA_MB_UPLOAD_PER_CLIENT = "i_ft_quota_mb_upload_per_client" } -export class PermissionInfo { +class PermissionInfo { name: string; id: number; description: string; @@ -370,21 +363,21 @@ export class PermissionInfo { } } -export class PermissionGroup { +class PermissionGroup { begin: number; end: number; deep: number; name: string; } -export class GroupedPermissions { +class GroupedPermissions { group: PermissionGroup; permissions: PermissionInfo[]; children: GroupedPermissions[]; parent: GroupedPermissions; } -export class PermissionValue { +class PermissionValue { readonly type: PermissionInfo; value: number; flag_skip: boolean; @@ -418,13 +411,13 @@ export class PermissionValue { } } -export class NeededPermissionValue extends PermissionValue { +class NeededPermissionValue extends PermissionValue { constructor(type, value) { super(type, value); } } -export namespace permissions { +namespace permissions { export type PermissionRequestKeys = { client_id?: number; channel_id?: number; @@ -480,13 +473,13 @@ export namespace permissions { } } -export type RequestLists = +type RequestLists = "requests_channel_permissions" | "requests_client_permissions" | "requests_client_channel_permissions" | "requests_playlist_permissions" | "requests_playlist_client_permissions"; -export class PermissionManager extends AbstractCommandHandler { +class PermissionManager extends connection.AbstractCommandHandler { readonly handle: ConnectionHandler; permissionList: PermissionInfo[] = []; @@ -610,7 +603,7 @@ export class PermissionManager extends AbstractCommandHandler { this._cacheNeededPermissions = undefined; } - handle_command(command: ServerCommand): boolean { + handle_command(command: connection.ServerCommand): boolean { switch (command.command) { case "notifyclientneededpermissions": this.onNeededPermissions(command.arguments); diff --git a/shared/js/profiles/ConnectionProfile.ts b/shared/js/profiles/ConnectionProfile.ts index f46266b3..f30cbe54 100644 --- a/shared/js/profiles/ConnectionProfile.ts +++ b/shared/js/profiles/ConnectionProfile.ts @@ -1,260 +1,251 @@ -import {MessageHelper} from "../ui/frames/chat"; -import {createErrorModal} from "../ui/elements/modal"; -import {guid} from "../crypto/uid"; -import {decode_identity, IdentitifyType, Identity} from "./Identity"; -import {static_forum_identity} from "./identities/TeaForumIdentity"; -import {TeaSpeakIdentity} from "./identities/TeamSpeakIdentity"; -import {AbstractServerConnection} from "../connection/ConnectionBase"; -import {connection} from "../connection/HandshakeHandler"; +namespace profiles { + export class ConnectionProfile { + id: string; -import HandshakeIdentityHandler = connection.HandshakeIdentityHandler; + profile_name: string; + default_username: string; + default_password: string; -export class ConnectionProfile { - id: string; + selected_identity_type: string = "unset"; + identities: { [key: string]: identities.Identity } = {}; - profile_name: string; - default_username: string; - default_password: string; - - selected_identity_type: string = "unset"; - identities: { [key: string]: Identity } = {}; - - constructor(id: string) { - this.id = id; - } - - connect_username(): string { - if (this.default_username && this.default_username !== "Another TeaSpeak user") - return this.default_username; - - let selected = this.selected_identity(); - let name = selected ? selected.fallback_name() : undefined; - return name || "Another TeaSpeak user"; - } - - selected_identity(current_type?: IdentitifyType): Identity { - if (!current_type) - current_type = this.selected_type(); - - if (current_type === undefined) - return undefined; - - if (current_type == IdentitifyType.TEAFORO) { - return static_forum_identity(); - } else if (current_type == IdentitifyType.TEAMSPEAK || current_type == IdentitifyType.NICKNAME) { - return this.identities[IdentitifyType[current_type].toLowerCase()]; + constructor(id: string) { + this.id = id; } + connect_username(): string { + if (this.default_username && this.default_username !== "Another TeaSpeak user") + return this.default_username; + + let selected = this.selected_identity(); + let name = selected ? selected.fallback_name() : undefined; + return name || "Another TeaSpeak user"; + } + + selected_identity(current_type?: identities.IdentitifyType): identities.Identity { + if (!current_type) + current_type = this.selected_type(); + + if (current_type === undefined) + return undefined; + + if (current_type == identities.IdentitifyType.TEAFORO) { + return identities.static_forum_identity(); + } else if (current_type == identities.IdentitifyType.TEAMSPEAK || current_type == identities.IdentitifyType.NICKNAME) { + return this.identities[identities.IdentitifyType[current_type].toLowerCase()]; + } + + return undefined; + } + + selected_type?(): identities.IdentitifyType { + return this.selected_identity_type ? identities.IdentitifyType[this.selected_identity_type.toUpperCase()] : undefined; + } + + set_identity(type: identities.IdentitifyType, identity: identities.Identity) { + this.identities[identities.IdentitifyType[type].toLowerCase()] = identity; + } + + spawn_identity_handshake_handler?(connection: connection.AbstractServerConnection): connection.HandshakeIdentityHandler { + const identity = this.selected_identity(); + if (!identity) + return undefined; + return identity.spawn_identity_handshake_handler(connection); + } + + encode?(): string { + const identity_data = {}; + for (const key in this.identities) + if (this.identities[key]) + identity_data[key] = this.identities[key].encode(); + + return JSON.stringify({ + version: 1, + username: this.default_username, + password: this.default_password, + profile_name: this.profile_name, + identity_type: this.selected_identity_type, + identity_data: identity_data, + id: this.id + }); + } + + valid(): boolean { + const identity = this.selected_identity(); + if (!identity || !identity.valid()) return false; + + return true; + } + } + + async function decode_profile(data): Promise { + data = JSON.parse(data); + if (data.version !== 1) + return "invalid version"; + + const result: ConnectionProfile = new ConnectionProfile(data.id); + result.default_username = data.username; + result.default_password = data.password; + result.profile_name = data.profile_name; + result.selected_identity_type = (data.identity_type || "").toLowerCase(); + + if (data.identity_data) { + for (const key in data.identity_data) { + const type = identities.IdentitifyType[key.toUpperCase() as string]; + const _data = data.identity_data[key]; + if (type == undefined) continue; + + const identity = await identities.decode_identity(type, _data); + if (identity == undefined) continue; + + result.identities[key.toLowerCase()] = identity; + } + } + + return result; + } + + interface ProfilesData { + version: number; + profiles: string[]; + } + + let available_profiles: ConnectionProfile[] = []; + + export async function load() { + available_profiles = []; + + const profiles_json = localStorage.getItem("profiles"); + let profiles_data: ProfilesData = (() => { + try { + return profiles_json ? JSON.parse(profiles_json) : {version: 0} as any; + } catch (error) { + debugger; + console.error(tr("Invalid profile json! Resetting profiles :( (%o)"), profiles_json); + createErrorModal(tr("Profile data invalid"), MessageHelper.formatMessage(tr("The profile data is invalid.{:br:}This might cause data loss."))).open(); + return {version: 0}; + } + })(); + + if (profiles_data.version === 0) { + profiles_data = { + version: 1, + profiles: [] + }; + } + if (profiles_data.version == 1) { + for (const profile_data of profiles_data.profiles) { + const profile = await decode_profile(profile_data); + if (typeof (profile) === 'string') { + console.error(tr("Failed to load profile. Reason: %s, Profile data: %s"), profile, profiles_data); + continue; + } + available_profiles.push(profile); + } + } + + if (!find_profile("default")) { //Create a default profile and teaforo profile + { + const profile = create_new_profile("default", "default"); + profile.default_password = ""; + profile.default_username = ""; + profile.profile_name = "Default Profile"; + + /* generate default identity */ + try { + const identity = await identities.TeaSpeakIdentity.generate_new(); + let active = true; + setTimeout(() => { + active = false; + }, 1000); + await identity.improve_level(8, 1, () => active); + profile.set_identity(identities.IdentitifyType.TEAMSPEAK, identity); + profile.selected_identity_type = identities.IdentitifyType[identities.IdentitifyType.TEAMSPEAK]; + } catch (error) { + createErrorModal(tr("Failed to generate default identity"), tr("Failed to generate default identity!
Please manually generate the identity within your settings => profiles")).open(); + } + } + + { /* forum identity (works only when connected to the forum) */ + const profile = create_new_profile("TeaSpeak Forum", "teaforo"); + profile.default_password = ""; + profile.default_username = ""; + profile.profile_name = "TeaSpeak Forum profile"; + + profile.set_identity(identities.IdentitifyType.TEAFORO, identities.static_forum_identity()); + profile.selected_identity_type = identities.IdentitifyType[identities.IdentitifyType.TEAFORO]; + } + + save(); + } + } + + export function create_new_profile(name: string, id?: string): ConnectionProfile { + const profile = new ConnectionProfile(id || guid()); + profile.profile_name = name; + profile.default_username = ""; + available_profiles.push(profile); + return profile; + } + + let _requires_save = false; + + export function save() { + const profiles: string[] = []; + for (const profile of available_profiles) + profiles.push(profile.encode()); + + const data = JSON.stringify({ + version: 1, + profiles: profiles + }); + localStorage.setItem("profiles", data); + } + + export function mark_need_save() { + _requires_save = true; + } + + export function requires_save(): boolean { + return _requires_save; + } + + export function profiles(): ConnectionProfile[] { + return available_profiles; + } + + export function find_profile(id: string): ConnectionProfile | undefined { + for (const profile of profiles()) + if (profile.id == id) + return profile; + return undefined; } - selected_type?(): IdentitifyType { - return this.selected_identity_type ? IdentitifyType[this.selected_identity_type.toUpperCase()] : undefined; + export function find_profile_by_name(name: string): ConnectionProfile | undefined { + name = name.toLowerCase(); + for (const profile of profiles()) + if ((profile.profile_name || "").toLowerCase() == name) + return profile; + + return undefined; } - set_identity(type: IdentitifyType, identity: Identity) { - this.identities[IdentitifyType[type].toLowerCase()] = identity; + + export function default_profile(): ConnectionProfile { + return find_profile("default"); } - spawn_identity_handshake_handler?(connection: AbstractServerConnection): HandshakeIdentityHandler { - const identity = this.selected_identity(); - if (!identity) - return undefined; - return identity.spawn_identity_handshake_handler(connection); - } - - encode?(): string { - const identity_data = {}; - for (const key in this.identities) - if (this.identities[key]) - identity_data[key] = this.identities[key].encode(); - - return JSON.stringify({ - version: 1, - username: this.default_username, - password: this.default_password, - profile_name: this.profile_name, - identity_type: this.selected_identity_type, - identity_data: identity_data, - id: this.id - }); - } - - valid(): boolean { - const identity = this.selected_identity(); - if (!identity || !identity.valid()) return false; - - return true; - } -} - -async function decode_profile(data): Promise { - data = JSON.parse(data); - if (data.version !== 1) - return "invalid version"; - - const result: ConnectionProfile = new ConnectionProfile(data.id); - result.default_username = data.username; - result.default_password = data.password; - result.profile_name = data.profile_name; - result.selected_identity_type = (data.identity_type || "").toLowerCase(); - - if (data.identity_data) { - for (const key in data.identity_data) { - const type = IdentitifyType[key.toUpperCase() as string]; - const _data = data.identity_data[key]; - if (type == undefined) continue; - - const identity = await decode_identity(type, _data); - if (identity == undefined) continue; - - result.identities[key.toLowerCase()] = identity; + export function set_default_profile(profile: ConnectionProfile) { + const old_default = default_profile(); + if (old_default && old_default != profile) { + old_default.id = guid(); } + profile.id = "default"; + return old_default; } - return result; -} - -interface ProfilesData { - version: number; - profiles: string[]; -} - -let available_profiles: ConnectionProfile[] = []; - -export async function load() { - available_profiles = []; - - const profiles_json = localStorage.getItem("profiles"); - let profiles_data: ProfilesData = (() => { - try { - return profiles_json ? JSON.parse(profiles_json) : {version: 0} as any; - } catch (error) { - debugger; - console.error(tr("Invalid profile json! Resetting profiles :( (%o)"), profiles_json); - createErrorModal(tr("Profile data invalid"), MessageHelper.formatMessage(tr("The profile data is invalid.{:br:}This might cause data loss."))).open(); - return {version: 0}; - } - })(); - - if (profiles_data.version === 0) { - profiles_data = { - version: 1, - profiles: [] - }; + export function delete_profile(profile: ConnectionProfile) { + available_profiles.remove(profile); } - if (profiles_data.version == 1) { - for (const profile_data of profiles_data.profiles) { - const profile = await decode_profile(profile_data); - if (typeof (profile) === 'string') { - console.error(tr("Failed to load profile. Reason: %s, Profile data: %s"), profile, profiles_data); - continue; - } - available_profiles.push(profile); - } - } - - if (!find_profile("default")) { //Create a default profile and teaforo profile - { - const profile = create_new_profile("default", "default"); - profile.default_password = ""; - profile.default_username = ""; - profile.profile_name = "Default Profile"; - - /* generate default identity */ - try { - const identity = await TeaSpeakIdentity.generate_new(); - let active = true; - setTimeout(() => { - active = false; - }, 1000); - await identity.improve_level(8, 1, () => active); - profile.set_identity(IdentitifyType.TEAMSPEAK, identity); - profile.selected_identity_type = IdentitifyType[IdentitifyType.TEAMSPEAK]; - } catch (error) { - createErrorModal(tr("Failed to generate default identity"), tr("Failed to generate default identity!
Please manually generate the identity within your settings => profiles")).open(); - } - } - - { /* forum identity (works only when connected to the forum) */ - const profile = create_new_profile("TeaSpeak Forum", "teaforo"); - profile.default_password = ""; - profile.default_username = ""; - profile.profile_name = "TeaSpeak Forum profile"; - - profile.set_identity(IdentitifyType.TEAFORO, static_forum_identity()); - profile.selected_identity_type = IdentitifyType[IdentitifyType.TEAFORO]; - } - - save(); - } -} - -export function create_new_profile(name: string, id?: string): ConnectionProfile { - const profile = new ConnectionProfile(id || guid()); - profile.profile_name = name; - profile.default_username = ""; - available_profiles.push(profile); - return profile; -} - -let _requires_save = false; - -export function save() { - const profiles: string[] = []; - for (const profile of available_profiles) - profiles.push(profile.encode()); - - const data = JSON.stringify({ - version: 1, - profiles: profiles - }); - localStorage.setItem("profiles", data); -} - -export function mark_need_save() { - _requires_save = true; -} - -export function requires_save(): boolean { - return _requires_save; -} - -export function profiles(): ConnectionProfile[] { - return available_profiles; -} - -export function find_profile(id: string): ConnectionProfile | undefined { - for (const profile of profiles()) - if (profile.id == id) - return profile; - - return undefined; -} - -export function find_profile_by_name(name: string): ConnectionProfile | undefined { - name = name.toLowerCase(); - for (const profile of profiles()) - if ((profile.profile_name || "").toLowerCase() == name) - return profile; - - return undefined; -} - - -export function default_profile(): ConnectionProfile { - return find_profile("default"); -} - -export function set_default_profile(profile: ConnectionProfile) { - const old_default = default_profile(); - if (old_default && old_default != profile) { - old_default.id = guid(); - } - profile.id = "default"; - return old_default; -} - -export function delete_profile(profile: ConnectionProfile) { - available_profiles.remove(profile); } \ No newline at end of file diff --git a/shared/js/profiles/Identity.ts b/shared/js/profiles/Identity.ts index 6e4dfbdc..c658f46d 100644 --- a/shared/js/profiles/Identity.ts +++ b/shared/js/profiles/Identity.ts @@ -1,116 +1,110 @@ -import {AbstractCommandHandler, AbstractServerConnection, ServerCommand} from "../connection/ConnectionBase"; -import {connection} from "../connection/HandshakeHandler"; - -import HandshakeIdentityHandler = connection.HandshakeIdentityHandler; -import {NameIdentity} from "./identities/NameIdentity"; -import {TeaForumIdentity} from "./identities/TeaForumIdentity"; -import {TeaSpeakIdentity} from "./identities/TeamSpeakIdentity"; - -export enum IdentitifyType { - TEAFORO, - TEAMSPEAK, - NICKNAME -} - -export interface Identity { - fallback_name(): string | undefined ; - uid() : string; - type() : IdentitifyType; - - valid() : boolean; - - encode?() : string; - decode(data: string) : Promise; - - spawn_identity_handshake_handler(connection: AbstractServerConnection) : HandshakeIdentityHandler; -} - -export async function decode_identity(type: IdentitifyType, data: string) : Promise { - let identity: Identity; - switch (type) { - case IdentitifyType.NICKNAME: - identity = new NameIdentity(); - break; - case IdentitifyType.TEAFORO: - identity = new TeaForumIdentity(undefined); - break; - case IdentitifyType.TEAMSPEAK: - identity = new TeaSpeakIdentity(undefined, undefined); - break; - } - if(!identity) - return undefined; - - try { - await identity.decode(data) - } catch(error) { - /* todo better error handling! */ - console.error(error); - return undefined; +namespace profiles.identities { + export enum IdentitifyType { + TEAFORO, + TEAMSPEAK, + NICKNAME } - return identity; -} + export interface Identity { + fallback_name(): string | undefined ; + uid() : string; + type() : IdentitifyType; -export function create_identity(type: IdentitifyType) { - let identity: Identity; - switch (type) { - case IdentitifyType.NICKNAME: - identity = new NameIdentity(); - break; - case IdentitifyType.TEAFORO: - identity = new TeaForumIdentity(undefined); - break; - case IdentitifyType.TEAMSPEAK: - identity = new TeaSpeakIdentity(undefined, undefined); - break; - } - return identity; -} + valid() : boolean; -export class HandshakeCommandHandler extends AbstractCommandHandler { - readonly handle: T; + encode?() : string; + decode(data: string) : Promise; - constructor(connection: AbstractServerConnection, handle: T) { - super(connection); - this.handle = handle; + spawn_identity_handshake_handler(connection: connection.AbstractServerConnection) : connection.HandshakeIdentityHandler; } - - handle_command(command: ServerCommand): boolean { - if($.isFunction(this[command.command])) - this[command.command](command.arguments); - else if(command.command == "error") { - return false; - } else { - console.warn(tr("Received unknown command while handshaking (%o)"), command); + export async function decode_identity(type: IdentitifyType, data: string) : Promise { + let identity: Identity; + switch (type) { + case IdentitifyType.NICKNAME: + identity = new NameIdentity(); + break; + case IdentitifyType.TEAFORO: + identity = new TeaForumIdentity(undefined); + break; + case IdentitifyType.TEAMSPEAK: + identity = new TeaSpeakIdentity(undefined, undefined); + break; } - return true; - } -} + if(!identity) + return undefined; -export abstract class AbstractHandshakeIdentityHandler implements connection.HandshakeIdentityHandler { - connection: AbstractServerConnection; + try { + await identity.decode(data) + } catch(error) { + /* todo better error handling! */ + console.error(error); + return undefined; + } - protected callbacks: ((success: boolean, message?: string) => any)[] = []; - - protected constructor(connection: AbstractServerConnection) { - this.connection = connection; + return identity; } - register_callback(callback: (success: boolean, message?: string) => any) { - this.callbacks.push(callback); + export function create_identity(type: IdentitifyType) { + let identity: Identity; + switch (type) { + case IdentitifyType.NICKNAME: + identity = new NameIdentity(); + break; + case IdentitifyType.TEAFORO: + identity = new TeaForumIdentity(undefined); + break; + case IdentitifyType.TEAMSPEAK: + identity = new TeaSpeakIdentity(undefined, undefined); + break; + } + return identity; } - abstract start_handshake(); + export class HandshakeCommandHandler extends connection.AbstractCommandHandler { + readonly handle: T; - protected trigger_success() { - for(const callback of this.callbacks) - callback(true); + constructor(connection: connection.AbstractServerConnection, handle: T) { + super(connection); + this.handle = handle; + } + + + handle_command(command: connection.ServerCommand): boolean { + if($.isFunction(this[command.command])) + this[command.command](command.arguments); + else if(command.command == "error") { + return false; + } else { + console.warn(tr("Received unknown command while handshaking (%o)"), command); + } + return true; + } } - protected trigger_fail(message: string) { - for(const callback of this.callbacks) - callback(false, message); + export abstract class AbstractHandshakeIdentityHandler implements connection.HandshakeIdentityHandler { + connection: connection.AbstractServerConnection; + + protected callbacks: ((success: boolean, message?: string) => any)[] = []; + + protected constructor(connection: connection.AbstractServerConnection) { + this.connection = connection; + } + + register_callback(callback: (success: boolean, message?: string) => any) { + this.callbacks.push(callback); + } + + abstract start_handshake(); + + protected trigger_success() { + for(const callback of this.callbacks) + callback(true); + } + + protected trigger_fail(message: string) { + for(const callback of this.callbacks) + callback(false, message); + } } } \ No newline at end of file diff --git a/shared/js/profiles/identities/NameIdentity.ts b/shared/js/profiles/identities/NameIdentity.ts index 876f6c46..51cbefc7 100644 --- a/shared/js/profiles/identities/NameIdentity.ts +++ b/shared/js/profiles/identities/NameIdentity.ts @@ -1,92 +1,88 @@ -import {CommandResult} from "../../connection/ServerConnectionDeclaration"; -import {log, LogCategory} from "../../log"; -import {AbstractServerConnection} from "../../connection/ConnectionBase"; -import {connection} from "../../connection/HandshakeHandler"; +/// -import HandshakeIdentityHandler = connection.HandshakeIdentityHandler; -import {AbstractHandshakeIdentityHandler, HandshakeCommandHandler, IdentitifyType, Identity} from "../Identity"; +namespace profiles.identities { + class NameHandshakeHandler extends AbstractHandshakeIdentityHandler { + readonly identity: NameIdentity; + handler: HandshakeCommandHandler; -class NameHandshakeHandler extends AbstractHandshakeIdentityHandler { - readonly identity: NameIdentity; - handler: HandshakeCommandHandler; + constructor(connection: connection.AbstractServerConnection, identity: profiles.identities.NameIdentity) { + super(connection); + this.identity = identity; - constructor(connection: AbstractServerConnection, identity: NameIdentity) { - super(connection); - this.identity = identity; + this.handler = new HandshakeCommandHandler(connection, this); + this.handler["handshakeidentityproof"] = () => this.trigger_fail("server requested unexpected proof"); + } - this.handler = new HandshakeCommandHandler(connection, this); - this.handler["handshakeidentityproof"] = () => this.trigger_fail("server requested unexpected proof"); + start_handshake() { + this.connection.command_handler_boss().register_handler(this.handler); + this.connection.send_command("handshakebegin", { + intention: 0, + authentication_method: this.identity.type(), + client_nickname: this.identity.name() + }).catch(error => { + log.error(LogCategory.IDENTITIES, tr("Failed to initialize name based handshake. Error: %o"), error); + if(error instanceof CommandResult) + error = error.extra_message || error.message; + this.trigger_fail("failed to execute begin (" + error + ")"); + }).then(() => this.trigger_success()); + } + + protected trigger_fail(message: string) { + this.connection.command_handler_boss().unregister_handler(this.handler); + super.trigger_fail(message); + } + + protected trigger_success() { + this.connection.command_handler_boss().unregister_handler(this.handler); + super.trigger_success(); + } } - start_handshake() { - this.connection.command_handler_boss().register_handler(this.handler); - this.connection.send_command("handshakebegin", { - intention: 0, - authentication_method: this.identity.type(), - client_nickname: this.identity.name() - }).catch(error => { - log.error(LogCategory.IDENTITIES, tr("Failed to initialize name based handshake. Error: %o"), error); - if(error instanceof CommandResult) - error = error.extra_message || error.message; - this.trigger_fail("failed to execute begin (" + error + ")"); - }).then(() => this.trigger_success()); - } + export class NameIdentity implements Identity { + private _name: string; - protected trigger_fail(message: string) { - this.connection.command_handler_boss().unregister_handler(this.handler); - super.trigger_fail(message); - } + constructor(name?: string) { + this._name = name; + } - protected trigger_success() { - this.connection.command_handler_boss().unregister_handler(this.handler); - super.trigger_success(); - } -} + set_name(name: string) { this._name = name; } -export class NameIdentity implements Identity { - private _name: string; + name() : string { return this._name; } - constructor(name?: string) { - this._name = name; - } + fallback_name(): string | undefined { + return this._name; + } - set_name(name: string) { this._name = name; } + uid(): string { + return btoa(this._name); //FIXME hash! + } - name() : string { return this._name; } + type(): IdentitifyType { + return IdentitifyType.NICKNAME; + } - fallback_name(): string | undefined { - return this._name; - } + valid(): boolean { + return this._name != undefined && this._name.length >= 5; + } - uid(): string { - return btoa(this._name); //FIXME hash! - } + decode(data) : Promise { + data = JSON.parse(data); + if(data.version !== 1) + throw "invalid version"; - type(): IdentitifyType { - return IdentitifyType.NICKNAME; - } + this._name = data["name"]; + return; + } - valid(): boolean { - return this._name != undefined && this._name.length >= 5; - } + encode?() : string { + return JSON.stringify({ + version: 1, + name: this._name + }); + } - decode(data) : Promise { - data = JSON.parse(data); - if(data.version !== 1) - throw "invalid version"; - - this._name = data["name"]; - return; - } - - encode?() : string { - return JSON.stringify({ - version: 1, - name: this._name - }); - } - - spawn_identity_handshake_handler(connection: AbstractServerConnection) : HandshakeIdentityHandler { - return new NameHandshakeHandler(connection, this); + spawn_identity_handshake_handler(connection: connection.AbstractServerConnection) : connection.HandshakeIdentityHandler { + return new NameHandshakeHandler(connection, this); + } } } \ No newline at end of file diff --git a/shared/js/profiles/identities/TeaForumIdentity.ts b/shared/js/profiles/identities/TeaForumIdentity.ts index d9224645..2f44e31e 100644 --- a/shared/js/profiles/identities/TeaForumIdentity.ts +++ b/shared/js/profiles/identities/TeaForumIdentity.ts @@ -1,126 +1,122 @@ -import {AbstractServerConnection} from "../../connection/ConnectionBase"; -import {log, LogCategory} from "../../log"; -import {CommandResult} from "../../connection/ServerConnectionDeclaration"; -import {forum} from "./teaspeak-forum"; -import {connection} from "../../connection/HandshakeHandler"; -import HandshakeIdentityHandler = connection.HandshakeIdentityHandler; -import {AbstractHandshakeIdentityHandler, HandshakeCommandHandler, IdentitifyType, Identity} from "../Identity"; +/// -class TeaForumHandshakeHandler extends AbstractHandshakeIdentityHandler { - readonly identity: TeaForumIdentity; - handler: HandshakeCommandHandler; +namespace profiles.identities { + class TeaForumHandshakeHandler extends AbstractHandshakeIdentityHandler { + readonly identity: TeaForumIdentity; + handler: HandshakeCommandHandler; - constructor(connection: AbstractServerConnection, identity: TeaForumIdentity) { - super(connection); - this.identity = identity; - this.handler = new HandshakeCommandHandler(connection, this); - this.handler["handshakeidentityproof"] = this.handle_proof.bind(this); + constructor(connection: connection.AbstractServerConnection, identity: profiles.identities.TeaForumIdentity) { + super(connection); + this.identity = identity; + this.handler = new HandshakeCommandHandler(connection, this); + this.handler["handshakeidentityproof"] = this.handle_proof.bind(this); + } + + start_handshake() { + this.connection.command_handler_boss().register_handler(this.handler); + this.connection.send_command("handshakebegin", { + intention: 0, + authentication_method: this.identity.type(), + data: this.identity.data().data_json() + }).catch(error => { + log.error(LogCategory.IDENTITIES, tr("Failed to initialize TeaForum based handshake. Error: %o"), error); + + if(error instanceof CommandResult) + error = error.extra_message || error.message; + this.trigger_fail("failed to execute begin (" + error + ")"); + }); + } + + + private handle_proof(json) { + this.connection.send_command("handshakeindentityproof", { + proof: this.identity.data().data_sign() + }).catch(error => { + log.error(LogCategory.IDENTITIES, tr("Failed to proof the identity. Error: %o"), error); + + if(error instanceof CommandResult) + error = error.extra_message || error.message; + this.trigger_fail("failed to execute proof (" + error + ")"); + }).then(() => this.trigger_success()); + } + + protected trigger_fail(message: string) { + this.connection.command_handler_boss().unregister_handler(this.handler); + super.trigger_fail(message); + } + + protected trigger_success() { + this.connection.command_handler_boss().unregister_handler(this.handler); + super.trigger_success(); + } } - start_handshake() { - this.connection.command_handler_boss().register_handler(this.handler); - this.connection.send_command("handshakebegin", { - intention: 0, - authentication_method: this.identity.type(), - data: this.identity.data().data_json() - }).catch(error => { - log.error(LogCategory.IDENTITIES, tr("Failed to initialize TeaForum based handshake. Error: %o"), error); + export class TeaForumIdentity implements Identity { + private readonly identity_data: forum.Data; - if(error instanceof CommandResult) - error = error.extra_message || error.message; - this.trigger_fail("failed to execute begin (" + error + ")"); - }); + valid() : boolean { + return !!this.identity_data && !this.identity_data.is_expired(); + } + + constructor(data: forum.Data) { + this.identity_data = data; + } + + data() : forum.Data { + return this.identity_data; + } + + decode(data) : Promise { + data = JSON.parse(data); + if(data.version !== 1) + throw "invalid version"; + + return; + } + + encode() : string { + return JSON.stringify({ + version: 1 + }); + } + + spawn_identity_handshake_handler(connection: connection.AbstractServerConnection) : connection.HandshakeIdentityHandler { + return new TeaForumHandshakeHandler(connection, this); + } + + fallback_name(): string | undefined { + return this.identity_data ? this.identity_data.name() : undefined; + } + + type(): profiles.identities.IdentitifyType { + return IdentitifyType.TEAFORO; + } + + uid(): string { + //FIXME: Real UID! + return "TeaForo#" + ((this.identity_data ? this.identity_data.name() : "Another TeaSpeak user")); + } } + let static_identity: TeaForumIdentity; - private handle_proof(json) { - this.connection.send_command("handshakeindentityproof", { - proof: this.identity.data().data_sign() - }).catch(error => { - log.error(LogCategory.IDENTITIES, tr("Failed to proof the identity. Error: %o"), error); - - if(error instanceof CommandResult) - error = error.extra_message || error.message; - this.trigger_fail("failed to execute proof (" + error + ")"); - }).then(() => this.trigger_success()); + export function set_static_identity(identity: TeaForumIdentity) { + static_identity = identity; } - protected trigger_fail(message: string) { - this.connection.command_handler_boss().unregister_handler(this.handler); - super.trigger_fail(message); + export function update_forum() { + if(forum.logged_in() && (!static_identity || static_identity.data() !== forum.data())) { + static_identity = new TeaForumIdentity(forum.data()); + } else { + static_identity = undefined; + } } - protected trigger_success() { - this.connection.command_handler_boss().unregister_handler(this.handler); - super.trigger_success(); - } -} - -export class TeaForumIdentity implements Identity { - private readonly identity_data: forum.Data; - - valid() : boolean { - return !!this.identity_data && !this.identity_data.is_expired(); + export function valid_static_forum_identity() : boolean { + return static_identity && static_identity.valid(); } - constructor(data: forum.Data) { - this.identity_data = data; + export function static_forum_identity() : TeaForumIdentity | undefined { + return static_identity; } - - data() : forum.Data { - return this.identity_data; - } - - decode(data) : Promise { - data = JSON.parse(data); - if(data.version !== 1) - throw "invalid version"; - - return; - } - - encode() : string { - return JSON.stringify({ - version: 1 - }); - } - - spawn_identity_handshake_handler(connection: AbstractServerConnection) : HandshakeIdentityHandler { - return new TeaForumHandshakeHandler(connection, this); - } - - fallback_name(): string | undefined { - return this.identity_data ? this.identity_data.name() : undefined; - } - - type(): IdentitifyType { - return IdentitifyType.TEAFORO; - } - - uid(): string { - //FIXME: Real UID! - return "TeaForo#" + ((this.identity_data ? this.identity_data.name() : "Another TeaSpeak user")); - } -} - -let static_identity: TeaForumIdentity; - -export function set_static_identity(identity: TeaForumIdentity) { - static_identity = identity; -} - -export function update_forum() { - if(forum.logged_in() && (!static_identity || static_identity.data() !== forum.data())) { - static_identity = new TeaForumIdentity(forum.data()); - } else { - static_identity = undefined; - } -} - -export function valid_static_forum_identity() : boolean { - return static_identity && static_identity.valid(); -} - -export function static_forum_identity() : TeaForumIdentity | undefined { - return static_identity; } \ No newline at end of file diff --git a/shared/js/profiles/identities/TeamSpeakIdentity.ts b/shared/js/profiles/identities/TeamSpeakIdentity.ts index 287adb02..b7c5be9c 100644 --- a/shared/js/profiles/identities/TeamSpeakIdentity.ts +++ b/shared/js/profiles/identities/TeamSpeakIdentity.ts @@ -1,115 +1,88 @@ -import {arrayBufferBase64, base64_encode_ab, str2ab8} from "../../main"; -import {sha} from "../../crypto/sha"; -import {asn1} from "../../crypto/asn1"; -import {AbstractServerConnection} from "../../connection/ConnectionBase"; -import {log, LogCategory} from "../../log"; -import {CommandResult} from "../../connection/ServerConnectionDeclaration"; -import {settings} from "../../settings"; -import {connection} from "../../connection/HandshakeHandler"; -import HandshakeIdentityHandler = connection.HandshakeIdentityHandler; -import {AbstractHandshakeIdentityHandler, HandshakeCommandHandler, IdentitifyType, Identity} from "../Identity"; +/// -export namespace CryptoHelper { - export function base64_url_encode(str){ - return str.replace(/\+/g, '-').replace(/\//g, '_').replace(/\=+$/, ''); - } +namespace profiles.identities { + export namespace CryptoHelper { + export function base64_url_encode(str){ + return str.replace(/\+/g, '-').replace(/\//g, '_').replace(/\=+$/, ''); + } - export function base64_url_decode(str: string, pad?: boolean){ - if(typeof(pad) === 'undefined' || pad) - str = (str + '===').slice(0, str.length + (str.length % 4)); - return str.replace(/-/g, '+').replace(/_/g, '/'); - } + export function base64_url_decode(str: string, pad?: boolean){ + if(typeof(pad) === 'undefined' || pad) + str = (str + '===').slice(0, str.length + (str.length % 4)); + return str.replace(/-/g, '+').replace(/_/g, '/'); + } - export function arraybuffer_to_string(buf) { - return String.fromCharCode.apply(null, new Uint16Array(buf)); - } + export function arraybuffer_to_string(buf) { + return String.fromCharCode.apply(null, new Uint16Array(buf)); + } - export async function export_ecc_key(crypto_key: CryptoKey, public_key: boolean) { - /* - Tomcrypt public key export: - if (type == PK_PRIVATE) { - flags[0] = 1; - err = der_encode_sequence_multi(out, outlen, - LTC_ASN1_BIT_STRING, 1UL, flags, - LTC_ASN1_SHORT_INTEGER, 1UL, &key_size, - LTC_ASN1_INTEGER, 1UL, key->pubkey.x, - LTC_ASN1_INTEGER, 1UL, key->pubkey.y, - LTC_ASN1_INTEGER, 1UL, key->k, - LTC_ASN1_EOL, 0UL, NULL); - } else { - flags[0] = 0; - err = der_encode_sequence_multi(out, outlen, - LTC_ASN1_BIT_STRING, 1UL, flags, - LTC_ASN1_SHORT_INTEGER, 1UL, &key_size, - LTC_ASN1_INTEGER, 1UL, key->pubkey.x, - LTC_ASN1_INTEGER, 1UL, key->pubkey.y, - LTC_ASN1_EOL, 0UL, NULL); + export async function export_ecc_key(crypto_key: CryptoKey, public_key: boolean) { + /* + Tomcrypt public key export: + if (type == PK_PRIVATE) { + flags[0] = 1; + err = der_encode_sequence_multi(out, outlen, + LTC_ASN1_BIT_STRING, 1UL, flags, + LTC_ASN1_SHORT_INTEGER, 1UL, &key_size, + LTC_ASN1_INTEGER, 1UL, key->pubkey.x, + LTC_ASN1_INTEGER, 1UL, key->pubkey.y, + LTC_ASN1_INTEGER, 1UL, key->k, + LTC_ASN1_EOL, 0UL, NULL); + } else { + flags[0] = 0; + err = der_encode_sequence_multi(out, outlen, + LTC_ASN1_BIT_STRING, 1UL, flags, + LTC_ASN1_SHORT_INTEGER, 1UL, &key_size, + LTC_ASN1_INTEGER, 1UL, key->pubkey.x, + LTC_ASN1_INTEGER, 1UL, key->pubkey.y, + LTC_ASN1_EOL, 0UL, NULL); + } + + */ + + const key_data = await crypto.subtle.exportKey("jwk", crypto_key); + + let index = 0; + const length = public_key ? 79 : 114; /* max lengths! Depends on the padding could be less */ + const buffer = new Uint8Array(length); /* fixed ASN1 length */ + { /* the initial sequence */ + buffer[index++] = 0x30; /* type */ + buffer[index++] = 0x00; /* we will set the sequence length later */ } - - */ - - const key_data = await crypto.subtle.exportKey("jwk", crypto_key); - - let index = 0; - const length = public_key ? 79 : 114; /* max lengths! Depends on the padding could be less */ - const buffer = new Uint8Array(length); /* fixed ASN1 length */ - { /* the initial sequence */ - buffer[index++] = 0x30; /* type */ - buffer[index++] = 0x00; /* we will set the sequence length later */ - } - { /* the flags bit string */ - buffer[index++] = 0x03; /* type */ - buffer[index++] = 0x02; /* length */ - buffer[index++] = 0x07; /* data */ - buffer[index++] = public_key ? 0x00 : 0x80; /* flag 1 or 0 (1 = private key)*/ - } - { /* key size (const 32 for P-256) */ - buffer[index++] = 0x02; /* type */ - buffer[index++] = 0x01; /* length */ - buffer[index++] = 0x20; - } - try { /* Public kex X */ - buffer[index++] = 0x02; /* type */ - buffer[index++] = 0x20; /* length */ - - const raw = atob(base64_url_decode(key_data.x, false)); - if(raw.charCodeAt(0) > 0x7F) { - buffer[index - 1] += 1; - buffer[index++] = 0; + { /* the flags bit string */ + buffer[index++] = 0x03; /* type */ + buffer[index++] = 0x02; /* length */ + buffer[index++] = 0x07; /* data */ + buffer[index++] = public_key ? 0x00 : 0x80; /* flag 1 or 0 (1 = private key)*/ } - - for(let i = 0; i < 32; i++) - buffer[index++] = raw.charCodeAt(i); - } catch(error) { - if(error instanceof DOMException) - throw "failed to parse x coordinate (invalid base64)"; - throw error; - } - - try { /* Public kex Y */ - buffer[index++] = 0x02; /* type */ - buffer[index++] = 0x20; /* length */ - - const raw = atob(base64_url_decode(key_data.y, false)); - if(raw.charCodeAt(0) > 0x7F) { - buffer[index - 1] += 1; - buffer[index++] = 0; + { /* key size (const 32 for P-256) */ + buffer[index++] = 0x02; /* type */ + buffer[index++] = 0x01; /* length */ + buffer[index++] = 0x20; } - - for(let i = 0; i < 32; i++) - buffer[index++] = raw.charCodeAt(i); - } catch(error) { - if(error instanceof DOMException) - throw "failed to parse y coordinate (invalid base64)"; - throw error; - } - - if(!public_key) { - try { /* Public kex K */ + try { /* Public kex X */ buffer[index++] = 0x02; /* type */ buffer[index++] = 0x20; /* length */ - const raw = atob(base64_url_decode(key_data.d, false)); + const raw = atob(base64_url_decode(key_data.x, false)); + if(raw.charCodeAt(0) > 0x7F) { + buffer[index - 1] += 1; + buffer[index++] = 0; + } + + for(let i = 0; i < 32; i++) + buffer[index++] = raw.charCodeAt(i); + } catch(error) { + if(error instanceof DOMException) + throw "failed to parse x coordinate (invalid base64)"; + throw error; + } + + try { /* Public kex Y */ + buffer[index++] = 0x02; /* type */ + buffer[index++] = 0x20; /* length */ + + const raw = atob(base64_url_decode(key_data.y, false)); if(raw.charCodeAt(0) > 0x7F) { buffer[index - 1] += 1; buffer[index++] = 0; @@ -122,284 +95,223 @@ export namespace CryptoHelper { throw "failed to parse y coordinate (invalid base64)"; throw error; } + + if(!public_key) { + try { /* Public kex K */ + buffer[index++] = 0x02; /* type */ + buffer[index++] = 0x20; /* length */ + + const raw = atob(base64_url_decode(key_data.d, false)); + if(raw.charCodeAt(0) > 0x7F) { + buffer[index - 1] += 1; + buffer[index++] = 0; + } + + for(let i = 0; i < 32; i++) + buffer[index++] = raw.charCodeAt(i); + } catch(error) { + if(error instanceof DOMException) + throw "failed to parse y coordinate (invalid base64)"; + throw error; + } + } + + buffer[1] = index - 2; /* set the final sequence length */ + + return base64_encode_ab(buffer.buffer.slice(0, index)); } - buffer[1] = index - 2; /* set the final sequence length */ - - return base64_encode_ab(buffer.buffer.slice(0, index)); - } - - const crypt_key = "b9dfaa7bee6ac57ac7b65f1094a1c155e747327bc2fe5d51c512023fe54a280201004e90ad1daaae1075d53b7d571c30e063b5a62a4a017bb394833aa0983e6e"; - function c_strlen(buffer: Uint8Array, offset: number) : number { - let index = 0; - while(index + offset < buffer.length && buffer[index + offset] != 0) - index++; - return index; - } - - export async function decrypt_ts_identity(buffer: Uint8Array) : Promise { - /* buffer could contains a zero! */ - const hash = new Uint8Array(await sha.sha1(buffer.buffer.slice(20, 20 + c_strlen(buffer, 20)))); - for(let i = 0; i < 20; i++) - buffer[i] ^= hash[i]; - - const length = Math.min(buffer.length, 100); - for(let i = 0; i < length; i++) - buffer[i] ^= crypt_key.charCodeAt(i); - - return arraybuffer_to_string(buffer); - } - - export async function encrypt_ts_identity(buffer: Uint8Array) : Promise { - const length = Math.min(buffer.length, 100); - for(let i = 0; i < length; i++) - buffer[i] ^= crypt_key.charCodeAt(i); - - const hash = new Uint8Array(await sha.sha1(buffer.buffer.slice(20, 20 + c_strlen(buffer, 20)))); - for(let i = 0; i < 20; i++) - buffer[i] ^= hash[i]; - - return base64_encode_ab(buffer); - } - - /** - * @param buffer base64 encoded ASN.1 string - */ - export function decode_tomcrypt_key(buffer: string) { - let decoded; - - try { - decoded = asn1.decode(atob(buffer)); - } catch(error) { - if(error instanceof DOMException) - throw "failed to parse key buffer (invalid base64)"; - throw error; + const crypt_key = "b9dfaa7bee6ac57ac7b65f1094a1c155e747327bc2fe5d51c512023fe54a280201004e90ad1daaae1075d53b7d571c30e063b5a62a4a017bb394833aa0983e6e"; + function c_strlen(buffer: Uint8Array, offset: number) : number { + let index = 0; + while(index + offset < buffer.length && buffer[index + offset] != 0) + index++; + return index; } - let {x, y, k} = { - x: decoded.children[2].content(Infinity, asn1.TagType.VisibleString), - y: decoded.children[3].content(Infinity, asn1.TagType.VisibleString), - k: decoded.children[4].content(Infinity, asn1.TagType.VisibleString) - }; + export async function decrypt_ts_identity(buffer: Uint8Array) : Promise { + /* buffer could contains a zero! */ + const hash = new Uint8Array(await sha.sha1(buffer.buffer.slice(20, 20 + c_strlen(buffer, 20)))); + for(let i = 0; i < 20; i++) + buffer[i] ^= hash[i]; - if(x.length > 32) { - if(x.charCodeAt(0) != 0) - throw "Invalid X coordinate! (Too long)"; - x = x.substr(1); + const length = Math.min(buffer.length, 100); + for(let i = 0; i < length; i++) + buffer[i] ^= crypt_key.charCodeAt(i); + + return arraybuffer_to_string(buffer); } - if(y.length > 32) { - if(y.charCodeAt(0) != 0) - throw "Invalid Y coordinate! (Too long)"; - y = y.substr(1); + export async function encrypt_ts_identity(buffer: Uint8Array) : Promise { + const length = Math.min(buffer.length, 100); + for(let i = 0; i < length; i++) + buffer[i] ^= crypt_key.charCodeAt(i); + + const hash = new Uint8Array(await sha.sha1(buffer.buffer.slice(20, 20 + c_strlen(buffer, 20)))); + for(let i = 0; i < 20; i++) + buffer[i] ^= hash[i]; + + return base64_encode_ab(buffer); } - if(k.length > 32) { - if(k.charCodeAt(0) != 0) - throw "Invalid private coordinate! (Too long)"; - k = k.substr(1); + /** + * @param buffer base64 encoded ASN.1 string + */ + export function decode_tomcrypt_key(buffer: string) { + let decoded; + + try { + decoded = asn1.decode(atob(buffer)); + } catch(error) { + if(error instanceof DOMException) + throw "failed to parse key buffer (invalid base64)"; + throw error; + } + + let {x, y, k} = { + x: decoded.children[2].content(Infinity, asn1.TagType.VisibleString), + y: decoded.children[3].content(Infinity, asn1.TagType.VisibleString), + k: decoded.children[4].content(Infinity, asn1.TagType.VisibleString) + }; + + if(x.length > 32) { + if(x.charCodeAt(0) != 0) + throw "Invalid X coordinate! (Too long)"; + x = x.substr(1); + } + + if(y.length > 32) { + if(y.charCodeAt(0) != 0) + throw "Invalid Y coordinate! (Too long)"; + y = y.substr(1); + } + + if(k.length > 32) { + if(k.charCodeAt(0) != 0) + throw "Invalid private coordinate! (Too long)"; + k = k.substr(1); + } + + /* + console.log("Key x: %s (%d)", btoa(x), x.length); + console.log("Key y: %s (%d)", btoa(y), y.length); + console.log("Key k: %s (%d)", btoa(k), k.length); + */ + return { + crv: "P-256", + d: base64_url_encode(btoa(k)), + x: base64_url_encode(btoa(x)), + y: base64_url_encode(btoa(y)), + + ext: true, + key_ops:["deriveKey", "sign"], + kty:"EC", + }; } - - /* - console.log("Key x: %s (%d)", btoa(x), x.length); - console.log("Key y: %s (%d)", btoa(y), y.length); - console.log("Key k: %s (%d)", btoa(k), k.length); - */ - return { - crv: "P-256", - d: base64_url_encode(btoa(k)), - x: base64_url_encode(btoa(x)), - y: base64_url_encode(btoa(y)), - - ext: true, - key_ops:["deriveKey", "sign"], - kty:"EC", - }; - } -} - -class TeaSpeakHandshakeHandler extends AbstractHandshakeIdentityHandler { - identity: TeaSpeakIdentity; - handler: HandshakeCommandHandler; - - constructor(connection: AbstractServerConnection, identity: TeaSpeakIdentity) { - super(connection); - this.identity = identity; - this.handler = new HandshakeCommandHandler(connection, this); - this.handler["handshakeidentityproof"] = this.handle_proof.bind(this); } - start_handshake() { - this.connection.command_handler_boss().register_handler(this.handler); - this.connection.send_command("handshakebegin", { - intention: 0, - authentication_method: this.identity.type(), - publicKey: this.identity.public_key - }).catch(error => { - log.error(LogCategory.IDENTITIES, tr("Failed to initialize TeamSpeak based handshake. Error: %o"), error); + class TeaSpeakHandshakeHandler extends AbstractHandshakeIdentityHandler { + identity: TeaSpeakIdentity; + handler: HandshakeCommandHandler; - if(error instanceof CommandResult) - error = error.extra_message || error.message; - this.trigger_fail("failed to execute begin (" + error + ")"); - }); - } - - private handle_proof(json) { - if(!json[0]["digest"]) { - this.trigger_fail("server too old"); - return; + constructor(connection: connection.AbstractServerConnection, identity: TeaSpeakIdentity) { + super(connection); + this.identity = identity; + this.handler = new HandshakeCommandHandler(connection, this); + this.handler["handshakeidentityproof"] = this.handle_proof.bind(this); } - this.identity.sign_message(json[0]["message"], json[0]["digest"]).then(proof => { - this.connection.send_command("handshakeindentityproof", {proof: proof}).catch(error => { - log.error(LogCategory.IDENTITIES, tr("Failed to proof the identity. Error: %o"), error); + start_handshake() { + this.connection.command_handler_boss().register_handler(this.handler); + this.connection.send_command("handshakebegin", { + intention: 0, + authentication_method: this.identity.type(), + publicKey: this.identity.public_key + }).catch(error => { + log.error(LogCategory.IDENTITIES, tr("Failed to initialize TeamSpeak based handshake. Error: %o"), error); if(error instanceof CommandResult) error = error.extra_message || error.message; - this.trigger_fail("failed to execute proof (" + error + ")"); - }).then(() => this.trigger_success()); - }).catch(error => { - this.trigger_fail("failed to sign message"); - }); - } - - protected trigger_fail(message: string) { - this.connection.command_handler_boss().unregister_handler(this.handler); - super.trigger_fail(message); - } - - protected trigger_success() { - this.connection.command_handler_boss().unregister_handler(this.handler); - super.trigger_success(); - } -} - -class IdentityPOWWorker { - private _worker: Worker; - private _current_hash: string; - private _best_level: number; - - async initialize(key: string) { - this._worker = new Worker(settings.static("worker_directory", "js/workers/") + "WorkerPOW.js"); - - /* initialize */ - await new Promise((resolve, reject) => { - const timeout_id = setTimeout(() => reject("timeout"), 1000); - - this._worker.onmessage = event => { - clearTimeout(timeout_id); - - if(!event.data) { - reject("invalid data"); - return; - } - - if(!event.data.success) { - reject("initialize failed (" + event.data.success + " | " + (event.data.message || "unknown eroror") + ")"); - return; - } - - this._worker.onmessage = event => this.handle_message(event.data); - resolve(); - }; - this._worker.onerror = event => { - log.error(LogCategory.IDENTITIES, tr("POW Worker error %o"), event); - clearTimeout(timeout_id); - reject("Failed to load worker (" + event.message + ")"); - }; - }); - - /* set data */ - await new Promise((resolve, reject) => { - this._worker.postMessage({ - type: "set_data", - private_key: key, - code: "set_data" + this.trigger_fail("failed to execute begin (" + error + ")"); }); + } - const timeout_id = setTimeout(() => reject("timeout (data)"), 1000); + private handle_proof(json) { + if(!json[0]["digest"]) { + this.trigger_fail("server too old"); + return; + } - this._worker.onmessage = event => { - clearTimeout(timeout_id); + this.identity.sign_message(json[0]["message"], json[0]["digest"]).then(proof => { + this.connection.send_command("handshakeindentityproof", {proof: proof}).catch(error => { + log.error(LogCategory.IDENTITIES, tr("Failed to proof the identity. Error: %o"), error); - if (!event.data) { - reject("invalid data"); - return; - } - - if (!event.data.success) { - reject("initialize of data failed (" + event.data.success + " | " + (event.data.message || "unknown eroror") + ")"); - return; - } - - this._worker.onmessage = event => this.handle_message(event.data); - resolve(); - }; - }); - } - - async mine(hash: string, iterations: number, target: number, timeout?: number) : Promise { - this._current_hash = hash; - if(target < this._best_level) - return true; - - return await new Promise((resolve, reject) => { - this._worker.postMessage({ - type: "mine", - hash: this._current_hash, - iterations: iterations, - target: target, - code: "mine" + if(error instanceof CommandResult) + error = error.extra_message || error.message; + this.trigger_fail("failed to execute proof (" + error + ")"); + }).then(() => this.trigger_success()); + }).catch(error => { + this.trigger_fail("failed to sign message"); }); + } - const timeout_id = setTimeout(() => reject("timeout (mine)"), timeout || 5000); + protected trigger_fail(message: string) { + this.connection.command_handler_boss().unregister_handler(this.handler); + super.trigger_fail(message); + } - this._worker.onmessage = event => { - this._worker.onmessage = event => this.handle_message(event.data); - - clearTimeout(timeout_id); - if (!event.data) { - reject("invalid data"); - return; - } - - if (!event.data.success) { - reject("mining failed (" + event.data.success + " | " + (event.data.message || "unknown eroror") + ")"); - return; - } - - if(event.data.result) { - this._best_level = event.data.level; - this._current_hash = event.data.hash; - resolve(true); - } else { - resolve(false); /* no result */ - } - }; - }); + protected trigger_success() { + this.connection.command_handler_boss().unregister_handler(this.handler); + super.trigger_success(); + } } - current_hash() : string { - return this._current_hash; - } + class IdentityPOWWorker { + private _worker: Worker; + private _current_hash: string; + private _best_level: number; - current_level() : number { - return this._best_level; - } + async initialize(key: string) { + this._worker = new Worker(settings.static("worker_directory", "js/workers/") + "WorkerPOW.js"); - async finalize(timeout?: number) { - try { + /* initialize */ await new Promise((resolve, reject) => { - this._worker.postMessage({ - type: "finalize", - code: "finalize" - }); - - const timeout_id = setTimeout(() => reject("timeout"), timeout || 250); + const timeout_id = setTimeout(() => reject("timeout"), 1000); this._worker.onmessage = event => { - this._worker.onmessage = event => this.handle_message(event.data); + clearTimeout(timeout_id); + if(!event.data) { + reject("invalid data"); + return; + } + + if(!event.data.success) { + reject("initialize failed (" + event.data.success + " | " + (event.data.message || "unknown eroror") + ")"); + return; + } + + this._worker.onmessage = event => this.handle_message(event.data); + resolve(); + }; + this._worker.onerror = event => { + log.error(LogCategory.IDENTITIES, tr("POW Worker error %o"), event); + clearTimeout(timeout_id); + reject("Failed to load worker (" + event.message + ")"); + }; + }); + + /* set data */ + await new Promise((resolve, reject) => { + this._worker.postMessage({ + type: "set_data", + private_key: key, + code: "set_data" + }); + + const timeout_id = setTimeout(() => reject("timeout (data)"), 1000); + + this._worker.onmessage = event => { clearTimeout(timeout_id); if (!event.data) { @@ -408,471 +320,552 @@ class IdentityPOWWorker { } if (!event.data.success) { - reject("failed to finalize (" + event.data.success + " | " + (event.data.message || "unknown eroror") + ")"); + reject("initialize of data failed (" + event.data.success + " | " + (event.data.message || "unknown eroror") + ")"); return; } + this._worker.onmessage = event => this.handle_message(event.data); resolve(); }; }); - } catch(error) { - log.error(LogCategory.IDENTITIES, tr("Failed to finalize POW worker! (%o)"), error); } - this._worker.terminate(); - this._worker = undefined; - } + async mine(hash: string, iterations: number, target: number, timeout?: number) : Promise { + this._current_hash = hash; + if(target < this._best_level) + return true; - private handle_message(message: any) { - log.info(LogCategory.IDENTITIES, tr("Received message: %o"), message); - } -} + return await new Promise((resolve, reject) => { + this._worker.postMessage({ + type: "mine", + hash: this._current_hash, + iterations: iterations, + target: target, + code: "mine" + }); -export class TeaSpeakIdentity implements Identity { - static async generate_new() : Promise { - let key: CryptoKeyPair; - try { - key = await crypto.subtle.generateKey({name:'ECDH', namedCurve: 'P-256'}, true, ["deriveKey"]); - } catch(e) { - log.error(LogCategory.IDENTITIES, tr("Could not generate a new key: %o"), e); - throw "Failed to generate keypair"; - } - const private_key = await CryptoHelper.export_ecc_key(key.privateKey, false); + const timeout_id = setTimeout(() => reject("timeout (mine)"), timeout || 5000); - const identity = new TeaSpeakIdentity(private_key, "0", undefined, false); - await identity.initialize(); - return identity; - } + this._worker.onmessage = event => { + this._worker.onmessage = event => this.handle_message(event.data); - static async import_ts(ts_string: string, ini?: boolean) : Promise { - const parse_string = string => { - /* parsing without INI structure */ - const V_index = string.indexOf('V'); - if(V_index == -1) throw "invalid input (missing V)"; + clearTimeout(timeout_id); + if (!event.data) { + reject("invalid data"); + return; + } - return { - hash: string.substr(0, V_index), - data: string.substr(V_index + 1), - name: "TeaSpeak user" - } - }; + if (!event.data.success) { + reject("mining failed (" + event.data.success + " | " + (event.data.message || "unknown eroror") + ")"); + return; + } - const {hash, data, name} = (!ini ? () => parse_string(ts_string) : () => { - /* parsing with INI structure */ - let identity: string, name: string; - - for(const line of ts_string.split("\n")) { - if(line.startsWith("identity=")) - identity = line.substr(9); - else if(line.startsWith("nickname=")) - name = line.substr(9); - } - - if(!identity) throw "missing identity keyword"; - identity = identity.match(/^"?([0-9]+V[0-9a-zA-Z+\/]+[=]+)"?$/)[1]; - if(!identity) throw "invalid identity key value"; - - const result = parse_string(identity); - result.name = name || result.name; - return result; - })(); - - if(!ts_string.match(/[0-9]+/g)) throw "invalid hash!"; - - let buffer; - try { - buffer = new Uint8Array(arrayBufferBase64(data)); - } catch(error) { - log.error(LogCategory.IDENTITIES, tr("Failed to decode given base64 data (%s)"), data); - throw "failed to base data (base64 decode failed)"; - } - const key64 = await CryptoHelper.decrypt_ts_identity(new Uint8Array(arrayBufferBase64(data))); - - const identity = new TeaSpeakIdentity(key64, hash, name, false); - await identity.initialize(); - return identity; - } - - hash_number: string; /* hash suffix for the private key */ - private_key: string; /* base64 representation of the private key */ - _name: string; - - public_key: string; /* only set when initialized */ - - private _initialized: boolean; - private _crypto_key: CryptoKey; - private _crypto_key_sign: CryptoKey; - - private _unique_id: string; - - constructor(private_key?: string, hash?: string, name?: string, initialize?: boolean) { - this.private_key = private_key; - this.hash_number = hash || "0"; - this._name = name; - - if(this.private_key && (typeof(initialize) === "undefined" || initialize)) { - this.initialize().catch(error => { - log.error(LogCategory.IDENTITIES, "Failed to initialize TeaSpeakIdentity (%s)", error); - this._initialized = false; + if(event.data.result) { + this._best_level = event.data.level; + this._current_hash = event.data.hash; + resolve(true); + } else { + resolve(false); /* no result */ + } + }; }); } + + current_hash() : string { + return this._current_hash; + } + + current_level() : number { + return this._best_level; + } + + async finalize(timeout?: number) { + try { + await new Promise((resolve, reject) => { + this._worker.postMessage({ + type: "finalize", + code: "finalize" + }); + + const timeout_id = setTimeout(() => reject("timeout"), timeout || 250); + + this._worker.onmessage = event => { + this._worker.onmessage = event => this.handle_message(event.data); + + clearTimeout(timeout_id); + + if (!event.data) { + reject("invalid data"); + return; + } + + if (!event.data.success) { + reject("failed to finalize (" + event.data.success + " | " + (event.data.message || "unknown eroror") + ")"); + return; + } + + resolve(); + }; + }); + } catch(error) { + log.error(LogCategory.IDENTITIES, tr("Failed to finalize POW worker! (%o)"), error); + } + + this._worker.terminate(); + this._worker = undefined; + } + + private handle_message(message: any) { + log.info(LogCategory.IDENTITIES, tr("Received message: %o"), message); + } } - fallback_name(): string | undefined { - return this._name; - } + export class TeaSpeakIdentity implements Identity { + static async generate_new() : Promise { + let key: CryptoKeyPair; + try { + key = await crypto.subtle.generateKey({name:'ECDH', namedCurve: 'P-256'}, true, ["deriveKey"]); + } catch(e) { + log.error(LogCategory.IDENTITIES, tr("Could not generate a new key: %o"), e); + throw "Failed to generate keypair"; + } + const private_key = await CryptoHelper.export_ecc_key(key.privateKey, false); - uid(): string { - return this._unique_id; - } + const identity = new TeaSpeakIdentity(private_key, "0", undefined, false); + await identity.initialize(); + return identity; + } - type(): IdentitifyType { - return IdentitifyType.TEAMSPEAK; - } + static async import_ts(ts_string: string, ini?: boolean) : Promise { + const parse_string = string => { + /* parsing without INI structure */ + const V_index = string.indexOf('V'); + if(V_index == -1) throw "invalid input (missing V)"; - valid(): boolean { - return this._initialized && !!this._crypto_key && !!this._crypto_key_sign; - } + return { + hash: string.substr(0, V_index), + data: string.substr(V_index + 1), + name: "TeaSpeak user" + } + }; - async decode(data: string) : Promise { - const json = JSON.parse(data); - if(!json) throw "invalid json"; + const {hash, data, name} = (!ini ? () => parse_string(ts_string) : () => { + /* parsing with INI structure */ + let identity: string, name: string; - if(json.version == 2) { - this.private_key = json.key; - this.hash_number = json.hash; - this._name = json.name; - } else if(json.version == 1) { - const key = json.key; - this._name = json.name; + for(const line of ts_string.split("\n")) { + if(line.startsWith("identity=")) + identity = line.substr(9); + else if(line.startsWith("nickname=")) + name = line.substr(9); + } - const clone = await TeaSpeakIdentity.import_ts(key, false); - this.private_key = clone.private_key; - this.hash_number = clone.hash_number; - } else - throw "invalid version"; + if(!identity) throw "missing identity keyword"; + identity = identity.match(/^"?([0-9]+V[0-9a-zA-Z+\/]+[=]+)"?$/)[1]; + if(!identity) throw "invalid identity key value"; - await this.initialize(); - } + const result = parse_string(identity); + result.name = name || result.name; + return result; + })(); - encode?() : string { - return JSON.stringify({ - key: this.private_key, - hash: this.hash_number, - name: this._name, - version: 2 - }); - } + if(!ts_string.match(/[0-9]+/g)) throw "invalid hash!"; - async level() : Promise { - if(!this._initialized || !this.public_key) - throw "not initialized"; + let buffer; + try { + buffer = new Uint8Array(arrayBufferBase64(data)); + } catch(error) { + log.error(LogCategory.IDENTITIES, tr("Failed to decode given base64 data (%s)"), data); + throw "failed to base data (base64 decode failed)"; + } + const key64 = await CryptoHelper.decrypt_ts_identity(new Uint8Array(arrayBufferBase64(data))); - const hash = new Uint8Array(await sha.sha1(this.public_key + this.hash_number)); + const identity = new TeaSpeakIdentity(key64, hash, name, false); + await identity.initialize(); + return identity; + } - let level = 0; - while(level < hash.byteLength && hash[level] == 0) - level++; + hash_number: string; /* hash suffix for the private key */ + private_key: string; /* base64 representation of the private key */ + _name: string; - if(level >= hash.byteLength) { - level = 256; - } else { - let byte = hash[level]; - level <<= 3; - while((byte & 0x1) == 0) { + public_key: string; /* only set when initialized */ + + private _initialized: boolean; + private _crypto_key: CryptoKey; + private _crypto_key_sign: CryptoKey; + + private _unique_id: string; + + constructor(private_key?: string, hash?: string, name?: string, initialize?: boolean) { + this.private_key = private_key; + this.hash_number = hash || "0"; + this._name = name; + + if(this.private_key && (typeof(initialize) === "undefined" || initialize)) { + this.initialize().catch(error => { + log.error(LogCategory.IDENTITIES, "Failed to initialize TeaSpeakIdentity (%s)", error); + this._initialized = false; + }); + } + } + + fallback_name(): string | undefined { + return this._name; + } + + uid(): string { + return this._unique_id; + } + + type(): IdentitifyType { + return IdentitifyType.TEAMSPEAK; + } + + valid(): boolean { + return this._initialized && !!this._crypto_key && !!this._crypto_key_sign; + } + + async decode(data: string) : Promise { + const json = JSON.parse(data); + if(!json) throw "invalid json"; + + if(json.version == 2) { + this.private_key = json.key; + this.hash_number = json.hash; + this._name = json.name; + } else if(json.version == 1) { + const key = json.key; + this._name = json.name; + + const clone = await TeaSpeakIdentity.import_ts(key, false); + this.private_key = clone.private_key; + this.hash_number = clone.hash_number; + } else + throw "invalid version"; + + await this.initialize(); + } + + encode?() : string { + return JSON.stringify({ + key: this.private_key, + hash: this.hash_number, + name: this._name, + version: 2 + }); + } + + async level() : Promise { + if(!this._initialized || !this.public_key) + throw "not initialized"; + + const hash = new Uint8Array(await sha.sha1(this.public_key + this.hash_number)); + + let level = 0; + while(level < hash.byteLength && hash[level] == 0) level++; - byte >>= 1; - } - } - return level; - } - - /** - * @param {string} a - * @param {string} b - * @description b must be smaller (in bytes) then a - */ - private string_add(a: string, b: string) { - const char_result: number[] = []; - const char_a = [...a].reverse().map(e => e.charCodeAt(0)); - const char_b = [...b].reverse().map(e => e.charCodeAt(0)); - - let carry = false; - while(char_b.length > 0) { - let result = char_b.pop_front() + char_a.pop_front() + (carry ? 1 : 0) - 48; - if((carry = result > 57)) - result -= 10; - char_result.push(result); - } - - while(char_a.length > 0) { - let result = char_a.pop_front() + (carry ? 1 : 0); - if((carry = result > 57)) - result -= 10; - char_result.push(result); - } - - if(carry) - char_result.push(49); - - return String.fromCharCode.apply(null, char_result.slice().reverse()); - } - - - async improve_level_for(time: number, threads: number) : Promise { - let active = true; - setTimeout(() => active = false, time); - - return await this.improve_level(-1, threads, () => active); - } - - async improve_level(target: number, threads: number, active_callback: () => boolean, callback_level?: (current: number) => any, callback_status?: (hash_rate: number) => any) : Promise { - if(!this._initialized || !this.public_key) - throw "not initialized"; - if(target == -1) /* get the highest level possible */ - target = 0; - else if(target <= await this.level()) - return true; - - const workers: IdentityPOWWorker[] = []; - - const iterations = 100000; - let current_hash; - const next_hash = () => { - if(!current_hash) - return (current_hash = this.hash_number); - - if(current_hash.length < iterations.toString().length) { - current_hash = this.string_add(iterations.toString(), current_hash); + if(level >= hash.byteLength) { + level = 256; } else { - current_hash = this.string_add(current_hash, iterations.toString()); + let byte = hash[level]; + level <<= 3; + while((byte & 0x1) == 0) { + level++; + byte >>= 1; + } } - return current_hash; - }; - { /* init */ - const initialize_promise: Promise[] = []; - for(let index = 0; index < threads; index++) { - const worker = new IdentityPOWWorker(); - workers.push(worker); - initialize_promise.push(worker.initialize(this.public_key)); + return level; + } + + /** + * @param {string} a + * @param {string} b + * @description b must be smaller (in bytes) then a + */ + private string_add(a: string, b: string) { + const char_result: number[] = []; + const char_a = [...a].reverse().map(e => e.charCodeAt(0)); + const char_b = [...b].reverse().map(e => e.charCodeAt(0)); + + let carry = false; + while(char_b.length > 0) { + let result = char_b.pop_front() + char_a.pop_front() + (carry ? 1 : 0) - 48; + if((carry = result > 57)) + result -= 10; + char_result.push(result); } + while(char_a.length > 0) { + let result = char_a.pop_front() + (carry ? 1 : 0); + if((carry = result > 57)) + result -= 10; + char_result.push(result); + } + + if(carry) + char_result.push(49); + + return String.fromCharCode.apply(null, char_result.slice().reverse()); + } + + + async improve_level_for(time: number, threads: number) : Promise { + let active = true; + setTimeout(() => active = false, time); + + return await this.improve_level(-1, threads, () => active); + } + + async improve_level(target: number, threads: number, active_callback: () => boolean, callback_level?: (current: number) => any, callback_status?: (hash_rate: number) => any) : Promise { + if(!this._initialized || !this.public_key) + throw "not initialized"; + if(target == -1) /* get the highest level possible */ + target = 0; + else if(target <= await this.level()) + return true; + + const workers: IdentityPOWWorker[] = []; + + const iterations = 100000; + let current_hash; + const next_hash = () => { + if(!current_hash) + return (current_hash = this.hash_number); + + if(current_hash.length < iterations.toString().length) { + current_hash = this.string_add(iterations.toString(), current_hash); + } else { + current_hash = this.string_add(current_hash, iterations.toString()); + } + return current_hash; + }; + + { /* init */ + const initialize_promise: Promise[] = []; + for(let index = 0; index < threads; index++) { + const worker = new IdentityPOWWorker(); + workers.push(worker); + initialize_promise.push(worker.initialize(this.public_key)); + } + + try { + await Promise.all(initialize_promise); + } catch(error) { + log.error(LogCategory.IDENTITIES, error); + throw "failed to initialize"; + } + } + + let result = false; + let best_level = 0; + let target_level = target > 0 ? target : await this.level() + 1; + + const worker_promise: Promise[] = []; + + const hash_timestamps: number[] = []; + let last_hashrate_update: number = 0; + + const update_hashrate = () => { + if(!callback_status) return; + const now = Date.now(); + hash_timestamps.push(now); + + if(last_hashrate_update + 1000 < now) { + last_hashrate_update = now; + + const timeout = now - 10 * 1000; /* 10s */ + const rounds = hash_timestamps.filter(e => e > timeout); + callback_status(Math.ceil((rounds.length * iterations) / Math.ceil((now - rounds[0]) / 1000))) + } + }; + try { - await Promise.all(initialize_promise); - } catch(error) { - log.error(LogCategory.IDENTITIES, error); - throw "failed to initialize"; - } - } + result = await new Promise((resolve, reject) => { + let active = true; - let result = false; - let best_level = 0; - let target_level = target > 0 ? target : await this.level() + 1; - - const worker_promise: Promise[] = []; - - const hash_timestamps: number[] = []; - let last_hashrate_update: number = 0; - - const update_hashrate = () => { - if(!callback_status) return; - const now = Date.now(); - hash_timestamps.push(now); - - if(last_hashrate_update + 1000 < now) { - last_hashrate_update = now; - - const timeout = now - 10 * 1000; /* 10s */ - const rounds = hash_timestamps.filter(e => e > timeout); - callback_status(Math.ceil((rounds.length * iterations) / Math.ceil((now - rounds[0]) / 1000))) - } - }; - - try { - result = await new Promise((resolve, reject) => { - let active = true; - - const exit = () => { - const timeout = setTimeout(() => resolve(true), 1000); - Promise.all(worker_promise).then(result => { - clearTimeout(timeout); - resolve(true); - }).catch(error => resolve(true)); - active = false; - }; - - for(const worker of workers) { - const worker_mine = () => { - if(!active) return; - - const promise = worker.mine(next_hash(), iterations, target_level); - const p = promise.then(result => { - update_hashrate(); - - worker_promise.remove(p); - - if(result.valueOf()) { - if(worker.current_level() > best_level) { - this.hash_number = worker.current_hash(); - - log.info(LogCategory.IDENTITIES, "Found new best at %s (%d). Old was %d", this.hash_number, worker.current_level(), best_level); - best_level = worker.current_level(); - if(callback_level) - callback_level(best_level); - } - - if(active) { - if(target > 0) - exit(); - else - target_level = best_level + 1; - } - } - - if(active && (active = active_callback())) - setTimeout(() => worker_mine(), 0); - else { - exit(); - } - - return Promise.resolve(); - }).catch(error => { - worker_promise.remove(p); - - log.warn(LogCategory.IDENTITIES, "POW worker error %o", error); - reject(error); - - return Promise.resolve(); - }); - - worker_promise.push(p); + const exit = () => { + const timeout = setTimeout(() => resolve(true), 1000); + Promise.all(worker_promise).then(result => { + clearTimeout(timeout); + resolve(true); + }).catch(error => resolve(true)); + active = false; }; - worker_mine(); + for(const worker of workers) { + const worker_mine = () => { + if(!active) return; + + const promise = worker.mine(next_hash(), iterations, target_level); + const p = promise.then(result => { + update_hashrate(); + + worker_promise.remove(p); + + if(result.valueOf()) { + if(worker.current_level() > best_level) { + this.hash_number = worker.current_hash(); + + log.info(LogCategory.IDENTITIES, "Found new best at %s (%d). Old was %d", this.hash_number, worker.current_level(), best_level); + best_level = worker.current_level(); + if(callback_level) + callback_level(best_level); + } + + if(active) { + if(target > 0) + exit(); + else + target_level = best_level + 1; + } + } + + if(active && (active = active_callback())) + setTimeout(() => worker_mine(), 0); + else { + exit(); + } + + return Promise.resolve(); + }).catch(error => { + worker_promise.remove(p); + + log.warn(LogCategory.IDENTITIES, "POW worker error %o", error); + reject(error); + + return Promise.resolve(); + }); + + worker_promise.push(p); + }; + + worker_mine(); + } + }); + } catch(error) { + //error already printed before reject had been called + } + + { /* shutdown */ + const finalize_promise: Promise[] = []; + for(const worker of workers) + finalize_promise.push(worker.finalize(250)); + + try { + await Promise.all(finalize_promise); + } catch(error) { + log.error(LogCategory.IDENTITIES, error); + throw "failed to finalize"; } - }); - } catch(error) { - //error already printed before reject had been called + } + + + return result; } - { /* shutdown */ - const finalize_promise: Promise[] = []; - for(const worker of workers) - finalize_promise.push(worker.finalize(250)); + private async initialize() { + if(!this.private_key) + throw "Invalid private key"; + + let jwk: any; + try { + jwk = await CryptoHelper.decode_tomcrypt_key(this.private_key); + if(!jwk) + throw "result undefined"; + } catch(error) { + throw "failed to parse key (" + error + ")"; + } try { - await Promise.all(finalize_promise); + this._crypto_key_sign = await crypto.subtle.importKey("jwk", jwk, {name:'ECDSA', namedCurve: 'P-256'}, false, ["sign"]); } catch(error) { log.error(LogCategory.IDENTITIES, error); - throw "failed to finalize"; - } - } - - - return result; - } - - private async initialize() { - if(!this.private_key) - throw "Invalid private key"; - - let jwk: any; - try { - jwk = await CryptoHelper.decode_tomcrypt_key(this.private_key); - if(!jwk) - throw "result undefined"; - } catch(error) { - throw "failed to parse key (" + error + ")"; - } - - try { - this._crypto_key_sign = await crypto.subtle.importKey("jwk", jwk, {name:'ECDSA', namedCurve: 'P-256'}, false, ["sign"]); - } catch(error) { - log.error(LogCategory.IDENTITIES, error); - throw "failed to create crypto sign key"; - } - - try { - this._crypto_key = await crypto.subtle.importKey("jwk", jwk, {name:'ECDH', namedCurve: 'P-256'}, true, ["deriveKey"]); - } catch(error) { - log.error(LogCategory.IDENTITIES, error); - throw "failed to create crypto key"; - } - - try { - this.public_key = await CryptoHelper.export_ecc_key(this._crypto_key, true); - this._unique_id = base64_encode_ab(await sha.sha1(this.public_key)); - } catch(error) { - log.error(LogCategory.IDENTITIES, error); - throw "failed to calculate unique id"; - } - - this._initialized = true; - //const public_key = await profiles.identities.CryptoHelper.export_ecc_key(key, true); - } - - async export_ts(ini?: boolean) : Promise { - if(!this.private_key) - throw "Invalid private key"; - - const identity = this.hash_number + "V" + await CryptoHelper.encrypt_ts_identity(new Uint8Array(str2ab8(this.private_key))); - if(!ini) return identity; - - return "[Identity]\n" + - "id=TeaWeb-Exported\n" + - "identity=\"" + identity + "\"\n" + - "nickname=\"" + this.fallback_name() + "\"\n" + - "phonetic_nickname="; - } - - async sign_message(message: string, hash: string = "SHA-256") : Promise { - /* bring this to libtomcrypt format */ - const sign_buffer = await crypto.subtle.sign({ - name: "ECDSA", - hash: hash - }, this._crypto_key_sign, str2ab8(message)); - const sign = new Uint8Array(sign_buffer); - /* first 32 r bits | last 32 s bits */ - - const buffer = new Uint8Array(72); - let index = 0; - - { /* the initial sequence */ - buffer[index++] = 0x30; /* type */ - buffer[index++] = 0x00; /* we will set the sequence length later */ - } - { /* integer r */ - buffer[index++] = 0x02; /* type */ - buffer[index++] = 0x20; /* length */ - - if(sign[0] > 0x7F) { - buffer[index - 1] += 1; - buffer[index++] = 0; + throw "failed to create crypto sign key"; } - for(let i = 0; i < 32; i++) - buffer[index++] = sign[i]; - } - { /* integer s */ - buffer[index++] = 0x02; /* type */ - buffer[index++] = 0x20; /* length */ - - if(sign[32] > 0x7F) { - buffer[index - 1] += 1; - buffer[index++] = 0; + try { + this._crypto_key = await crypto.subtle.importKey("jwk", jwk, {name:'ECDH', namedCurve: 'P-256'}, true, ["deriveKey"]); + } catch(error) { + log.error(LogCategory.IDENTITIES, error); + throw "failed to create crypto key"; } - for(let i = 0; i < 32; i++) - buffer[index++] = sign[32 + i]; + try { + this.public_key = await CryptoHelper.export_ecc_key(this._crypto_key, true); + this._unique_id = base64_encode_ab(await sha.sha1(this.public_key)); + } catch(error) { + log.error(LogCategory.IDENTITIES, error); + throw "failed to calculate unique id"; + } + + this._initialized = true; + //const public_key = await profiles.identities.CryptoHelper.export_ecc_key(key, true); } - buffer[1] = index - 2; - return base64_encode_ab(buffer.subarray(0, index)); - } + async export_ts(ini?: boolean) : Promise { + if(!this.private_key) + throw "Invalid private key"; - spawn_identity_handshake_handler(connection: AbstractServerConnection): HandshakeIdentityHandler { - return new TeaSpeakHandshakeHandler(connection, this); + const identity = this.hash_number + "V" + await CryptoHelper.encrypt_ts_identity(new Uint8Array(str2ab8(this.private_key))); + if(!ini) return identity; + + return "[Identity]\n" + + "id=TeaWeb-Exported\n" + + "identity=\"" + identity + "\"\n" + + "nickname=\"" + this.fallback_name() + "\"\n" + + "phonetic_nickname="; + } + + async sign_message(message: string, hash: string = "SHA-256") : Promise { + /* bring this to libtomcrypt format */ + const sign_buffer = await crypto.subtle.sign({ + name: "ECDSA", + hash: hash + }, this._crypto_key_sign, str2ab8(message)); + const sign = new Uint8Array(sign_buffer); + /* first 32 r bits | last 32 s bits */ + + const buffer = new Uint8Array(72); + let index = 0; + + { /* the initial sequence */ + buffer[index++] = 0x30; /* type */ + buffer[index++] = 0x00; /* we will set the sequence length later */ + } + { /* integer r */ + buffer[index++] = 0x02; /* type */ + buffer[index++] = 0x20; /* length */ + + if(sign[0] > 0x7F) { + buffer[index - 1] += 1; + buffer[index++] = 0; + } + + for(let i = 0; i < 32; i++) + buffer[index++] = sign[i]; + } + { /* integer s */ + buffer[index++] = 0x02; /* type */ + buffer[index++] = 0x20; /* length */ + + if(sign[32] > 0x7F) { + buffer[index - 1] += 1; + buffer[index++] = 0; + } + + for(let i = 0; i < 32; i++) + buffer[index++] = sign[32 + i]; + } + buffer[1] = index - 2; + + return base64_encode_ab(buffer.subarray(0, index)); + } + + spawn_identity_handshake_handler(connection: connection.AbstractServerConnection): connection.HandshakeIdentityHandler { + return new TeaSpeakHandshakeHandler(connection, this); + } } } \ No newline at end of file diff --git a/shared/js/profiles/identities/teaspeak-forum.ts b/shared/js/profiles/identities/teaspeak-forum.ts index 802cb87f..1ab6cd53 100644 --- a/shared/js/profiles/identities/teaspeak-forum.ts +++ b/shared/js/profiles/identities/teaspeak-forum.ts @@ -1,11 +1,8 @@ -import {Settings, settings} from "../../settings"; -import {update_forum} from "./TeaForumIdentity"; - -declare interface Window { +interface Window { grecaptcha: GReCaptcha; } -export interface GReCaptcha { +interface GReCaptcha { render(container: string | HTMLElement, parameters: { sitekey: string; theme?: "dark" | "light"; @@ -21,10 +18,10 @@ export interface GReCaptcha { reset(widget_id?: string); } -export namespace forum { +namespace forum { export namespace gcaptcha { export async function initialize() { - if(typeof((window as any).grecaptcha) === "undefined") { + if(typeof(window.grecaptcha) === "undefined") { let script = document.createElement("script"); script.async = true; @@ -53,7 +50,7 @@ export namespace forum { } } - if(typeof((window as any).grecaptcha) === "undefined") + if(typeof(window.grecaptcha) === "undefined") throw tr("failed to load recaptcha"); } @@ -65,9 +62,9 @@ export namespace forum { throw tr("initialisation failed"); } if(container.attr("captcha-uuid")) - (window as any).grecaptcha.reset(container.attr("captcha-uuid")); + window.grecaptcha.reset(container.attr("captcha-uuid")); else { - container.attr("captcha-uuid", (window as any).grecaptcha.render(container[0], { + container.attr("captcha-uuid", window.grecaptcha.render(container[0], { "sitekey": key, callback: callback_data })); @@ -209,7 +206,7 @@ export namespace forum { localStorage.setItem("teaspeak-forum-data", response["data"]); localStorage.setItem("teaspeak-forum-sign", response["sign"]); localStorage.setItem("teaspeak-forum-auth", response["auth-key"]); - update_forum(); + profiles.identities.update_forum(); } catch(error) { console.error(tr("Failed to parse forum given data: %o"), error); return { @@ -269,7 +266,7 @@ export namespace forum { _data = new Data(_data.auth_key, response["data"], response["sign"]); localStorage.setItem("teaspeak-forum-data", response["data"]); localStorage.setItem("teaspeak-forum-sign", response["sign"]); - update_forum(); + profiles.identities.update_forum(); } catch(error) { console.error(tr("Failed to parse forum given data: %o"), error); throw tr("failed to parse data"); @@ -323,7 +320,7 @@ export namespace forum { localStorage.removeItem("teaspeak-forum-data"); localStorage.removeItem("teaspeak-forum-sign"); localStorage.removeItem("teaspeak-forum-auth"); - update_forum(); + profiles.identities.update_forum(); } loader.register_task(loader.Stage.JAVASCRIPT_INITIALIZING, { diff --git a/shared/js/proto.ts b/shared/js/proto.ts index 37ee32e9..18a162a7 100644 --- a/shared/js/proto.ts +++ b/shared/js/proto.ts @@ -1,3 +1,5 @@ +//Used by CertAccept popup + interface Array { remove(elem?: T): boolean; last?(): T; diff --git a/shared/js/settings.ts b/shared/js/settings.ts index d90dc807..59a9c8eb 100644 --- a/shared/js/settings.ts +++ b/shared/js/settings.ts @@ -1,9 +1,6 @@ /// //Used by CertAccept popup -import {log, LogCategory} from "./log"; -import {createErrorModal} from "./ui/elements/modal"; - if(typeof(customElements) !== "undefined") { try { class X_Properties extends HTMLElement {} @@ -17,7 +14,7 @@ if(typeof(customElements) !== "undefined") { } /* T = value type */ -export interface SettingsKey { +interface SettingsKey { key: string; fallback_keys?: string | string[]; @@ -28,7 +25,7 @@ export interface SettingsKey { require_restart?: boolean; } -export class SettingsBase { +class SettingsBase { protected static readonly UPDATE_DIRECT: boolean = true; protected static transformStO?(input?: string, _default?: T, default_type?: string) : T { @@ -80,7 +77,7 @@ export class SettingsBase { } } -export class StaticSettings extends SettingsBase { +class StaticSettings extends SettingsBase { private static _instance: StaticSettings; static get instance() : StaticSettings { if(!this._instance) @@ -142,7 +139,7 @@ export class StaticSettings extends SettingsBase { } } -export class Settings extends StaticSettings { +class Settings extends StaticSettings { static readonly KEY_USER_IS_NEW: SettingsKey = { key: 'user_is_new_user', default_value: true @@ -436,7 +433,7 @@ export class Settings extends StaticSettings { } } -export class ServerSettings extends SettingsBase { +class ServerSettings extends SettingsBase { private cacheServer = {}; private _server_unique_id: string; private _server_save_worker: NodeJS.Timer; @@ -514,4 +511,4 @@ export class ServerSettings extends SettingsBase { } } -export let settings: Settings; \ No newline at end of file +let settings: Settings; \ No newline at end of file diff --git a/shared/js/sound/Sounds.ts b/shared/js/sound/Sounds.ts index f2992312..cbb75329 100644 --- a/shared/js/sound/Sounds.ts +++ b/shared/js/sound/Sounds.ts @@ -1,8 +1,4 @@ -import {settings} from "../settings"; -import {log, LogCategory} from "../log"; -import {ConnectionHandler} from "../ConnectionHandler"; - -export enum Sound { +enum Sound { SOUND_TEST = "sound.test", SOUND_EGG = "sound.egg", @@ -65,7 +61,7 @@ export enum Sound { GROUP_CHANNEL_CHANGED_SELF = "group.channel.changed.self" } -export namespace sound { +namespace sound { export interface SoundHandle { key: string; filename: string; diff --git a/shared/js/stats.ts b/shared/js/stats.ts index cefdfff4..ce9ca84b 100644 --- a/shared/js/stats.ts +++ b/shared/js/stats.ts @@ -1,6 +1,4 @@ -import {log, LogCategory} from "./log"; - -export namespace stats { +namespace stats { const LOG_PREFIX = "[Statistics] "; export enum CloseCodes { diff --git a/shared/js/ui/channel-tree/channel.css b/shared/js/ui/channel-tree/channel.css deleted file mode 100644 index 49a762ab..00000000 --- a/shared/js/ui/channel-tree/channel.css +++ /dev/null @@ -1,81 +0,0 @@ -.channel-container { - display: flex; - flex-direction: column; -} -.channel-container .container-channel { - position: relative; - display: flex; - flex-direction: row; - justify-content: stretch; - width: 100%; - min-height: 16px; - align-items: center; - cursor: pointer; -} -.channel-container .container-channel .channel-type { - flex-grow: 0; - flex-shrink: 0; - margin-right: 2px; -} -.channel-container .container-channel .container-channel-name { - display: flex; - flex-direction: row; - flex-grow: 1; - flex-shrink: 1; - justify-content: left; - max-width: 100%; - /* important for the repetitive channel name! */ - overflow-x: hidden; - height: 16px; -} -.channel-container .container-channel .container-channel-name.align-right { - justify-content: right; -} -.channel-container .container-channel .container-channel-name.align-center, .channel-container .container-channel .container-channel-name.align-repetitive { - justify-content: center; -} -.channel-container .container-channel .container-channel-name .channel-name { - align-self: center; - color: #828282; - min-width: 0; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; -} -.channel-container .container-channel .container-channel-name.align-repetitive .channel-name { - text-overflow: clip; -} -.channel-container .container-channel .icons { - display: flex; - flex-direction: row; - flex-grow: 0; - flex-shrink: 0; -} -.channel-container .container-channel.move-selected { - border-bottom: 1px solid black; -} -.channel-container .container-channel .show-channel-normal-only { - display: none; -} -.channel-container .container-channel .show-channel-normal-only.channel-normal { - display: block; -} -.channel-container .container-channel .icon_no_sound { - position: relative; - display: flex; -} -.channel-container .container-channel .icon_no_sound .background { - width: 10px; - height: 14px; - background: red; - position: absolute; - top: 1px; - left: 3px; - z-index: -1; -} -.channel-container .container-clients { - display: flex; - flex-direction: column; -} - -/*# sourceMappingURL=channel.css.map */ diff --git a/shared/js/ui/channel-tree/channel.css.map b/shared/js/ui/channel-tree/channel.css.map deleted file mode 100644 index 5c2199e9..00000000 --- a/shared/js/ui/channel-tree/channel.css.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"sourceRoot":"","sources":["channel.scss","colors.scss"],"names":[],"mappings":"AAEA;EACI;EACA;;AAEA;EACI;EAEA;EACA;EACA;EAEA;EACA;EAEA;EACA;;AAEA;EACI;EACA;EAEA;;AAGJ;EACI;EACA;EAEA;EACA;EAEA;EAEA;AAAiB;EACjB;EACA;;AAEA;EACI;;AAGJ;EACI;;AAGJ;EACI;EACA,OC/CW;EDiDX;EACA;EACA;EACA;;AAIA;EACI;;AAKZ;EACI;EACA;EAEA;EACA;;AAGJ;EACI;;AAGJ;EACI;;AAEA;EACI;;AAIR;EACI;EACA;;AAEA;EACI;EACA;EAEA;EACA;EACA;EACA;EACA;;AAKZ;EACI;EACA","file":"channel.css"} \ No newline at end of file diff --git a/shared/js/ui/channel-tree/channel.scss b/shared/js/ui/channel-tree/channel.scss deleted file mode 100644 index 6fa7ebbc..00000000 --- a/shared/js/ui/channel-tree/channel.scss +++ /dev/null @@ -1,106 +0,0 @@ -@import "colors"; - -.channel-container { - display: flex; - flex-direction: column; - - .container-channel { - position: relative; - - display: flex; - flex-direction: row; - justify-content: stretch; - - width: 100%; - min-height: 16px; - - align-items: center; - cursor: pointer; - - .channel-type { - flex-grow: 0; - flex-shrink: 0; - - margin-right: 2px; - } - - .container-channel-name { - display: flex; - flex-direction: row; - - flex-grow: 1; - flex-shrink: 1; - - justify-content: left; - - max-width: 100%; /* important for the repetitive channel name! */ - overflow-x: hidden; - height: 16px; - - &.align-right { - justify-content: right; - } - - &.align-center, &.align-repetitive { - justify-content: center; - } - - .channel-name { - align-self: center; - color: $channel-tree-entry-color; - - min-width: 0; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - } - - &.align-repetitive { - .channel-name { - text-overflow: clip; - } - } - } - - .icons { - display: flex; - flex-direction: row; - - flex-grow: 0; - flex-shrink: 0; - } - - &.move-selected { - border-bottom: 1px solid black; - } - - .show-channel-normal-only { - display: none; - - &.channel-normal { - display: block; - } - } - - .icon_no_sound { - position: relative; - display: flex; - - .background { - width: 10px; - height: 14px; - - background: red; - position: absolute; - top: 1px; - left: 3px; - z-index: -1; - } - } - } - - .container-clients { - display: flex; - flex-direction: column; - } -} \ No newline at end of file diff --git a/shared/js/ui/channel-tree/channel.tsx b/shared/js/ui/channel-tree/channel.tsx deleted file mode 100644 index 3e24be3b..00000000 --- a/shared/js/ui/channel-tree/channel.tsx +++ /dev/null @@ -1,120 +0,0 @@ -import * as React from "react"; -import {ChannelEntry} from "../../channel-tree/channel"; - -const cssChannel = require("./channel.scss"); -const cssTree = require("./tree.scss"); - -class ChannelIcon extends React.Component<{ channel: ChannelEntry }, {}> { - channel_icon_classes() { - const class_list = []; - const channel = this.props.channel; - - if(channel.formattedChannelName() !== channel.channelName()) - return "channel-spacer"; - - let icon_color; - if(channel.properties.channel_flag_password && !channel.cached_password()) - icon_color = "yellow"; - else if(channel.properties.channel_flag_maxclients_unlimited && channel.clients().length >= channel.properties.channel_maxclients) - icon_color = "red"; - else if(channel.properties.channel_flag_maxfamilyclients_unlimited && channel.clients(true).length >= channel.properties.channel_maxfamilyclients) - icon_color = "red"; - else - icon_color = "green"; - - - return "channel-normal client-channel_" + icon_color + (channel.flag_subscribed ? "_subscribed" : ""); - } - - render() { - return
; - } -} - -class ChannelIcons extends React.Component<{channel: ChannelEntry}, {}> { - render_icon(target: HTMLDivElement) { - const props = this.props.channel.properties; - if(!props.channel_icon_id) return; - - const tag = this.props.channel.channelTree.client.fileManager.icons.generateTag(props.channel_icon_id); - tag.appendTo($(target)); - } - - render() { - const icons = []; - - const props = this.props.channel.properties; - if(props.channel_flag_default) { - icons.push(
); - } - if(props.channel_flag_password) { - icons.push(
); - } - if(props.channel_codec_quality > 4) { - icons.push(
); - } - if(props.channel_needed_talk_power > 0) { - icons.push(
); - } - if(props.channel_icon_id != 0) { - icons.push(
this.render_icon(e) }/>) - } - if(!audio.codec.supported(props.channel_codec)) { - icons.push(
-
-
-
) - } - - return (
{icons}
); - } -} - -class ChannelLine extends React.Component<{ channel: ChannelEntry }, {}> { - - render() { - let depth = 1; - let parent = this.props.channel; - while((parent = parent.parent)) - depth++; - - //TODO: On handle spacer alignments in channel name! - return ( -
-
- - ); - } -} - -class ChannelClientsView extends React.Component<{}, {}> { -} - -class SubChannelView extends React.Component<{ channels: ChannelEntry[] }, {}> { - - render() { - return this.props.channels.map(e => ); - } -} - -export class Channel extends React.Component<{ channel: ChannelEntry }, {}> { - children: ChannelEntry[]; - - channel_entry() : ChannelEntry { return this.props.channel; } - - render() { - return ( -
- - - -
- ); - } -} \ No newline at end of file diff --git a/shared/js/ui/channel-tree/colors.css b/shared/js/ui/channel-tree/colors.css deleted file mode 100644 index f48e716e..00000000 --- a/shared/js/ui/channel-tree/colors.css +++ /dev/null @@ -1,3 +0,0 @@ - - -/*# sourceMappingURL=colors.css.map */ diff --git a/shared/js/ui/channel-tree/colors.css.map b/shared/js/ui/channel-tree/colors.css.map deleted file mode 100644 index 6ee84d1b..00000000 --- a/shared/js/ui/channel-tree/colors.css.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"sourceRoot":"","sources":[],"names":[],"mappings":"","file":"colors.css"} \ No newline at end of file diff --git a/shared/js/ui/channel-tree/colors.scss b/shared/js/ui/channel-tree/colors.scss deleted file mode 100644 index b12d3a2e..00000000 --- a/shared/js/ui/channel-tree/colors.scss +++ /dev/null @@ -1,3 +0,0 @@ - -$channel-tree-new-message-color: #a814147F; -$channel-tree-entry-color: #828282; \ No newline at end of file diff --git a/shared/js/ui/channel-tree/tree.css b/shared/js/ui/channel-tree/tree.css deleted file mode 100644 index 3f5f7faf..00000000 --- a/shared/js/ui/channel-tree/tree.css +++ /dev/null @@ -1,38 +0,0 @@ -/* Some general browser helpers */ -.tree-container .tree .entry .depth-filler { - font-size: 16px; - flex-shrink: 0; - flex-grow: 0; -} -.tree-container .marker-text-unread { - position: absolute; - left: 0; - top: 0; - bottom: 0; - width: 1px; - background-color: #a814147F; - opacity: 1; - -moz-transition: opacity 0.25s; - -o-transition: opacity 0.25s; - -webkit-transition: opacity 0.25s; - transition: opacity 0.25s; -} -.tree-container .marker-text-unread:before { - content: ""; - position: absolute; - left: 0; - top: 0; - bottom: 0; - width: 24px; - background: -moz-linear-gradient(left, rgba(168, 20, 20, 0.18) 0%, rgba(168, 20, 20, 0) 100%); - /* FF3.6-15 */ - background: -webkit-linear-gradient(left, rgba(168, 20, 20, 0.18) 0%, rgba(168, 20, 20, 0) 100%); - /* Chrome10-25,Safari5.1-6 */ - background: linear-gradient(to right, rgba(168, 20, 20, 0.18) 0%, rgba(168, 20, 20, 0) 100%); - /* W3C, IE10+, FF16+, Chrome26+, Opera12+, Safari7+ */ -} -.tree-container .marker-text-unread.hidden { - opacity: 0; -} - -/*# sourceMappingURL=tree.css.map */ diff --git a/shared/js/ui/channel-tree/tree.css.map b/shared/js/ui/channel-tree/tree.css.map deleted file mode 100644 index ab305210..00000000 --- a/shared/js/ui/channel-tree/tree.css.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"sourceRoot":"","sources":["../../../css/static/mixin.scss","tree.scss"],"names":[],"mappings":"AAAA;ACQY;EACI;EAEA;EACA;;AAKZ;EACI;EACA;EACA;EACA;EAEA;EACA;EAEA;EDvBP,iBC4CO;ED3CP,eC2CO;ED1CP,oBC0CO;EDzCP,YCyCO;;AAnBA;EACI;EACA;EAEA;EACA;EACA;EAEA;EAEA;AAAyF;EACzF;AAA2F;EAC3F;AAAuF;;AAG3F;EACI","file":"tree.css"} \ No newline at end of file diff --git a/shared/js/ui/channel-tree/tree.scss b/shared/js/ui/channel-tree/tree.scss deleted file mode 100644 index b54c059a..00000000 --- a/shared/js/ui/channel-tree/tree.scss +++ /dev/null @@ -1,50 +0,0 @@ -@import "../../../css/static/mixin"; -@import "../../../css/static/properties"; - -.tree-container { - .tree { - - .entry { - - .depth-filler { - font-size: 16px; - - flex-shrink: 0; - flex-grow: 0; - } - } - } - - .marker-text-unread { - position: absolute; - left: 0; - top: 0; - bottom: 0; - - width: 1px; - background-color: #a814147F; - - opacity: 1; - - &:before { - content: ''; - position: absolute; - - left: 0; - top: 0; - bottom: 0; - - width: 24px; - - background: -moz-linear-gradient(left, rgba(168,20,20,.18) 0%, rgba(168,20,20,0) 100%); /* FF3.6-15 */ - background: -webkit-linear-gradient(left, rgba(168,20,20,.18) 0%,rgba(168,20,20,0) 100%); /* Chrome10-25,Safari5.1-6 */ - background: linear-gradient(to right, rgba(168,20,20,.18) 0%,rgba(168,20,20,0) 100%); /* W3C, IE10+, FF16+, Chrome26+, Opera12+, Safari7+ */ - } - - &.hidden { - opacity: 0; - } - - @include transition(opacity $button_hover_animation_time); - } -} \ No newline at end of file diff --git a/shared/js/ui/channel-tree/tree.tsx b/shared/js/ui/channel-tree/tree.tsx deleted file mode 100644 index ea705ddf..00000000 --- a/shared/js/ui/channel-tree/tree.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import * as React from "react"; - -export class TreeView { - -} \ No newline at end of file diff --git a/shared/js/channel-tree/channel.ts b/shared/js/ui/channel.ts similarity index 99% rename from shared/js/channel-tree/channel.ts rename to shared/js/ui/channel.ts index 52da4cee..1a510f05 100644 --- a/shared/js/channel-tree/channel.ts +++ b/shared/js/ui/channel.ts @@ -1,15 +1,12 @@ /// /// -import {ChannelTree} from "./view"; -import {ClientEntry} from "./client"; - -export enum ChannelType { +enum ChannelType { PERMANENT, SEMI_PERMANENT, TEMPORARY } -export namespace ChannelType { +namespace ChannelType { export function normalize(mode: ChannelType) { let value: string = ChannelType[mode]; value = value.toLowerCase(); @@ -17,13 +14,13 @@ export namespace ChannelType { } } -export enum ChannelSubscribeMode { +enum ChannelSubscribeMode { SUBSCRIBED, UNSUBSCRIBED, INHERITED } -export class ChannelProperties { +class ChannelProperties { channel_order: number = 0; channel_name: string = ""; channel_name_phonetic: string = ""; @@ -58,7 +55,7 @@ export class ChannelProperties { channel_conversation_history_length: number = -1; } -export class ChannelEntry { +class ChannelEntry { channelTree: ChannelTree; channelId: number; parent?: ChannelEntry; diff --git a/shared/js/channel-tree/client.ts b/shared/js/ui/client.ts similarity index 99% rename from shared/js/channel-tree/client.ts rename to shared/js/ui/client.ts index 7010a7a5..d898f8f7 100644 --- a/shared/js/channel-tree/client.ts +++ b/shared/js/ui/client.ts @@ -1,11 +1,8 @@ /// -/// -/// +/// +/// -import {ChannelEntry} from "./channel"; -import {ChannelTree} from "./view"; - -export enum ClientType { +enum ClientType { CLIENT_VOICE, CLIENT_QUERY, CLIENT_INTERNAL, @@ -14,7 +11,7 @@ export enum ClientType { CLIENT_UNDEFINED } -export class ClientProperties { +class ClientProperties { client_type: ClientType = ClientType.CLIENT_VOICE; //TeamSpeaks type client_type_exact: ClientType = ClientType.CLIENT_VOICE; @@ -60,7 +57,7 @@ export class ClientProperties { client_is_priority_speaker: boolean = false; } -export class ClientConnectionInfo { +class ClientConnectionInfo { connection_bandwidth_received_last_minute_control: number = -1; connection_bandwidth_received_last_minute_keepalive: number = -1; connection_bandwidth_received_last_minute_speech: number = -1; @@ -112,7 +109,7 @@ export class ClientConnectionInfo { connection_client_port: number = -1; } -export class ClientEntry { +class ClientEntry { readonly events: events.Registry; protected _clientId: number; @@ -1126,7 +1123,7 @@ export class ClientEntry { } } -export class LocalClientEntry extends ClientEntry { +class LocalClientEntry extends ClientEntry { handle: ConnectionHandler; private renaming: boolean; @@ -1235,7 +1232,7 @@ export class LocalClientEntry extends ClientEntry { } } -export class MusicClientProperties extends ClientProperties { +class MusicClientProperties extends ClientProperties { player_state: number = 0; player_volume: number = 0; @@ -1267,7 +1264,7 @@ export class MusicClientProperties extends ClientProperties { } */ -export class SongInfo { +class SongInfo { song_id: number = 0; song_url: string = ""; song_invoker: number = 0; @@ -1280,7 +1277,7 @@ export class SongInfo { song_length: number = 0; } -export class MusicClientPlayerInfo extends SongInfo { +class MusicClientPlayerInfo extends SongInfo { bot_id: number = 0; player_state: number = 0; @@ -1293,7 +1290,7 @@ export class MusicClientPlayerInfo extends SongInfo { player_description: string = ""; } -export class MusicClientEntry extends ClientEntry { +class MusicClientEntry extends ClientEntry { private _info_promise: Promise; private _info_promise_age: number = 0; private _info_promise_resolve: any; diff --git a/shared/js/ui/client_move.ts b/shared/js/ui/client_move.ts index c99f7c61..5ccfa8a1 100644 --- a/shared/js/ui/client_move.ts +++ b/shared/js/ui/client_move.ts @@ -1,4 +1,6 @@ -export class ClientMover { +/// + +class ClientMover { static readonly listener_root = $(document); static readonly move_element = $("#mouse-move"); readonly channel_tree: ChannelTree; diff --git a/shared/js/ui/elements/context_divider.ts b/shared/js/ui/elements/context_divider.ts index c0426898..3ec65814 100644 --- a/shared/js/ui/elements/context_divider.ts +++ b/shared/js/ui/elements/context_divider.ts @@ -1,14 +1,10 @@ -import {settings} from "../../settings"; -import {log, LogCategory} from "../../log"; - -declare const $: any; interface JQuery { dividerfy() : this; } if(!$.fn.dividerfy) { $.fn.dividerfy = function(this: JQuery) : JQuery { - (this as any).find(".container-seperator").each(function (this: T) { + this.find(".container-seperator").each(function (this: T) { if(!this.previousElementSibling) return; if(!this.nextElementSibling) return; diff --git a/shared/js/ui/elements/context_menu.ts b/shared/js/ui/elements/context_menu.ts index c3543e2e..f56eb023 100644 --- a/shared/js/ui/elements/context_menu.ts +++ b/shared/js/ui/elements/context_menu.ts @@ -1,4 +1,4 @@ -export namespace contextmenu { +namespace contextmenu { export interface MenuEntry { callback?: () => void; type: MenuEntryType; diff --git a/shared/js/ui/elements/modal.ts b/shared/js/ui/elements/modal.ts index 68838f63..a0030309 100644 --- a/shared/js/ui/elements/modal.ts +++ b/shared/js/ui/elements/modal.ts @@ -1,13 +1,13 @@ -import {KeyCode} from "../../PPTListener"; +/// -export enum ElementType { +enum ElementType { HEADER, BODY, FOOTER } -export type BodyCreator = (() => JQuery | JQuery[] | string) | string | JQuery | JQuery[]; -export const ModalFunctions = { +type BodyCreator = (() => JQuery | JQuery[] | string) | string | JQuery | JQuery[]; +const ModalFunctions = { divify: function (val: JQuery) { if(val.length > 1) return $.spawn("div").append(val); @@ -54,7 +54,7 @@ export const ModalFunctions = { } }; -export class ModalProperties { +class ModalProperties { template?: string; header: BodyCreator = () => "HEADER"; body: BodyCreator = () => "BODY"; @@ -89,7 +89,7 @@ export class ModalProperties { full_size?: boolean = false; } -export namespace modal { +namespace modal { export function initialize_modals() { register_global_events(); } @@ -184,7 +184,7 @@ let _global_modal_count = 0; let _global_modal_last: HTMLElement; let _global_modal_last_time: number; -export class Modal { +class Modal { private _htmlTag: JQuery; properties: ModalProperties; shown: boolean; @@ -296,11 +296,11 @@ export class Modal { } } -export function createModal(data: ModalProperties | any) : Modal { +function createModal(data: ModalProperties | any) : Modal { return new Modal(ModalFunctions.warpProperties(data)); } -export class InputModalProperties extends ModalProperties { +class InputModalProperties extends ModalProperties { maxLength?: number; field_title?: string; @@ -310,7 +310,7 @@ export class InputModalProperties extends ModalProperties { error_message?: string; } -export function createInputModal(headMessage: BodyCreator, question: BodyCreator, validator: (input: string) => boolean, callback: (flag: boolean | string) => void, props: InputModalProperties | any = {}) : Modal { +function createInputModal(headMessage: BodyCreator, question: BodyCreator, validator: (input: string) => boolean, callback: (flag: boolean | string) => void, props: InputModalProperties | any = {}) : Modal { props = ModalFunctions.warpProperties(props); props.template_properties || (props.template_properties = {}); props.template_properties.field_title = props.field_title; @@ -370,7 +370,7 @@ export function createInputModal(headMessage: BodyCreator, question: BodyCreator return modal; } -export function createErrorModal(header: BodyCreator, message: BodyCreator, props: ModalProperties | any = { footer: undefined }) { +function createErrorModal(header: BodyCreator, message: BodyCreator, props: ModalProperties | any = { footer: undefined }) { props = ModalFunctions.warpProperties(props); (props.template_properties || (props.template_properties = {})).header_class = "modal-header-error"; @@ -382,7 +382,7 @@ export function createErrorModal(header: BodyCreator, message: BodyCreator, prop return modal; } -export function createInfoModal(header: BodyCreator, message: BodyCreator, props: ModalProperties | any = { footer: undefined }) { +function createInfoModal(header: BodyCreator, message: BodyCreator, props: ModalProperties | any = { footer: undefined }) { props = ModalFunctions.warpProperties(props); (props.template_properties || (props.template_properties = {})).header_class = "modal-header-info"; @@ -393,3 +393,73 @@ export function createInfoModal(header: BodyCreator, message: BodyCreator, props modal.htmlTag.find(".modal-body").addClass("modal-info"); return modal; } + +/* extend jquery */ + +interface ModalElements { + header?: BodyCreator; + body?: BodyCreator; + footer?: BodyCreator; +} + +interface JQuery { + modalize(entry_callback?: (header: JQuery, body: JQuery, footer: JQuery) => ModalElements | void, properties?: ModalProperties | any) : Modal; +} + +$.fn.modalize = function (this: JQuery, entry_callback?: (header: JQuery, body: JQuery, footer: JQuery) => ModalElements | void, properties?: ModalProperties | any) : Modal { + properties = properties || {} as ModalProperties; + entry_callback = entry_callback || ((a,b,c) => undefined); + + let tag_modal = this[0].tagName.toLowerCase() == "modal" ? this : undefined; /* TODO may throw exception? */ + + let tag_head = tag_modal ? tag_modal.find("modal-header") : ModalFunctions.jqueriefy(properties.header); + let tag_body = tag_modal ? tag_modal.find("modal-body") : this; + let tag_footer = tag_modal ? tag_modal.find("modal-footer") : ModalFunctions.jqueriefy(properties.footer); + + const result = entry_callback(tag_head as any, tag_body, tag_footer as any) || {}; + properties.header = result.header || tag_head; + properties.body = result.body || tag_body; + properties.footer = result.footer || tag_footer; + return createModal(properties); +}; + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/shared/js/ui/elements/net_graph.ts b/shared/js/ui/elements/net_graph.ts index a605d1cc..5e440872 100644 --- a/shared/js/ui/elements/net_graph.ts +++ b/shared/js/ui/elements/net_graph.ts @@ -1,4 +1,4 @@ -export namespace net.graph { +namespace net.graph { export type Entry = { timestamp: number; diff --git a/shared/js/ui/elements/slider.ts b/shared/js/ui/elements/slider.ts index 83c2f77c..0e956585 100644 --- a/shared/js/ui/elements/slider.ts +++ b/shared/js/ui/elements/slider.ts @@ -1,4 +1,4 @@ -export interface SliderOptions { +interface SliderOptions { min_value?: number; max_value?: number; initial_value?: number; @@ -8,11 +8,11 @@ export interface SliderOptions { value_field?: JQuery | JQuery[]; } -export interface Slider { +interface Slider { value(value?: number) : number; } -export function sliderfy(slider: JQuery, options?: SliderOptions) : Slider { +function sliderfy(slider: JQuery, options?: SliderOptions) : Slider { options = Object.assign( { initial_value: 0, min_value: 0, diff --git a/shared/js/ui/elements/tab.ts b/shared/js/ui/elements/tab.ts new file mode 100644 index 00000000..fb34577c --- /dev/null +++ b/shared/js/ui/elements/tab.ts @@ -0,0 +1,161 @@ +/// + +interface JQuery { + asTabWidget(copy?: boolean) : JQuery; + tabify(copy?: boolean) : this; + + changeElementType(type: string) : JQuery; +} + + +if(typeof (customElements) !== "undefined") { + try { + class X_Tab extends HTMLElement {} + class X_Entry extends HTMLElement {} + class X_Tag extends HTMLElement {} + class X_Content extends HTMLElement {} + + customElements.define('x-tab', X_Tab, { extends: 'div' }); + customElements.define('x-entry', X_Entry, { extends: 'div' }); + customElements.define('x-tag', X_Tag, { extends: 'div' }); + customElements.define('x-content', X_Content, { extends: 'div' }); + } catch(error) { + console.warn("failed to define costum elements"); + } +} else { + console.warn(tr("Could not defied tab customElements!")); +} + +var TabFunctions = { + tabify(template: JQuery, copy: boolean = true) : JQuery { + console.log("Tabify: copy=" + copy); + console.log(template); + + let tag = $.spawn("div"); + tag.addClass("tab"); + + let header = $.spawn("div"); + header.addClass("tab-header"); + + let content = $.spawn("div"); + content.addClass("tab-content"); + + content.append($.spawn("div").addClass("height-watcher")); + + let silentContent = $.spawn("div"); + silentContent.addClass("tab-content-invisible"); + + /* add some kind of min height */ + const update_height = () => { + const height_watcher = tag.find("> .tab-content .height-watcher"); + const entries: JQuery = tag.find("> .tab-content-invisible x-content, > .tab-content x-content"); + console.error(entries); + let max_height = 0; + + entries.each((_, _e) => { + const entry = $(_e); + const height = entry.visible_height(); + if(height > max_height) + max_height = height; + }); + + height_watcher.css('min-height', max_height + "px"); + tag.find(".window-resize-listener").trigger('resize'); + }; + + template.find("x-entry").each( (_, _entry) => { + const entry = $(_entry); + + const tag_header = $.spawn("div").addClass("entry"); + const tag_content = copy ? entry.find("x-content").clone(true, true) : entry.find("x-content"); + + { + const header_tag = entry.find("x-tag"); + const header_data = copy ? header_tag.contents().clone(true, true) : header_tag.contents(); + + if(header_tag.attr("x-entry-class")) + tag_header.addClass(header_tag.attr("x-entry-class")); + if(header_tag.attr("x-entry-id")) + tag_header.attr("x-id", header_tag.attr("x-entry-id")); + + tag_header.append(header_data); + + /* listener if the tab might got removed */ + tag_header.addClass("window-resize-listener"); + tag_header.on('resize', event => { + if(!tag_header.is(':visible') && tag_header.hasClass('selected')) { + let element = tag_header.next('.entry:visible'); + if(element.length == 0) + element = tag_header.prev('.entry:visible'); + if(element.length == 0) { + tag_header.removeClass("selected"); + tag_content.hide(); + } else { + element.first().trigger('click'); + } + console.log("Next: %o", tag_header.next('.entry:visible')); + console.log("prev: %o", tag_header.prev('.entry:visible')); + } + }); + } + + content.append(tag_content.hide()); + + tag_header.on("click", () => { + if(tag_header.hasClass("selected")) return; + + tag.find(".tab-header .selected").removeClass("selected"); + tag_header.addClass("selected"); + + content.find("> x-content").hide(); + /* don't show many nodes at once */ + let entries = tag_content.find(".tab-show-partitional"); + entries.hide(); + const show_next = index => { + console.log("Show " + index); + if(index >= entries.length) return; + entries.eq(index).show(); + + setTimeout(show_next.bind(undefined, index + 1), 0); + }; + show_next(0); + + tag_content.trigger('show'); + tag_content.show(); + }); + + console.log(this); + header.append(tag_header); + }); + + setTimeout(() => header.find(".entry").first().trigger("click"), 0); + + tag.append(header); + tag.append(content); + tag.append(silentContent); + + tag.on('tab.resize', update_height); + return tag; + } +}; + +if(!$.fn.asTabWidget) { + $.fn.asTabWidget = function (copy?: boolean) : JQuery { + if($(this).prop("tagName") == "X-TAB") + return TabFunctions.tabify($(this), typeof(copy) === "boolean" ? copy : true); + else { + throw "Invalid tag! " + $(this).prop("tagName"); + } + } +} + +if(!$.fn.tabify) { + $.fn.tabify = function (this: JQuery, copy?: boolean) { + const wrapped_tag = $.spawn("div").append(this); + wrapped_tag.find("x-tab").each((_, _element) => { + const element = $(_element); + element.replaceWith(element.asTabWidget(copy)); + }); + return wrapped_tag.children(); + } +} \ No newline at end of file diff --git a/shared/js/ui/elements/tooltip.ts b/shared/js/ui/elements/tooltip.ts index 89627bbe..b64f96b5 100644 --- a/shared/js/ui/elements/tooltip.ts +++ b/shared/js/ui/elements/tooltip.ts @@ -1,8 +1,8 @@ -export function tooltip(entry: JQuery) { +function tooltip(entry: JQuery) { return tooltip.initialize(entry); } -export namespace tooltip { +namespace tooltip { let _global_tooltip: JQuery; export type Handle = { show(); diff --git a/shared/js/ui/frames/ControlBar.ts b/shared/js/ui/frames/ControlBar.ts index ff205531..b2d6dad3 100644 --- a/shared/js/ui/frames/ControlBar.ts +++ b/shared/js/ui/frames/ControlBar.ts @@ -13,27 +13,12 @@ client_away_message Value: '' */ -import {ConnectionHandler, DisconnectReason} from "../../ConnectionHandler"; -import {top_menu} from "./MenuBar"; -import {Settings, settings} from "../../settings"; -import {createErrorModal, createInfoModal, createInputModal} from "../elements/modal"; -import {default_recorder} from "../../voice/RecorderProfile"; -import {sound, Sound} from "../../sound/Sounds"; -import {Modals} from "../modal/ModalConnect"; -import {Modal as ModalsS} from "../modal/ModalSettings"; -import {log} from "./server_log"; -import {MessageHelper} from "./chat"; -import {CommandResult} from "../../connection/ServerConnectionDeclaration"; -import {PermissionType} from "../../permission/PermissionManager"; -import {bookmarks} from "../../bookmarks"; -import {contextmenu} from "../elements/context_menu"; +let control_bar: ControlBar; /* global variable to access the control bar */ -export let control_bar: ControlBar; /* global variable to access the control bar */ - -export type MicrophoneState = "disabled" | "muted" | "enabled"; -export type HeadphoneState = "muted" | "enabled"; -export type AwayState = "away-global" | "away" | "online"; -export class ControlBar { +type MicrophoneState = "disabled" | "muted" | "enabled"; +type HeadphoneState = "muted" | "enabled"; +type AwayState = "away-global" | "away" | "online"; +class ControlBar { private _button_away_active: AwayState; private _button_microphone: MicrophoneState; private _button_speakers: HeadphoneState; @@ -436,7 +421,7 @@ export class ControlBar { } private on_open_settings() { - ModalsS.spawnSettingsModal(); + Modals.spawnSettingsModal(); } private on_open_connect() { diff --git a/shared/js/ui/frames/MenuBar.ts b/shared/js/ui/frames/MenuBar.ts index 10fb83cb..04885afa 100644 --- a/shared/js/ui/frames/MenuBar.ts +++ b/shared/js/ui/frames/MenuBar.ts @@ -1,12 +1,4 @@ -import {bookmarks} from "../../bookmarks"; -import {Modals} from "../modal/ModalBookmarks"; -import {DisconnectReason} from "../../ConnectionHandler"; -import {control_bar} from "./ControlBar"; -import {createErrorModal} from "../elements/modal"; -import {PermissionType} from "../../permission/PermissionManager"; -import {Sound} from "../../sound/Sounds"; - -export namespace top_menu { +namespace top_menu { export interface HRItem { } export interface MenuItem { diff --git a/shared/js/ui/frames/chat.ts b/shared/js/ui/frames/chat.ts index c0f38ee0..9de0d55b 100644 --- a/shared/js/ui/frames/chat.ts +++ b/shared/js/ui/frames/chat.ts @@ -1,6 +1,4 @@ -import {log, LogCategory} from "../../log"; - -export enum ChatType { +enum ChatType { GENERAL, SERVER, CHANNEL, @@ -8,7 +6,7 @@ export enum ChatType { } declare const xbbcode: any; -export namespace MessageHelper { +namespace MessageHelper { export function htmlEscape(message: string) : string[] { const div = document.createElement('div'); div.innerText = message; diff --git a/shared/js/ui/frames/chat_frame.ts b/shared/js/ui/frames/chat_frame.ts index 6921f9b4..b871ce95 100644 --- a/shared/js/ui/frames/chat_frame.ts +++ b/shared/js/ui/frames/chat_frame.ts @@ -1,11 +1,5 @@ /* the bar on the right with the chats (Channel & Client) */ -import {ChannelEntry} from "../../channel-tree/channel"; -import {Modals} from "../modal/ModalMusicManage"; -import {MessageHelper} from "./chat"; -import {ServerEntry} from "../../channel-tree/server"; -import {ConnectionHandler} from "../../ConnectionHandler"; - -export namespace chat { +namespace chat { declare function setInterval(handler: TimerHandler, timeout?: number, ...arguments: any[]): number; declare function setTimeout(handler: TimerHandler, timeout?: number, ...arguments: any[]): number; diff --git a/shared/js/ui/frames/connection_handlers.ts b/shared/js/ui/frames/connection_handlers.ts index be3347f2..4d9fb936 100644 --- a/shared/js/ui/frames/connection_handlers.ts +++ b/shared/js/ui/frames/connection_handlers.ts @@ -1,11 +1,7 @@ -import {ConnectionHandler, DisconnectReason} from "../../ConnectionHandler"; -import {Settings, settings} from "../../settings"; -import {control_bar} from "./ControlBar"; -import {top_menu} from "./MenuBar"; -export let server_connections: ServerConnectionManager; +let server_connections: ServerConnectionManager; -export class ServerConnectionManager { +class ServerConnectionManager { private connection_handlers: ConnectionHandler[] = []; private active_handler: ConnectionHandler | undefined; diff --git a/shared/js/ui/frames/hostbanner.ts b/shared/js/ui/frames/hostbanner.ts index 6dffddb5..2b0466d1 100644 --- a/shared/js/ui/frames/hostbanner.ts +++ b/shared/js/ui/frames/hostbanner.ts @@ -1,8 +1,4 @@ -import {ConnectionHandler} from "../../ConnectionHandler"; -import {Settings, settings} from "../../settings"; -import {log, LogCategory} from "../../log"; - -export class Hostbanner { +class Hostbanner { readonly html_tag: JQuery; readonly client: ConnectionHandler; diff --git a/shared/js/ui/frames/image_preview.ts b/shared/js/ui/frames/image_preview.ts index ee69ca3f..af46412f 100644 --- a/shared/js/ui/frames/image_preview.ts +++ b/shared/js/ui/frames/image_preview.ts @@ -1,4 +1,4 @@ -export namespace image_preview { +namespace image_preview { let preview_overlay: JQuery; let container_image: JQuery; let button_open_in_browser: JQuery; diff --git a/shared/js/ui/frames/server_log.ts b/shared/js/ui/frames/server_log.ts index 8107611a..d6e29f22 100644 --- a/shared/js/ui/frames/server_log.ts +++ b/shared/js/ui/frames/server_log.ts @@ -1,571 +1,566 @@ -import {ConnectionHandler, ViewReasonId} from "../../ConnectionHandler"; -import {PermissionInfo} from "../../permission/PermissionManager"; -import {htmltags} from "../htmltags"; -import {MessageHelper} from "./chat"; -import {i18n} from "../../i18n/localize"; +namespace log { + export namespace server { + export enum Type { + CONNECTION_BEGIN = "connection_begin", + CONNECTION_HOSTNAME_RESOLVE = "connection_hostname_resolve", + CONNECTION_HOSTNAME_RESOLVE_ERROR = "connection_hostname_resolve_error", + CONNECTION_HOSTNAME_RESOLVED = "connection_hostname_resolved", + CONNECTION_LOGIN = "connection_login", + CONNECTION_CONNECTED = "connection_connected", + CONNECTION_FAILED = "connection_failed", -export namespace server { - export enum Type { - CONNECTION_BEGIN = "connection_begin", - CONNECTION_HOSTNAME_RESOLVE = "connection_hostname_resolve", - CONNECTION_HOSTNAME_RESOLVE_ERROR = "connection_hostname_resolve_error", - CONNECTION_HOSTNAME_RESOLVED = "connection_hostname_resolved", - CONNECTION_LOGIN = "connection_login", - CONNECTION_CONNECTED = "connection_connected", - CONNECTION_FAILED = "connection_failed", + DISCONNECTED = "disconnected", - DISCONNECTED = "disconnected", + CONNECTION_VOICE_SETUP_FAILED = "connection_voice_setup_failed", + CONNECTION_COMMAND_ERROR = "connection_command_error", - CONNECTION_VOICE_SETUP_FAILED = "connection_voice_setup_failed", - CONNECTION_COMMAND_ERROR = "connection_command_error", + GLOBAL_MESSAGE = "global_message", - GLOBAL_MESSAGE = "global_message", + SERVER_WELCOME_MESSAGE = "server_welcome_message", + SERVER_HOST_MESSAGE = "server_host_message", + SERVER_HOST_MESSAGE_DISCONNECT = "server_host_message_disconnect", - SERVER_WELCOME_MESSAGE = "server_welcome_message", - SERVER_HOST_MESSAGE = "server_host_message", - SERVER_HOST_MESSAGE_DISCONNECT = "server_host_message_disconnect", + SERVER_CLOSED = "server_closed", + SERVER_BANNED = "server_banned", + SERVER_REQUIRES_PASSWORD = "server_requires_password", - SERVER_CLOSED = "server_closed", - SERVER_BANNED = "server_banned", - SERVER_REQUIRES_PASSWORD = "server_requires_password", + CLIENT_VIEW_ENTER = "client_view_enter", + CLIENT_VIEW_LEAVE = "client_view_leave", + CLIENT_VIEW_MOVE = "client_view_move", - CLIENT_VIEW_ENTER = "client_view_enter", - CLIENT_VIEW_LEAVE = "client_view_leave", - CLIENT_VIEW_MOVE = "client_view_move", + CLIENT_NICKNAME_CHANGED = "client_nickname_changed", + CLIENT_NICKNAME_CHANGE_FAILED = "client_nickname_change_failed", - CLIENT_NICKNAME_CHANGED = "client_nickname_changed", - CLIENT_NICKNAME_CHANGE_FAILED = "client_nickname_change_failed", + CLIENT_SERVER_GROUP_ADD = "client_server_group_add", + CLIENT_SERVER_GROUP_REMOVE = "client_server_group_remove", + CLIENT_CHANNEL_GROUP_CHANGE = "client_channel_group_change", - CLIENT_SERVER_GROUP_ADD = "client_server_group_add", - CLIENT_SERVER_GROUP_REMOVE = "client_server_group_remove", - CLIENT_CHANNEL_GROUP_CHANGE = "client_channel_group_change", + CHANNEL_CREATE = "channel_create", + CHANNEL_DELETE = "channel_delete", - CHANNEL_CREATE = "channel_create", - CHANNEL_DELETE = "channel_delete", + ERROR_CUSTOM = "error_custom", + ERROR_PERMISSION = "error_permission", - ERROR_CUSTOM = "error_custom", - ERROR_PERMISSION = "error_permission", - - RECONNECT_SCHEDULED = "reconnect_scheduled", - RECONNECT_EXECUTE = "reconnect_execute", - RECONNECT_CANCELED = "reconnect_canceled" - } - - export namespace base { - export type Client = { - client_unique_id: string; - client_name: string; - client_id: number; - } - export type Channel = { - channel_id: number; - channel_name: string; - } - export type Server = { - server_name: string; - server_unique_id: string; - } - export type ServerAddress = { - server_hostname: string; - server_port: number; - } - } - - export namespace event { - export type GlobalMessage = { - sender: base.Client; - message: string; - } - export type ConnectBegin = { - address: base.ServerAddress; - client_nickname: string; - } - export type ErrorCustom = { - message: string; + RECONNECT_SCHEDULED = "reconnect_scheduled", + RECONNECT_EXECUTE = "reconnect_execute", + RECONNECT_CANCELED = "reconnect_canceled" } - export type ReconnectScheduled = { - timeout: number; + export namespace base { + export type Client = { + client_unique_id: string; + client_name: string; + client_id: number; + } + export type Channel = { + channel_id: number; + channel_name: string; + } + export type Server = { + server_name: string; + server_unique_id: string; + } + export type ServerAddress = { + server_hostname: string; + server_port: number; + } } - export type ReconnectCanceled = { } - export type ReconnectExecute = { } - - export type ErrorPermission = { - permission: PermissionInfo; - } - - export type WelcomeMessage = { - message: string; - } - - export type HostMessageDisconnect = { - message: string; - } - - export type ClientMove = { - channel_from?: base.Channel; - channel_from_own: boolean; - - channel_to?: base.Channel; - channel_to_own: boolean; - - client: base.Client; - client_own: boolean; - - invoker?: base.Client; - - message?: string; - reason: ViewReasonId; - } - - export type ClientEnter = { - channel_from?: base.Channel; - channel_to?: base.Channel; - - client: base.Client; - invoker?: base.Client; - - message?: string; - own_channel: boolean; - - reason: ViewReasonId; - ban_time?: number; - } - - export type ClientLeave = { - channel_from?: base.Channel; - channel_to?: base.Channel; - - client: base.Client; - invoker?: base.Client; - - message?: string; - own_channel: boolean; - - reason: ViewReasonId; - ban_time?: number; - } - - export type ChannelCreate = { - creator: base.Client; - channel: base.Channel; - - own_action: boolean; - } - - export type ChannelDelete = { - deleter: base.Client; - channel: base.Channel; - - own_action: boolean; - } - - export type ConnectionConnected = { - own_client: base.Client; - } - export type ConnectionFailed = {}; - export type ConnectionLogin = {} - export type ConnectionHostnameResolve = {}; - export type ConnectionHostnameResolved = { - address: base.ServerAddress; - } - export type ConnectionHostnameResolveError = { - message: string; - } - - export type ConnectionVoiceSetupFailed = { - reason: string; - reconnect_delay: number; /* if less or equal to 0 reconnect is prohibited */ - } - - export type ConnectionCommandError = { - error: any; - } - - export type ClientNicknameChanged = { - own_client: boolean; - - client: base.Client; - - old_name: string; - new_name: string; - } - - export type ClientNicknameChangeFailed = { - reason: string; - } - - export type ServerClosed = { - message: string; - } - - export type ServerRequiresPassword = {} - - export type ServerBanned = { - message: string; - time: number; - - invoker: base.Client; - } - } - - export type LogMessage = { - type: Type; - timestamp: number; - data: any; - } - - export interface TypeInfo { - "connection_begin" : event.ConnectBegin; - "global_message": event.GlobalMessage; - - "error_custom": event.ErrorCustom; - "error_permission": event.ErrorPermission; - - "connection_hostname_resolved": event.ConnectionHostnameResolved; - "connection_hostname_resolve": event.ConnectionHostnameResolve; - "connection_hostname_resolve_error": event.ConnectionHostnameResolveError; - "connection_failed": event.ConnectionFailed; - "connection_login": event.ConnectionLogin; - "connection_connected": event.ConnectionConnected; - "connection_voice_setup_failed": event.ConnectionVoiceSetupFailed; - "connection_command_error": event.ConnectionCommandError; - - "reconnect_scheduled": event.ReconnectScheduled; - "reconnect_canceled": event.ReconnectCanceled; - "reconnect_execute": event.ReconnectExecute; - - "server_welcome_message": event.WelcomeMessage; - "server_host_message": event.WelcomeMessage; - "server_host_message_disconnect": event.HostMessageDisconnect; - - "server_closed": event.ServerClosed; - "server_requires_password": event.ServerRequiresPassword; - "server_banned": event.ServerBanned; - - "client_view_enter": event.ClientEnter; - "client_view_move": event.ClientMove; - "client_view_leave": event.ClientLeave; - - "client_nickname_change_failed": event.ClientNicknameChangeFailed, - "client_nickname_changed": event.ClientNicknameChanged, - - "channel_create": event.ChannelCreate; - "channel_delete": event.ChannelDelete; - - "disconnected": any; - } - - export type MessageBuilderOptions = {}; - export type MessageBuilder = (data: TypeInfo[T], options: MessageBuilderOptions) => JQuery[] | undefined; - - export const MessageBuilders: {[key: string]: MessageBuilder} = { - "error_custom": (data: event.ErrorCustom, options) => { - return [$.spawn("div").addClass("log-error").text(data.message)] - } - }; -} - -export class ServerLog { - private readonly handle: ConnectionHandler; - private history_length: number = 100; - - private _log: server.LogMessage[] = []; - private _html_tag: JQuery; - private _log_container: JQuery; - private auto_follow: boolean; /* automatic scroll to bottom */ - private _ignore_event: number; /* after auto scroll we've triggered the scroll event. We want to prevent this so we capture the next event */ - - constructor(handle: ConnectionHandler) { - this.handle = handle; - this.auto_follow = true; - - this._html_tag = $.spawn("div").addClass("container-log"); - this._log_container = $.spawn("div").addClass("container-messages"); - this._log_container.appendTo(this._html_tag); - - this._html_tag.on('scroll', event => { - if(Date.now() - this._ignore_event < 100) { - this._ignore_event = 0; - return; + export namespace event { + export type GlobalMessage = { + sender: base.Client; + message: string; + } + export type ConnectBegin = { + address: base.ServerAddress; + client_nickname: string; + } + export type ErrorCustom = { + message: string; } - this.auto_follow = (this._html_tag[0].scrollTop + this._html_tag[0].clientHeight + this._html_tag[0].clientHeight * .125) > this._html_tag[0].scrollHeight; - }); - } + export type ReconnectScheduled = { + timeout: number; + } - log(type: T, data: server.TypeInfo[T]) { - const event = { - data: data, - timestamp: Date.now(), - type: type as any + export type ReconnectCanceled = { } + export type ReconnectExecute = { } + + export type ErrorPermission = { + permission: PermissionInfo; + } + + export type WelcomeMessage = { + message: string; + } + + export type HostMessageDisconnect = { + message: string; + } + + export type ClientMove = { + channel_from?: base.Channel; + channel_from_own: boolean; + + channel_to?: base.Channel; + channel_to_own: boolean; + + client: base.Client; + client_own: boolean; + + invoker?: base.Client; + + message?: string; + reason: ViewReasonId; + } + + export type ClientEnter = { + channel_from?: base.Channel; + channel_to?: base.Channel; + + client: base.Client; + invoker?: base.Client; + + message?: string; + own_channel: boolean; + + reason: ViewReasonId; + ban_time?: number; + } + + export type ClientLeave = { + channel_from?: base.Channel; + channel_to?: base.Channel; + + client: base.Client; + invoker?: base.Client; + + message?: string; + own_channel: boolean; + + reason: ViewReasonId; + ban_time?: number; + } + + export type ChannelCreate = { + creator: base.Client; + channel: base.Channel; + + own_action: boolean; + } + + export type ChannelDelete = { + deleter: base.Client; + channel: base.Channel; + + own_action: boolean; + } + + export type ConnectionConnected = { + own_client: base.Client; + } + export type ConnectionFailed = {}; + export type ConnectionLogin = {} + export type ConnectionHostnameResolve = {}; + export type ConnectionHostnameResolved = { + address: base.ServerAddress; + } + export type ConnectionHostnameResolveError = { + message: string; + } + + export type ConnectionVoiceSetupFailed = { + reason: string; + reconnect_delay: number; /* if less or equal to 0 reconnect is prohibited */ + } + + export type ConnectionCommandError = { + error: any; + } + + export type ClientNicknameChanged = { + own_client: boolean; + + client: base.Client; + + old_name: string; + new_name: string; + } + + export type ClientNicknameChangeFailed = { + reason: string; + } + + export type ServerClosed = { + message: string; + } + + export type ServerRequiresPassword = {} + + export type ServerBanned = { + message: string; + time: number; + + invoker: base.Client; + } + } + + export type LogMessage = { + type: Type; + timestamp: number; + data: any; + } + + export interface TypeInfo { + "connection_begin" : event.ConnectBegin; + "global_message": event.GlobalMessage; + + "error_custom": event.ErrorCustom; + "error_permission": event.ErrorPermission; + + "connection_hostname_resolved": event.ConnectionHostnameResolved; + "connection_hostname_resolve": event.ConnectionHostnameResolve; + "connection_hostname_resolve_error": event.ConnectionHostnameResolveError; + "connection_failed": event.ConnectionFailed; + "connection_login": event.ConnectionLogin; + "connection_connected": event.ConnectionConnected; + "connection_voice_setup_failed": event.ConnectionVoiceSetupFailed; + "connection_command_error": event.ConnectionCommandError; + + "reconnect_scheduled": event.ReconnectScheduled; + "reconnect_canceled": event.ReconnectCanceled; + "reconnect_execute": event.ReconnectExecute; + + "server_welcome_message": event.WelcomeMessage; + "server_host_message": event.WelcomeMessage; + "server_host_message_disconnect": event.HostMessageDisconnect; + + "server_closed": event.ServerClosed; + "server_requires_password": event.ServerRequiresPassword; + "server_banned": event.ServerBanned; + + "client_view_enter": event.ClientEnter; + "client_view_move": event.ClientMove; + "client_view_leave": event.ClientLeave; + + "client_nickname_change_failed": event.ClientNicknameChangeFailed, + "client_nickname_changed": event.ClientNicknameChanged, + + "channel_create": event.ChannelCreate; + "channel_delete": event.ChannelDelete; + + "disconnected": any; + } + + export type MessageBuilderOptions = {}; + export type MessageBuilder = (data: TypeInfo[T], options: MessageBuilderOptions) => JQuery[] | undefined; + + export const MessageBuilders: {[key: string]: MessageBuilder} = { + "error_custom": (data: event.ErrorCustom, options) => { + return [$.spawn("div").addClass("log-error").text(data.message)] + } }; - - this._log.push(event); - while(this._log.length > this.history_length) - this._log.pop_front(); - - this.append_log(event); } - html_tag() : JQuery { - return this._html_tag; - } + export class ServerLog { + private readonly handle: ConnectionHandler; + private history_length: number = 100; - destroy() { - this._html_tag && this._html_tag.remove(); - this._html_tag = undefined; - this._log_container = undefined; + private _log: server.LogMessage[] = []; + private _html_tag: JQuery; + private _log_container: JQuery; + private auto_follow: boolean; /* automatic scroll to bottom */ + private _ignore_event: number; /* after auto scroll we've triggered the scroll event. We want to prevent this so we capture the next event */ - this._log = undefined; - } + constructor(handle: ConnectionHandler) { + this.handle = handle; + this.auto_follow = true; - private _scroll_task: number; + this._html_tag = $.spawn("div").addClass("container-log"); + this._log_container = $.spawn("div").addClass("container-messages"); + this._log_container.appendTo(this._html_tag); - private append_log(message: server.LogMessage) { - let container = $.spawn("div").addClass("log-message"); + this._html_tag.on('scroll', event => { + if(Date.now() - this._ignore_event < 100) { + this._ignore_event = 0; + return; + } - /* build timestamp */ - { - const num = number => ('00' + number).substr(-2); - const date = new Date(message.timestamp); - $.spawn("div") - .addClass("timestamp") - .text("<" + num(date.getHours()) + ":" + num(date.getMinutes()) + ":" + num(date.getSeconds()) + ">") - .appendTo(container); + this.auto_follow = (this._html_tag[0].scrollTop + this._html_tag[0].clientHeight + this._html_tag[0].clientHeight * .125) > this._html_tag[0].scrollHeight; + }); } - /* build message data */ - { - const builder = server.MessageBuilders[message.type]; - if(!builder) { - MessageHelper.formatMessage(tr("missing log message builder {0}!"), message.type).forEach(e => e.addClass("log-error").appendTo(container)); - } else { - const elements = builder(message.data, {}); - if(!elements || elements.length == 0) - return; /* discard message */ - container.append(...elements); + log(type: T, data: server.TypeInfo[T]) { + const event = { + data: data, + timestamp: Date.now(), + type: type as any + }; + + this._log.push(event); + while(this._log.length > this.history_length) + this._log.pop_front(); + + this.append_log(event); + } + + html_tag() : JQuery { + return this._html_tag; + } + + destroy() { + this._html_tag && this._html_tag.remove(); + this._html_tag = undefined; + this._log_container = undefined; + + this._log = undefined; + } + + private _scroll_task: number; + + private append_log(message: server.LogMessage) { + let container = $.spawn("div").addClass("log-message"); + + /* build timestamp */ + { + const num = number => ('00' + number).substr(-2); + const date = new Date(message.timestamp); + $.spawn("div") + .addClass("timestamp") + .text("<" + num(date.getHours()) + ":" + num(date.getMinutes()) + ":" + num(date.getSeconds()) + ">") + .appendTo(container); } - } - this._ignore_event = Date.now(); - this._log_container.append(container); - /* max history messages! */ - const messages = this._log_container.children(); - let index = 0; - while(messages.length - index > this.history_length) - index++; - const hide_elements = messages.filter(idx => idx < index); - hide_elements.hide(250, () => hide_elements.remove()); + /* build message data */ + { + const builder = server.MessageBuilders[message.type]; + if(!builder) { + MessageHelper.formatMessage(tr("missing log message builder {0}!"), message.type).forEach(e => e.addClass("log-error").appendTo(container)); + } else { + const elements = builder(message.data, {}); + if(!elements || elements.length == 0) + return; /* discard message */ + container.append(...elements); + } + } + this._ignore_event = Date.now(); + this._log_container.append(container); - if(this.auto_follow) { - clearTimeout(this._scroll_task); + /* max history messages! */ + const messages = this._log_container.children(); + let index = 0; + while(messages.length - index > this.history_length) + index++; + const hide_elements = messages.filter(idx => idx < index); + hide_elements.hide(250, () => hide_elements.remove()); - /* do not enforce a recalculate style here */ - this._scroll_task = setTimeout(() => { - this._html_tag.scrollTop(this._html_tag[0].scrollHeight); - this._scroll_task = 0; - }, 5) as any; + if(this.auto_follow) { + clearTimeout(this._scroll_task); + + /* do not enforce a recalculate style here */ + this._scroll_task = setTimeout(() => { + this._html_tag.scrollTop(this._html_tag[0].scrollHeight); + this._scroll_task = 0; + }, 5) as any; + } } } } /* impl of the parsers */ -export namespace server { - namespace impl { - import MessageBuilders = server.MessageBuilders; - import base = server.base; - import tra = i18n.tra; - const client_tag = (client: base.Client, braces?: boolean) => htmltags.generate_client_object({ - client_unique_id: client.client_unique_id, - client_id: client.client_id, - client_name: client.client_name, - add_braces: braces - }); - const channel_tag = (channel: base.Channel, braces?: boolean) => htmltags.generate_channel_object({ - channel_display_name: channel.channel_name, - channel_name: channel.channel_name, - channel_id: channel.channel_id, - add_braces: braces - }); +namespace log { + export namespace server { + namespace impl { + const client_tag = (client: base.Client, braces?: boolean) => htmltags.generate_client_object({ + client_unique_id: client.client_unique_id, + client_id: client.client_id, + client_name: client.client_name, + add_braces: braces + }); + const channel_tag = (channel: base.Channel, braces?: boolean) => htmltags.generate_channel_object({ + channel_display_name: channel.channel_name, + channel_name: channel.channel_name, + channel_id: channel.channel_id, + add_braces: braces + }); - MessageBuilders["connection_begin"] = (data: server.event.ConnectBegin, options) => { - return MessageHelper.formatMessage(tr("Connecting to {0}{1}"), data.address.server_hostname, data.address.server_port == 9987 ? "" : (":" + data.address.server_port)); - }; + MessageBuilders["connection_begin"] = (data: event.ConnectBegin, options) => { + return MessageHelper.formatMessage(tr("Connecting to {0}{1}"), data.address.server_hostname, data.address.server_port == 9987 ? "" : (":" + data.address.server_port)); + }; - MessageBuilders["connection_hostname_resolve"] = (data: event.ConnectionHostnameResolve, options) => MessageHelper.formatMessage(tr("Resolving hostname")); - MessageBuilders["connection_hostname_resolved"] = (data: event.ConnectionHostnameResolved, options) => MessageHelper.formatMessage(tr("Hostname resolved successfully to {0}:{1}"), data.address.server_hostname, data.address.server_port); - MessageBuilders["connection_hostname_resolve_error"] = (data: event.ConnectionHostnameResolveError, options) => MessageHelper.formatMessage(tr("Failed to resolve hostname. Connecting to given hostname. Error: {0}"), data.message); + MessageBuilders["connection_hostname_resolve"] = (data: event.ConnectionHostnameResolve, options) => MessageHelper.formatMessage(tr("Resolving hostname")); + MessageBuilders["connection_hostname_resolved"] = (data: event.ConnectionHostnameResolved, options) => MessageHelper.formatMessage(tr("Hostname resolved successfully to {0}:{1}"), data.address.server_hostname, data.address.server_port); + MessageBuilders["connection_hostname_resolve_error"] = (data: event.ConnectionHostnameResolveError, options) => MessageHelper.formatMessage(tr("Failed to resolve hostname. Connecting to given hostname. Error: {0}"), data.message); - MessageBuilders["connection_login"] = (data: event.ConnectionLogin, options) => MessageHelper.formatMessage(tr("Logging in...")); - MessageBuilders["connection_failed"] = (data: event.ConnectionFailed, options) => MessageHelper.formatMessage(tr("Connect failed.")); - MessageBuilders["connection_connected"] = (data: event.ConnectionConnected, options) => MessageHelper.formatMessage(tr("Connected as {0}"), client_tag(data.own_client, true)); + MessageBuilders["connection_login"] = (data: event.ConnectionLogin, options) => MessageHelper.formatMessage(tr("Logging in...")); + MessageBuilders["connection_failed"] = (data: event.ConnectionFailed, options) => MessageHelper.formatMessage(tr("Connect failed.")); + MessageBuilders["connection_connected"] = (data: event.ConnectionConnected, options) => MessageHelper.formatMessage(tr("Connected as {0}"), client_tag(data.own_client, true)); - MessageBuilders["connection_voice_setup_failed"] = (data: event.ConnectionVoiceSetupFailed, options) => { - return MessageHelper.formatMessage(tr("Failed to setup voice bridge: {0}. Allow reconnect: {1}"), data.reason, data.reconnect_delay > 0 ? tr("yes") : tr("no")); - }; + MessageBuilders["connection_voice_setup_failed"] = (data: event.ConnectionVoiceSetupFailed, options) => { + return MessageHelper.formatMessage(tr("Failed to setup voice bridge: {0}. Allow reconnect: {1}"), data.reason, data.reconnect_delay > 0 ? tr("yes") : tr("no")); + }; - MessageBuilders["error_permission"] = (data: event.ErrorPermission, options) => { - return MessageHelper.formatMessage(tr("Insufficient client permissions. Failed on permission {0}"), data.permission ? data.permission.name : "unknown").map(e => e.addClass("log-error")); - }; + MessageBuilders["error_permission"] = (data: event.ErrorPermission, options) => { + return MessageHelper.formatMessage(tr("Insufficient client permissions. Failed on permission {0}"), data.permission ? data.permission.name : "unknown").map(e => e.addClass("log-error")); + }; - MessageBuilders["client_view_enter"] = (data: event.ClientEnter, options) => { - if(data.reason == ViewReasonId.VREASON_SYSTEM) { - return undefined; - } if(data.reason == ViewReasonId.VREASON_USER_ACTION) { - /* client appeared */ - if(data.channel_from) { - return MessageHelper.formatMessage(data.own_channel ? tr("{0} appeared from {1} to your {2}") : tr("{0} appeared from {1} to {2}"), client_tag(data.client), channel_tag(data.channel_from), channel_tag(data.channel_to)); - } else { - return MessageHelper.formatMessage(data.own_channel ? tr("{0} connected to your channel {1}") : tr("{0} connected to channel {1}"), client_tag(data.client), channel_tag(data.channel_to)); + MessageBuilders["client_view_enter"] = (data: event.ClientEnter, options) => { + if(data.reason == ViewReasonId.VREASON_SYSTEM) { + return undefined; + } if(data.reason == ViewReasonId.VREASON_USER_ACTION) { + /* client appeared */ + if(data.channel_from) { + return MessageHelper.formatMessage(data.own_channel ? tr("{0} appeared from {1} to your {2}") : tr("{0} appeared from {1} to {2}"), client_tag(data.client), channel_tag(data.channel_from), channel_tag(data.channel_to)); + } else { + return MessageHelper.formatMessage(data.own_channel ? tr("{0} connected to your channel {1}") : tr("{0} connected to channel {1}"), client_tag(data.client), channel_tag(data.channel_to)); + } + } else if(data.reason == ViewReasonId.VREASON_MOVED) { + if(data.channel_from) { + return MessageHelper.formatMessage(data.own_channel ? tr("{0} appeared from {1} to your channel {2}, moved by {3}") : tr("{0} appeared from {1} to {2}, moved by {3}"), + client_tag(data.client), + channel_tag(data.channel_from), + channel_tag(data.channel_to), + client_tag(data.invoker) + ); + } else { + return MessageHelper.formatMessage(data.own_channel ? tr("{0} appeared to your channel {1}, moved by {2}") : tr("{0} appeared to {1}, moved by {2}"), + client_tag(data.client), + channel_tag(data.channel_to), + client_tag(data.invoker) + ); + } + } else if(data.reason == ViewReasonId.VREASON_CHANNEL_KICK) { + if(data.channel_from) { + return MessageHelper.formatMessage(data.own_channel ? tr("{0} appeared from {1} to your channel {2}, kicked by {3}{4}") : tr("{0} appeared from {1} to {2}, kicked by {3}{4}"), + client_tag(data.client), + channel_tag(data.channel_from), + channel_tag(data.channel_to), + client_tag(data.invoker), + data.message ? (" (" + data.message + ")") : "" + ); + } else { + return MessageHelper.formatMessage(data.own_channel ? tr("{0} appeared to your channel {1}, kicked by {2}{3}") : tr("{0} appeared to {1}, kicked by {2}{3}"), + client_tag(data.client), + channel_tag(data.channel_to), + client_tag(data.invoker), + data.message ? (" (" + data.message + ")") : "" + ); + } } - } else if(data.reason == ViewReasonId.VREASON_MOVED) { - if(data.channel_from) { - return MessageHelper.formatMessage(data.own_channel ? tr("{0} appeared from {1} to your channel {2}, moved by {3}") : tr("{0} appeared from {1} to {2}, moved by {3}"), + return [$.spawn("div").addClass("log-error").text("Invalid view enter reason id (" + data.message + ")")]; + }; + + MessageBuilders["client_view_move"] = (data: event.ClientMove, options) => { + if(data.reason == ViewReasonId.VREASON_MOVED) { + return MessageHelper.formatMessage(data.client_own ? tr("You was moved by {3} from channel {1} to {2}") : tr("{0} was moved from channel {1} to {2} by {3}"), client_tag(data.client), channel_tag(data.channel_from), channel_tag(data.channel_to), client_tag(data.invoker) ); - } else { - return MessageHelper.formatMessage(data.own_channel ? tr("{0} appeared to your channel {1}, moved by {2}") : tr("{0} appeared to {1}, moved by {2}"), + } else if(data.reason == ViewReasonId.VREASON_USER_ACTION) { + return MessageHelper.formatMessage(data.client_own ? tr("You switched from channel {1} to {2}") : tr("{0} switched from channel {1} to {2}"), client_tag(data.client), - channel_tag(data.channel_to), - client_tag(data.invoker) + channel_tag(data.channel_from), + channel_tag(data.channel_to) ); - } - } else if(data.reason == ViewReasonId.VREASON_CHANNEL_KICK) { - if(data.channel_from) { - return MessageHelper.formatMessage(data.own_channel ? tr("{0} appeared from {1} to your channel {2}, kicked by {3}{4}") : tr("{0} appeared from {1} to {2}, kicked by {3}{4}"), + } else if(data.reason == ViewReasonId.VREASON_CHANNEL_KICK) { + return MessageHelper.formatMessage(data.client_own ? tr("You got kicked out of the channel {1} to channel {2} by {3}{4}") : tr("{0} got kicked from channel {1} to {2} by {3}{4}"), client_tag(data.client), channel_tag(data.channel_from), channel_tag(data.channel_to), client_tag(data.invoker), data.message ? (" (" + data.message + ")") : "" ); - } else { - return MessageHelper.formatMessage(data.own_channel ? tr("{0} appeared to your channel {1}, kicked by {2}{3}") : tr("{0} appeared to {1}, kicked by {2}{3}"), + } + return [$.spawn("div").addClass("log-error").text("Invalid view move reason id (" + data.reason + ")")]; + }; + + MessageBuilders["client_view_leave"] = (data: event.ClientLeave, options) => { + if(data.reason == ViewReasonId.VREASON_USER_ACTION) { + return MessageHelper.formatMessage(data.own_channel ? tr("{0} disappeared from your channel {1} to {2}") : tr("{0} disappeared from {1} to {2}"), client_tag(data.client), channel_tag(data.channel_from), channel_tag(data.channel_to)); + } else if(data.reason == ViewReasonId.VREASON_SERVER_LEFT) { + return MessageHelper.formatMessage(tr("{0} left the server{1}"), client_tag(data.client), data.message ? (" (" + data.message + ")") : ""); + } else if(data.reason == ViewReasonId.VREASON_SERVER_KICK) { + return MessageHelper.formatMessage(tr("{0} was kicked from the server by {1}.{2}"), client_tag(data.client), client_tag(data.invoker), data.message ? (" (" + data.message + ")") : ""); + } else if(data.reason == ViewReasonId.VREASON_CHANNEL_KICK) { + return MessageHelper.formatMessage(data.own_channel ? tr("{0} was kicked from your channel by {2}.{3}") : tr("{0} was kicked from channel {1} by {2}.{3}"), client_tag(data.client), - channel_tag(data.channel_to), + channel_tag(data.channel_from), client_tag(data.invoker), data.message ? (" (" + data.message + ")") : "" ); + } else if(data.reason == ViewReasonId.VREASON_BAN) { + let duration = "permanently"; + if(data.ban_time) + duration = "for " + formatDate(data.ban_time); + return MessageHelper.formatMessage(tr("{0} was banned {1} by {2}.{3}"), + client_tag(data.client), + duration, + client_tag(data.invoker), + data.message ? (" (" + data.message + ")") : "" + ); + } else if(data.reason == ViewReasonId.VREASON_TIMEOUT) { + return MessageHelper.formatMessage(tr("{0} timed out{1}"), client_tag(data.client), data.message ? (" (" + data.message + ")") : ""); + } else if(data.reason == ViewReasonId.VREASON_MOVED) { + return MessageHelper.formatMessage(data.own_channel ? tr("{0} disappeared from your channel {1} to {2}, moved by {3}") : tr("{0} disappeared from {1} to {2}, moved by {3}"), client_tag(data.client), channel_tag(data.channel_from), channel_tag(data.channel_to), client_tag(data.invoker)); } - } - return [$.spawn("div").addClass("log-error").text("Invalid view enter reason id (" + data.message + ")")]; - }; - MessageBuilders["client_view_move"] = (data: event.ClientMove, options) => { - if(data.reason == ViewReasonId.VREASON_MOVED) { - return MessageHelper.formatMessage(data.client_own ? tr("You was moved by {3} from channel {1} to {2}") : tr("{0} was moved from channel {1} to {2} by {3}"), - client_tag(data.client), - channel_tag(data.channel_from), - channel_tag(data.channel_to), - client_tag(data.invoker) - ); - } else if(data.reason == ViewReasonId.VREASON_USER_ACTION) { - return MessageHelper.formatMessage(data.client_own ? tr("You switched from channel {1} to {2}") : tr("{0} switched from channel {1} to {2}"), - client_tag(data.client), - channel_tag(data.channel_from), - channel_tag(data.channel_to) - ); - } else if(data.reason == ViewReasonId.VREASON_CHANNEL_KICK) { - return MessageHelper.formatMessage(data.client_own ? tr("You got kicked out of the channel {1} to channel {2} by {3}{4}") : tr("{0} got kicked from channel {1} to {2} by {3}{4}"), - client_tag(data.client), - channel_tag(data.channel_from), - channel_tag(data.channel_to), - client_tag(data.invoker), - data.message ? (" (" + data.message + ")") : "" - ); - } - return [$.spawn("div").addClass("log-error").text("Invalid view move reason id (" + data.reason + ")")]; - }; + return [$.spawn("div").addClass("log-error").text("Invalid view leave reason id (" + data.reason + ")")]; + }; - MessageBuilders["client_view_leave"] = (data: event.ClientLeave, options) => { - if(data.reason == ViewReasonId.VREASON_USER_ACTION) { - return MessageHelper.formatMessage(data.own_channel ? tr("{0} disappeared from your channel {1} to {2}") : tr("{0} disappeared from {1} to {2}"), client_tag(data.client), channel_tag(data.channel_from), channel_tag(data.channel_to)); - } else if(data.reason == ViewReasonId.VREASON_SERVER_LEFT) { - return MessageHelper.formatMessage(tr("{0} left the server{1}"), client_tag(data.client), data.message ? (" (" + data.message + ")") : ""); - } else if(data.reason == ViewReasonId.VREASON_SERVER_KICK) { - return MessageHelper.formatMessage(tr("{0} was kicked from the server by {1}.{2}"), client_tag(data.client), client_tag(data.invoker), data.message ? (" (" + data.message + ")") : ""); - } else if(data.reason == ViewReasonId.VREASON_CHANNEL_KICK) { - return MessageHelper.formatMessage(data.own_channel ? tr("{0} was kicked from your channel by {2}.{3}") : tr("{0} was kicked from channel {1} by {2}.{3}"), - client_tag(data.client), - channel_tag(data.channel_from), - client_tag(data.invoker), - data.message ? (" (" + data.message + ")") : "" - ); - } else if(data.reason == ViewReasonId.VREASON_BAN) { - let duration = "permanently"; - if(data.ban_time) - duration = "for " + formatDate(data.ban_time); - return MessageHelper.formatMessage(tr("{0} was banned {1} by {2}.{3}"), - client_tag(data.client), - duration, - client_tag(data.invoker), - data.message ? (" (" + data.message + ")") : "" - ); - } else if(data.reason == ViewReasonId.VREASON_TIMEOUT) { - return MessageHelper.formatMessage(tr("{0} timed out{1}"), client_tag(data.client), data.message ? (" (" + data.message + ")") : ""); - } else if(data.reason == ViewReasonId.VREASON_MOVED) { - return MessageHelper.formatMessage(data.own_channel ? tr("{0} disappeared from your channel {1} to {2}, moved by {3}") : tr("{0} disappeared from {1} to {2}, moved by {3}"), client_tag(data.client), channel_tag(data.channel_from), channel_tag(data.channel_to), client_tag(data.invoker)); - } + MessageBuilders["server_welcome_message"] = (data: event.WelcomeMessage, options) => { + return MessageHelper.bbcode_chat("[color=green]" + data.message + "[/color]"); + }; - return [$.spawn("div").addClass("log-error").text("Invalid view leave reason id (" + data.reason + ")")]; - }; + MessageBuilders["server_host_message"] = (data: event.WelcomeMessage, options) => { + return MessageHelper.bbcode_chat("[color=green]" + data.message + "[/color]"); + }; - MessageBuilders["server_welcome_message"] = (data: event.WelcomeMessage, options) => { - return MessageHelper.bbcode_chat("[color=green]" + data.message + "[/color]"); - }; + MessageBuilders["client_nickname_changed"] = (data: event.ClientNicknameChanged, options) => { + if(data.own_client) { + return MessageHelper.formatMessage(tr("Nickname successfully changed.")); + } else { + return MessageHelper.formatMessage(tr("{0} changed his nickname from \"{1}\" to \"{2}\""), client_tag(data.client), data.old_name, data.new_name); + } + }; - MessageBuilders["server_host_message"] = (data: event.WelcomeMessage, options) => { - return MessageHelper.bbcode_chat("[color=green]" + data.message + "[/color]"); - }; + MessageBuilders["global_message"] = (data: event.GlobalMessage, options) => { + return []; /* we do not show global messages within log */ + }; - MessageBuilders["client_nickname_changed"] = (data: event.ClientNicknameChanged, options) => { - if(data.own_client) { - return MessageHelper.formatMessage(tr("Nickname successfully changed.")); - } else { - return MessageHelper.formatMessage(tr("{0} changed his nickname from \"{1}\" to \"{2}\""), client_tag(data.client), data.old_name, data.new_name); - } - }; + MessageBuilders["disconnected"] = () => MessageHelper.formatMessage(tr("Disconnected from server")); - MessageBuilders["global_message"] = (data: event.GlobalMessage, options) => { - return []; /* we do not show global messages within log */ - }; + MessageBuilders["reconnect_scheduled"] = (data: event.ReconnectScheduled, options) => { + return tra("Reconnecting in {0}.", MessageHelper.format_time(data.timeout, tr("now"))) + }; - MessageBuilders["disconnected"] = () => MessageHelper.formatMessage(tr("Disconnected from server")); + MessageBuilders["reconnect_canceled"] = (data: event.ReconnectCanceled, options) => { + return tra("Canceled reconnect.") + }; - MessageBuilders["reconnect_scheduled"] = (data: event.ReconnectScheduled, options) => { - return tra("Reconnecting in {0}.", MessageHelper.format_time(data.timeout, tr("now"))) - }; + MessageBuilders["reconnect_execute"] = (data: event.ReconnectExecute, options) => { + return tra("Reconnecting...") + }; - MessageBuilders["reconnect_canceled"] = (data: event.ReconnectCanceled, options) => { - return tra("Canceled reconnect.") - }; + MessageBuilders["server_banned"] = (data: event.ServerBanned, options) => { + let result: JQuery[]; - MessageBuilders["reconnect_execute"] = (data: event.ReconnectExecute, options) => { - return tra("Reconnecting...") - }; + const time = data.time == 0 ? tr("ever") : MessageHelper.format_time(data.time * 1000, tr("one second")); + if(data.invoker.client_id > 0) { + if(data.message) + result = tra("You've been banned from the server by {0} for {1}. Reason: {2}", client_tag(data.invoker), time, data.message); + else + result = tra("You've been banned from the server by {0} for {1}.", client_tag(data.invoker), time); + } else { + if(data.message) + result = tra("You've been banned from the server for {0}. Reason: {1}", time, data.message); + else + result = tra("You've been banned from the server for {0}.", time); + } - MessageBuilders["server_banned"] = (data: event.ServerBanned, options) => { - let result: JQuery[]; - - const time = data.time == 0 ? tr("ever") : MessageHelper.format_time(data.time * 1000, tr("one second")); - if(data.invoker.client_id > 0) { - if(data.message) - result = tra("You've been banned from the server by {0} for {1}. Reason: {2}", client_tag(data.invoker), time, data.message); - else - result = tra("You've been banned from the server by {0} for {1}.", client_tag(data.invoker), time); - } else { - if(data.message) - result = tra("You've been banned from the server for {0}. Reason: {1}", time, data.message); - else - result = tra("You've been banned from the server for {0}.", time); - } - - return result.map(e => e.addClass("log-error")); - }; + return result.map(e => e.addClass("log-error")); + }; + } } } \ No newline at end of file diff --git a/shared/js/ui/frames/side/chat_box.ts b/shared/js/ui/frames/side/chat_box.ts index 12474fcf..863f9335 100644 --- a/shared/js/ui/frames/side/chat_box.ts +++ b/shared/js/ui/frames/side/chat_box.ts @@ -1,4 +1,4 @@ -export namespace chat { +namespace chat { declare function setInterval(handler: TimerHandler, timeout?: number, ...arguments: any[]): number; declare function setTimeout(handler: TimerHandler, timeout?: number, ...arguments: any[]): number; diff --git a/shared/js/ui/frames/side/chat_helper.ts b/shared/js/ui/frames/side/chat_helper.ts index 79e3bf55..c3e7396f 100644 --- a/shared/js/ui/frames/side/chat_helper.ts +++ b/shared/js/ui/frames/side/chat_helper.ts @@ -1,4 +1,4 @@ -export namespace chat { +namespace chat { export namespace helpers { //https://regex101.com/r/YQbfcX/2 //static readonly URL_REGEX = /^(?([a-zA-Z0-9-]+\.)+[a-zA-Z0-9-]{2,63})(?:\/(?(?:[^\s?]+)?)(?:\?(?\S+))?)?$/gm; diff --git a/shared/js/ui/frames/side/client_info.ts b/shared/js/ui/frames/side/client_info.ts index cacbed19..9847b42b 100644 --- a/shared/js/ui/frames/side/client_info.ts +++ b/shared/js/ui/frames/side/client_info.ts @@ -1,11 +1,4 @@ -import {ClientEntry, LocalClientEntry} from "../../../channel-tree/client"; -import {Modals} from "../../modal/ModalClientInfo"; -import {htmltags} from "../../htmltags"; -import {image_preview} from "../image_preview"; -import {i18n} from "../../../i18n/country"; -import {GroupManager} from "../../../permission/GroupManager"; - -export namespace chat { +namespace chat { declare function setInterval(handler: TimerHandler, timeout?: number, ...arguments: any[]): number; declare function setTimeout(handler: TimerHandler, timeout?: number, ...arguments: any[]): number; diff --git a/shared/js/ui/frames/side/conversations.ts b/shared/js/ui/frames/side/conversations.ts index 5351a673..3c45863a 100644 --- a/shared/js/ui/frames/side/conversations.ts +++ b/shared/js/ui/frames/side/conversations.ts @@ -1,10 +1,4 @@ -import {htmltags} from "../../htmltags"; -import {MessageHelper} from "../chat"; -import {CommandResult, ErrorID} from "../../../connection/ServerConnectionDeclaration"; -import {log, LogCategory} from "../../../log"; -import {createErrorModal} from "../../elements/modal"; - -export namespace chat { +namespace chat { declare function setInterval(handler: TimerHandler, timeout?: number, ...arguments: any[]): number; declare function setTimeout(handler: TimerHandler, timeout?: number, ...arguments: any[]): number; diff --git a/shared/js/ui/frames/side/music_info.ts b/shared/js/ui/frames/side/music_info.ts index 3b79e309..41cbebbc 100644 --- a/shared/js/ui/frames/side/music_info.ts +++ b/shared/js/ui/frames/side/music_info.ts @@ -1,7 +1,6 @@ -import {MusicClientEntry} from "../../../channel-tree/client"; -import {image_preview} from "../image_preview"; +namespace chat { + import PlayerState = connection.voice.PlayerState; -export namespace chat { declare function setInterval(handler: TimerHandler, timeout?: number, ...arguments: any[]): number; declare function setTimeout(handler: TimerHandler, timeout?: number, ...arguments: any[]): number; diff --git a/shared/js/ui/frames/side/private_conversations.ts b/shared/js/ui/frames/side/private_conversations.ts index 08edd3ba..f85ffe7c 100644 --- a/shared/js/ui/frames/side/private_conversations.ts +++ b/shared/js/ui/frames/side/private_conversations.ts @@ -1,9 +1,5 @@ /* the bar on the right with the chats (Channel & Client) */ -import {log, LogCategory} from "../../../log"; -import {htmltags} from "../../htmltags"; -import {MessageHelper} from "../chat"; - -export namespace chat { +namespace chat { declare function setInterval(handler: TimerHandler, timeout?: number, ...arguments: any[]): number; declare function setTimeout(handler: TimerHandler, timeout?: number, ...arguments: any[]): number; diff --git a/shared/js/ui/htmltags.ts b/shared/js/ui/htmltags.ts index 76c98159..b208043f 100644 --- a/shared/js/ui/htmltags.ts +++ b/shared/js/ui/htmltags.ts @@ -1,4 +1,4 @@ -export namespace htmltags { +namespace htmltags { let mouse_coordinates: {x: number, y: number} = {x: 0, y: 0}; function initialize() { diff --git a/shared/js/ui/modal/ModalAbout.ts b/shared/js/ui/modal/ModalAbout.ts index 518e5a3a..39207f1c 100644 --- a/shared/js/ui/modal/ModalAbout.ts +++ b/shared/js/ui/modal/ModalAbout.ts @@ -1,4 +1,8 @@ -export namespace Modals { +/// +/// +/// + +namespace Modals { function format_date(date: number) { const d = new Date(date); diff --git a/shared/js/ui/modal/ModalAvatar.ts b/shared/js/ui/modal/ModalAvatar.ts index e877f45f..c5fcac24 100644 --- a/shared/js/ui/modal/ModalAvatar.ts +++ b/shared/js/ui/modal/ModalAvatar.ts @@ -1,4 +1,8 @@ -export namespace Modals { +/// +/// +/// + +namespace Modals { //TODO: Test if we could render this image and not only the browser by knowing the type. export function spawnAvatarUpload(callback_data: (data: ArrayBuffer | undefined | null) => any) { const modal = createModal({ diff --git a/shared/js/ui/modal/ModalAvatarList.ts b/shared/js/ui/modal/ModalAvatarList.ts index beca0f7e..021430f2 100644 --- a/shared/js/ui/modal/ModalAvatarList.ts +++ b/shared/js/ui/modal/ModalAvatarList.ts @@ -1,4 +1,8 @@ -export namespace Modals { +/// +/// +/// + +namespace Modals { const avatar_to_uid = (id: string) => { const buffer = new Uint8Array(id.length / 2); for(let index = 0; index < id.length; index += 2) { diff --git a/shared/js/ui/modal/ModalBanClient.ts b/shared/js/ui/modal/ModalBanClient.ts index 24a47d6b..bb7b0627 100644 --- a/shared/js/ui/modal/ModalBanClient.ts +++ b/shared/js/ui/modal/ModalBanClient.ts @@ -1,3 +1,8 @@ +/// +/// +/// + +namespace Modals { export type BanEntry = { name?: string; unique_id: string; diff --git a/shared/js/ui/modal/ModalBanList.ts b/shared/js/ui/modal/ModalBanList.ts index dc6344de..e5460964 100644 --- a/shared/js/ui/modal/ModalBanList.ts +++ b/shared/js/ui/modal/ModalBanList.ts @@ -1,4 +1,9 @@ -export namespace Modals { +/// +/// +/// +/// + +namespace Modals { export function openBanList(client: ConnectionHandler) { let modal: Modal; diff --git a/shared/js/ui/modal/ModalBookmarks.ts b/shared/js/ui/modal/ModalBookmarks.ts index 875a34f2..950b3658 100644 --- a/shared/js/ui/modal/ModalBookmarks.ts +++ b/shared/js/ui/modal/ModalBookmarks.ts @@ -1,4 +1,8 @@ -export namespace Modals { +/// +/// +/// + +namespace Modals { export function spawnBookmarkModal() { let modal: Modal; modal = createModal({ diff --git a/shared/js/ui/modal/ModalChangeLatency.ts b/shared/js/ui/modal/ModalChangeLatency.ts index 7690dcd3..3b8dca95 100644 --- a/shared/js/ui/modal/ModalChangeLatency.ts +++ b/shared/js/ui/modal/ModalChangeLatency.ts @@ -1,4 +1,8 @@ -export namespace Modals { +/// +/// +/// + +namespace Modals { let modal: Modal; export function spawnChangeLatency(client: ClientEntry, current: connection.voice.LatencySettings, reset: () => connection.voice.LatencySettings, apply: (settings: connection.voice.LatencySettings) => any, callback_flush?: () => any) { if(modal) modal.close(); diff --git a/shared/js/ui/modal/ModalChangeVolume.ts b/shared/js/ui/modal/ModalChangeVolume.ts index 691f61ab..b9b0778a 100644 --- a/shared/js/ui/modal/ModalChangeVolume.ts +++ b/shared/js/ui/modal/ModalChangeVolume.ts @@ -1,4 +1,8 @@ -export namespace Modals { +/// +/// +/// + +namespace Modals { //TODO: Use the max limit! let modal: Modal; diff --git a/shared/js/ui/modal/ModalChannelInfo.ts b/shared/js/ui/modal/ModalChannelInfo.ts index ace6027b..db91989f 100644 --- a/shared/js/ui/modal/ModalChannelInfo.ts +++ b/shared/js/ui/modal/ModalChannelInfo.ts @@ -1,4 +1,4 @@ -export namespace Modals { +namespace Modals { export function openChannelInfo(channel: ChannelEntry) { let modal: Modal; diff --git a/shared/js/ui/modal/ModalClientInfo.ts b/shared/js/ui/modal/ModalClientInfo.ts index 67953f79..482a42d5 100644 --- a/shared/js/ui/modal/ModalClientInfo.ts +++ b/shared/js/ui/modal/ModalClientInfo.ts @@ -1,4 +1,4 @@ -export namespace Modals { +namespace Modals { type InfoUpdateCallback = (info: ClientConnectionInfo) => any; export function openClientInfo(client: ClientEntry) { let modal: Modal; diff --git a/shared/js/ui/modal/ModalConnect.ts b/shared/js/ui/modal/ModalConnect.ts index 9c9be5a4..cefdf403 100644 --- a/shared/js/ui/modal/ModalConnect.ts +++ b/shared/js/ui/modal/ModalConnect.ts @@ -1,5 +1,7 @@ +/// + //FIXME: Move this shit out of this file! -export namespace connection_log { +namespace connection_log { //TODO: Save password data export type ConnectionData = { name: string; @@ -89,7 +91,7 @@ export namespace connection_log { }); } -export namespace Modals { +namespace Modals { export function spawnConnectModal(options: { default_connect_new_tab?: boolean /* default false */ }, defaultHost: { url: string, enforce: boolean} = { url: "ts.TeaSpeak.de", enforce: false}, connect_profile?: { profile: profiles.ConnectionProfile, enforce: boolean}) { diff --git a/shared/js/ui/modal/ModalCreateChannel.ts b/shared/js/ui/modal/ModalCreateChannel.ts index 5a5ba4d6..5f7be3bc 100644 --- a/shared/js/ui/modal/ModalCreateChannel.ts +++ b/shared/js/ui/modal/ModalCreateChannel.ts @@ -1,4 +1,6 @@ -export namespace Modals { +/// + +namespace Modals { export function createChannelModal(connection: ConnectionHandler, channel: ChannelEntry | undefined, parent: ChannelEntry | undefined, permissions: PermissionManager, callback: (properties?: ChannelProperties, permissions?: PermissionValue[]) => any) { let properties: ChannelProperties = { } as ChannelProperties; //The changes properties const modal = createModal({ diff --git a/shared/js/ui/modal/ModalGroupAssignment.ts b/shared/js/ui/modal/ModalGroupAssignment.ts index 9d2e05d1..52deb24d 100644 --- a/shared/js/ui/modal/ModalGroupAssignment.ts +++ b/shared/js/ui/modal/ModalGroupAssignment.ts @@ -1,4 +1,4 @@ -export namespace Modals { +namespace Modals { let current_modal: Modal; export function createServerGroupAssignmentModal(client: ClientEntry, callback: (groups: number[], flag: boolean) => Promise) { if(current_modal) diff --git a/shared/js/ui/modal/ModalIconSelect.ts b/shared/js/ui/modal/ModalIconSelect.ts index 65f915d8..42911e61 100644 --- a/shared/js/ui/modal/ModalIconSelect.ts +++ b/shared/js/ui/modal/ModalIconSelect.ts @@ -1,4 +1,8 @@ -export namespace Modals { +/// +/// +/// + +namespace Modals { export function spawnIconSelect(client: ConnectionHandler, callback_icon?: (id: number) => any, selected_icon?: number) { selected_icon = selected_icon || 0; let allow_manage = client.permissions.neededPermission(PermissionType.B_ICON_MANAGE).granted(1); diff --git a/shared/js/ui/modal/ModalIdentity.ts b/shared/js/ui/modal/ModalIdentity.ts index 12aec88b..1c5f9296 100644 --- a/shared/js/ui/modal/ModalIdentity.ts +++ b/shared/js/ui/modal/ModalIdentity.ts @@ -1,4 +1,4 @@ -export namespace Modals { +namespace Modals { export function spawnTeamSpeakIdentityImprove(identity: profiles.identities.TeaSpeakIdentity, name: string): Modal { let modal: Modal; let elapsed_timer: NodeJS.Timer; diff --git a/shared/js/ui/modal/ModalInvite.ts b/shared/js/ui/modal/ModalInvite.ts index 77865c63..45a7acaa 100644 --- a/shared/js/ui/modal/ModalInvite.ts +++ b/shared/js/ui/modal/ModalInvite.ts @@ -1,4 +1,8 @@ -export namespace Modals { +/// +/// +/// + +namespace Modals { type URLGeneratorSettings = { flag_direct: boolean, flag_resolved: boolean diff --git a/shared/js/ui/modal/ModalKeySelect.ts b/shared/js/ui/modal/ModalKeySelect.ts index 194f925b..8ad60b2d 100644 --- a/shared/js/ui/modal/ModalKeySelect.ts +++ b/shared/js/ui/modal/ModalKeySelect.ts @@ -1,4 +1,4 @@ -export namespace Modals { +namespace Modals { export function spawnKeySelect(callback: (key?: ppt.KeyEvent) => void) { let modal = createModal({ header: tr("Select a key"), diff --git a/shared/js/ui/modal/ModalMusicManage.ts b/shared/js/ui/modal/ModalMusicManage.ts index ce84ee24..1736ca05 100644 --- a/shared/js/ui/modal/ModalMusicManage.ts +++ b/shared/js/ui/modal/ModalMusicManage.ts @@ -1,4 +1,8 @@ -export namespace Modals { +/// +/// +/// + +namespace Modals { export function openMusicManage(client: ConnectionHandler, bot: MusicClientEntry) { const ev_registry = new events.Registry(); ev_registry.enable_debug("music-manage"); diff --git a/shared/js/ui/modal/ModalNewcomer.ts b/shared/js/ui/modal/ModalNewcomer.ts index 6a8d350d..20f414a8 100644 --- a/shared/js/ui/modal/ModalNewcomer.ts +++ b/shared/js/ui/modal/ModalNewcomer.ts @@ -1,4 +1,8 @@ -export namespace Modals { +/// +/// +/// + +namespace Modals { const next_step: {[key: string]:string} = { "welcome": "microphone", //"microphone": app.is_web() ? "identity" : "speaker", /* speaker setup only for the native client! */ diff --git a/shared/js/ui/modal/ModalPlaylistEdit.ts b/shared/js/ui/modal/ModalPlaylistEdit.ts index e6a4e48b..69ec39c5 100644 --- a/shared/js/ui/modal/ModalPlaylistEdit.ts +++ b/shared/js/ui/modal/ModalPlaylistEdit.ts @@ -1,4 +1,8 @@ -export namespace Modals { +/// +/// +/// + +namespace Modals { export function spawnPlaylistSongInfo(song: PlaylistSong) { let modal: Modal; diff --git a/shared/js/ui/modal/ModalPlaylistList.ts b/shared/js/ui/modal/ModalPlaylistList.ts index 887a65f4..58c7451c 100644 --- a/shared/js/ui/modal/ModalPlaylistList.ts +++ b/shared/js/ui/modal/ModalPlaylistList.ts @@ -1,4 +1,9 @@ -export namespace Modals { +/// +/// +/// +/// + +namespace Modals { export function spawnPlaylistManage(client: ConnectionHandler) { { createErrorModal(tr("Not implemented"), tr("Playlist management hasn't yet been implemented")).open(); diff --git a/shared/js/ui/modal/ModalPoke.ts b/shared/js/ui/modal/ModalPoke.ts index 744bf155..d4769a2c 100644 --- a/shared/js/ui/modal/ModalPoke.ts +++ b/shared/js/ui/modal/ModalPoke.ts @@ -1,4 +1,8 @@ -export namespace Modals { +/// +/// +/// + +namespace Modals { let global_modal: PokeModal; interface ServerEntry { diff --git a/shared/js/ui/modal/ModalQuery.ts b/shared/js/ui/modal/ModalQuery.ts index 0fc337d1..af3e8560 100644 --- a/shared/js/ui/modal/ModalQuery.ts +++ b/shared/js/ui/modal/ModalQuery.ts @@ -1,4 +1,8 @@ -export namespace Modals { +/// +/// +/// + +namespace Modals { export function spawnQueryCreate(connection: ConnectionHandler, callback_created?: (user, pass) => any) { let modal; modal = createModal({ diff --git a/shared/js/ui/modal/ModalQueryManage.ts b/shared/js/ui/modal/ModalQueryManage.ts index da9cd35a..c856ab83 100644 --- a/shared/js/ui/modal/ModalQueryManage.ts +++ b/shared/js/ui/modal/ModalQueryManage.ts @@ -1,4 +1,8 @@ -export namespace Modals { +/// +/// +/// + +namespace Modals { /* export function spawnQueryManage(client: ConnectionHandler) { let modal: Modal; diff --git a/shared/js/ui/modal/ModalServerEdit.ts b/shared/js/ui/modal/ModalServerEdit.ts index cf94122e..089f1522 100644 --- a/shared/js/ui/modal/ModalServerEdit.ts +++ b/shared/js/ui/modal/ModalServerEdit.ts @@ -1,4 +1,4 @@ -export namespace Modals { +namespace Modals { export function createServerModal(server: ServerEntry, callback: (properties?: ServerProperties) => Promise) { const properties = Object.assign({}, server.properties); diff --git a/shared/js/ui/modal/ModalServerInfo.ts b/shared/js/ui/modal/ModalServerInfo.ts index d470f174..08fe0c71 100644 --- a/shared/js/ui/modal/ModalServerInfo.ts +++ b/shared/js/ui/modal/ModalServerInfo.ts @@ -1,4 +1,4 @@ -export namespace Modals { +namespace Modals { export function openServerInfo(server: ServerEntry) { let modal: Modal; let update_callbacks: ServerBandwidthInfoUpdateCallback[] = []; diff --git a/shared/js/ui/modal/ModalServerInfoBandwidth.ts b/shared/js/ui/modal/ModalServerInfoBandwidth.ts index 4965b8f8..8bb6fdc8 100644 --- a/shared/js/ui/modal/ModalServerInfoBandwidth.ts +++ b/shared/js/ui/modal/ModalServerInfoBandwidth.ts @@ -1,4 +1,4 @@ -export namespace Modals { +namespace Modals { export enum RequestInfoStatus { SUCCESS, UNKNOWN, diff --git a/shared/js/ui/modal/ModalSettings.ts b/shared/js/ui/modal/ModalSettings.ts index 523b28e1..155e3076 100644 --- a/shared/js/ui/modal/ModalSettings.ts +++ b/shared/js/ui/modal/ModalSettings.ts @@ -1,4 +1,4 @@ -export namespace Modals { +namespace Modals { export function spawnSettingsModal(default_page?: string) : Modal { let modal: Modal; modal = createModal({ diff --git a/shared/js/ui/modal/ModalYesNo.ts b/shared/js/ui/modal/ModalYesNo.ts index 8a9af6b1..4562d4ac 100644 --- a/shared/js/ui/modal/ModalYesNo.ts +++ b/shared/js/ui/modal/ModalYesNo.ts @@ -1,6 +1,6 @@ -import {BodyCreator, ModalFunctions} from "../elements/modal"; +/// -export namespace Modals { +namespace Modals { export function spawnYesNo(header: BodyCreator, body: BodyCreator, callback: (_: boolean) => any, properties?: { text_yes?: string, text_no?: string, diff --git a/shared/js/ui/modal/permission/CanvasPermissionEditor.ts b/shared/js/ui/modal/permission/CanvasPermissionEditor.ts index edf6b296..f2c5ecfd 100644 --- a/shared/js/ui/modal/permission/CanvasPermissionEditor.ts +++ b/shared/js/ui/modal/permission/CanvasPermissionEditor.ts @@ -1,5 +1,7 @@ +/// /* first needs the AbstractPermissionEdit */ + /* Canvas Permission Editor */ -export namespace pe { +namespace pe { namespace ui { export namespace scheme { export interface CheckBox { diff --git a/shared/js/ui/modal/permission/HTMLPermissionEditor.ts b/shared/js/ui/modal/permission/HTMLPermissionEditor.ts index 32a13d0b..f1400da9 100644 --- a/shared/js/ui/modal/permission/HTMLPermissionEditor.ts +++ b/shared/js/ui/modal/permission/HTMLPermissionEditor.ts @@ -1,4 +1,6 @@ -export namespace pe { +/// /* first needs the AbstractPermissionEdit */ + +namespace pe { class HTMLPermission { readonly handle: HTMLPermissionEditor; readonly group: HTMLPermissionGroup; @@ -121,7 +123,7 @@ export namespace pe { this._tag_value_input.on('change', event => { const str_value = this._tag_value_input.val() as string; const value = parseInt(str_value); - if(!HTMLPermission.number_filter_re.test(str_value) || value == NaN) { + if(!HTMLPermission.number_filter_re.test(str_value) || isNaN(value)) { console.warn(tr("Failed to parse given permission value string: %s"), this._tag_value_input.val()); this._reset_value(); return; diff --git a/shared/js/ui/modal/permission/ModalPermissionEdit.ts b/shared/js/ui/modal/permission/ModalPermissionEdit.ts index fcb85f8a..f405dd9c 100644 --- a/shared/js/ui/modal/permission/ModalPermissionEdit.ts +++ b/shared/js/ui/modal/permission/ModalPermissionEdit.ts @@ -1,8 +1,12 @@ +/// +/// +/// + interface JQuery { dropdown: any; } -export namespace Modals { +namespace Modals { export namespace PermissionEditor { export interface PermissionEntry { tag: JQuery; diff --git a/shared/js/ui/modal/permission/SenselessPermissions.ts b/shared/js/ui/modal/permission/SenselessPermissions.ts index 1ff6524b..06ceed6a 100644 --- a/shared/js/ui/modal/permission/SenselessPermissions.ts +++ b/shared/js/ui/modal/permission/SenselessPermissions.ts @@ -1,6 +1,6 @@ -import {PermissionType} from "../../../permission/PermissionManager"; +/// -export namespace permissions { +namespace permissions { export const senseless_server_group_permissions: PermissionType[] = [ PermissionType.B_CHANNEL_GROUP_INHERITANCE_END ]; diff --git a/shared/js/channel-tree/server.ts b/shared/js/ui/server.ts similarity index 98% rename from shared/js/channel-tree/server.ts rename to shared/js/ui/server.ts index 2c02ae8a..598f9d22 100644 --- a/shared/js/channel-tree/server.ts +++ b/shared/js/ui/server.ts @@ -1,9 +1,7 @@ /// -/// +/// -import {ChannelTree} from "./view"; - -export class ServerProperties { +class ServerProperties { virtualserver_host: string = ""; virtualserver_port: number = 0; @@ -80,7 +78,7 @@ export class ServerProperties { virtualserver_total_bytes_uploaded: number = 0; } -export interface ServerConnectionInfo { +interface ServerConnectionInfo { connection_filetransfer_bandwidth_sent: number; connection_filetransfer_bandwidth_received: number; @@ -105,12 +103,12 @@ export interface ServerConnectionInfo { connection_ping: number; } -export interface ServerAddress { +interface ServerAddress { host: string; port: number; } -export class ServerEntry { +class ServerEntry { remote_address: ServerAddress; channelTree: ChannelTree; properties: ServerProperties; diff --git a/shared/js/channel-tree/view.ts b/shared/js/ui/view.ts similarity index 99% rename from shared/js/channel-tree/view.ts rename to shared/js/ui/view.ts index 5f1e8108..4530e28e 100644 --- a/shared/js/channel-tree/view.ts +++ b/shared/js/ui/view.ts @@ -4,12 +4,12 @@ /// /// /// -/// -/// +/// +/// /// -export class ChannelTree { +class ChannelTree { client: ConnectionHandler; server: ServerEntry; diff --git a/shared/js/utils/helpers.ts b/shared/js/utils/helpers.ts index f3104911..1b8cc2d9 100644 --- a/shared/js/utils/helpers.ts +++ b/shared/js/utils/helpers.ts @@ -1,6 +1,6 @@ -import {sha} from "../crypto/sha"; +/// -export namespace helpers { +namespace helpers { export function hashPassword(password: string) : Promise { return new Promise((resolve, reject) => { sha.sha1(password).then(result => { @@ -10,7 +10,7 @@ export namespace helpers { } } -export class LaterPromise extends Promise { +class LaterPromise extends Promise { private _handle: Promise; private _resolve: ($: T) => any; private _reject: ($: any) => any; diff --git a/shared/js/voice/RecorderBase.ts b/shared/js/voice/RecorderBase.ts index 3fb11f55..ba32fb38 100644 --- a/shared/js/voice/RecorderBase.ts +++ b/shared/js/voice/RecorderBase.ts @@ -1,4 +1,4 @@ -export namespace audio { +namespace audio { export namespace recorder { export interface InputDevice { unique_id: string; diff --git a/shared/js/voice/RecorderProfile.ts b/shared/js/voice/RecorderProfile.ts index d149a6ac..ec5ea0dc 100644 --- a/shared/js/voice/RecorderProfile.ts +++ b/shared/js/voice/RecorderProfile.ts @@ -1,7 +1,7 @@ -import {ConnectionHandler} from "../ConnectionHandler"; +/// -export type VadType = "threshold" | "push_to_talk" | "active"; -export interface RecorderProfileConfig { +type VadType = "threshold" | "push_to_talk" | "active"; +interface RecorderProfileConfig { version: number; /* devices unique id */ @@ -25,8 +25,8 @@ export interface RecorderProfileConfig { } } -export let default_recorder: RecorderProfile; /* needs initialize */ -export class RecorderProfile { +let default_recorder: RecorderProfile; /* needs initialize */ +class RecorderProfile { readonly name; readonly volatile; /* not saving profile */ diff --git a/tsconfig.json b/tsconfig.json index 95a8baf6..fdf294a3 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -9,7 +9,9 @@ "jsx": "react", "baseUrl": ".", "paths": { - "*": ["shared/declarations/*"] + "*": ["shared/declarations/*"], + "tc-shared/*": ["shared/js/*"], + "tc-backend/*": ["web/js/*"] } }, "exclude": [ diff --git a/web/js/audio/AudioPlayer.ts b/web/js/audio/AudioPlayer.ts index a0c6ea34..25538997 100644 --- a/web/js/audio/AudioPlayer.ts +++ b/web/js/audio/AudioPlayer.ts @@ -1,5 +1,3 @@ -import {log, LogCategory} from "shared-app/log"; - namespace audio.player { let _globalContext: AudioContext; let _global_destination: GainNode; diff --git a/webpack.config.js b/webpack.config.js index 9c9a692f..ca37ea76 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -51,6 +51,10 @@ module.exports = { }, resolve: { extensions: ['.tsx', '.ts', '.js', ".scss"], + alias: { + "tc-shared": path.resolve(__dirname, "shared/js"), + "tc-backend": path.resolve(__dirname, "web/js") + } }, output: { filename: 'bundle.js',