/// /// /// enum ClientType { CLIENT_VOICE, CLIENT_QUERY, CLIENT_INTERNAL, CLIENT_WEB, CLIENT_MUSIC, CLIENT_UNDEFINED } class ClientProperties { client_type: ClientType = ClientType.CLIENT_VOICE; //TeamSpeaks type client_type_exact: ClientType = ClientType.CLIENT_VOICE; client_database_id: number = 0; client_version: string = ""; client_platform: string = ""; client_nickname: string = "unknown"; client_unique_identifier: string = "unknown"; client_description: string = ""; client_servergroups: string = ""; client_channel_group_id: number = 0; client_lastconnected: number = 0; client_created: number = 0; client_totalconnections: number = 0; client_flag_avatar: string = ""; client_icon_id: number = 0; client_away_message: string = ""; client_away: boolean = false; client_country: string = ""; client_input_hardware: boolean = false; client_output_hardware: boolean = false; client_input_muted: boolean = false; client_output_muted: boolean = false; client_is_channel_commander: boolean = false; client_teaforo_id: number = 0; client_teaforo_name: string = ""; client_teaforo_flags: number = 0; /* 0x01 := Banned | 0x02 := Stuff | 0x04 := Premium */ /* not updated in view! */ client_month_bytes_uploaded: number = 0; client_month_bytes_downloaded: number = 0; client_total_bytes_uploaded: number = 0; client_total_bytes_downloaded: number = 0; client_talk_power: number = 0; } 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; connection_bandwidth_received_last_second_control: number = -1; connection_bandwidth_received_last_second_keepalive: number = -1; connection_bandwidth_received_last_second_speech: number = -1; connection_bandwidth_sent_last_minute_control: number = -1; connection_bandwidth_sent_last_minute_keepalive: number = -1; connection_bandwidth_sent_last_minute_speech: number = -1; connection_bandwidth_sent_last_second_control: number = -1; connection_bandwidth_sent_last_second_keepalive: number = -1; connection_bandwidth_sent_last_second_speech: number = -1; connection_bytes_received_control: number = -1; connection_bytes_received_keepalive: number = -1; connection_bytes_received_speech: number = -1; connection_bytes_sent_control: number = -1; connection_bytes_sent_keepalive: number = -1; connection_bytes_sent_speech: number = -1; connection_packets_received_control: number = -1; connection_packets_received_keepalive: number = -1; connection_packets_received_speech: number = -1; connection_packets_sent_control: number = -1; connection_packets_sent_keepalive: number = -1; connection_packets_sent_speech: number = -1; connection_ping: number = -1; connection_ping_deviation: number = -1; connection_server2client_packetloss_control: number = -1; connection_server2client_packetloss_keepalive: number = -1; connection_server2client_packetloss_speech: number = -1; connection_server2client_packetloss_total: number = -1; connection_client2server_packetloss_speech: number = -1; connection_client2server_packetloss_keepalive: number = -1; connection_client2server_packetloss_control: number = -1; connection_client2server_packetloss_total: number = -1; connection_filetransfer_bandwidth_sent: number = -1; connection_filetransfer_bandwidth_received: number = -1; connection_connected_time: number = -1; connection_idle_time: number = -1; connection_client_ip: string | undefined; connection_client_port: number = -1; } class ClientEntry { protected _clientId: number; protected _channel: ChannelEntry; protected _tag: JQuery; protected _properties: ClientProperties; protected lastVariableUpdate: number = 0; protected _speaking: boolean; protected _listener_initialized: boolean; protected _audio_handle: connection.voice.VoiceClient; protected _audio_volume: number; protected _audio_muted: boolean; private _info_variables_promise: Promise; private _info_variables_promise_timestamp: number; private _info_connection_promise: Promise; private _info_connection_promise_timestamp: number; private _info_connection_promise_resolve: any; private _info_connection_promise_reject: any; channelTree: ChannelTree; constructor(clientId: number, clientName, properties: ClientProperties = new ClientProperties()) { this._properties = properties; this._properties.client_nickname = clientName; this._clientId = clientId; this.channelTree = null; this._channel = null; } destroy() { if(this._tag) { this._tag.remove(); this._tag = undefined; } if(this._audio_handle) { log.warn(LogCategory.AUDIO, tr("Destroying client with an active audio handle. This could cause memory leaks!")); try { this._audio_handle.abort_replay(); } catch(error) { log.warn(LogCategory.AUDIO, tr("Failed to abort replay: %o"), error); } this._audio_handle.callback_playback = undefined; this._audio_handle.callback_stopped = undefined; this._audio_handle = undefined; } this._channel = undefined; } tree_unregistered() { this.channelTree = undefined; if(this._audio_handle) { try { this._audio_handle.abort_replay(); } catch(error) { log.warn(LogCategory.AUDIO, tr("Failed to abort replay: %o"), error); } this._audio_handle.callback_playback = undefined; this._audio_handle.callback_stopped = undefined; this._audio_handle = undefined; } this._channel = undefined; } set_audio_handle(handle: connection.voice.VoiceClient) { if(this._audio_handle === handle) return; if(this._audio_handle) { this._audio_handle.callback_playback = undefined; this._audio_handle.callback_stopped = undefined; } //TODO may ensure that the id is the same? this._audio_handle = handle; if(!handle) { this.speaking = false; return; } handle.callback_playback = () => this.speaking = true; handle.callback_stopped = () => this.speaking = false; } get_audio_handle() : connection.voice.VoiceClient { return this._audio_handle; } get properties() : ClientProperties { return this._properties; } currentChannel() : ChannelEntry { return this._channel; } clientNickName(){ return this.properties.client_nickname; } clientUid(){ return this.properties.client_unique_identifier; } clientId(){ return this._clientId; } is_muted() { return !!this._audio_muted; } set_muted(flag: boolean, update_icon: boolean, force?: boolean) { if(this._audio_muted === flag && !force) return; if(flag) { this.channelTree.client.serverConnection.send_command('clientmute', { clid: this.clientId() }); } else if(this._audio_muted) { this.channelTree.client.serverConnection.send_command('clientunmute', { clid: this.clientId() }); } this._audio_muted = flag; this.channelTree.client.settings.changeServer("mute_client_" + this.clientUid(), flag); if(this._audio_handle) { if(flag) { this._audio_handle.set_volume(0); } else { this._audio_handle.set_volume(this._audio_volume); } } if(update_icon) this.updateClientSpeakIcon(); for(const client of this.channelTree.clients) { if(client === this || client.properties.client_unique_identifier != this.properties.client_unique_identifier) continue; client.set_muted(flag, true); } } protected initializeListener() { if(this._listener_initialized) return; this._listener_initialized = true; this.tag.on('mouseup', event => { if(!this.channelTree.client_mover.is_active()) { this.channelTree.onSelect(this); } }); if(!(this instanceof LocalClientEntry) && !(this instanceof MusicClientEntry)) this.tag.dblclick(event => { if($.isArray(this.channelTree.currently_selected)) { //Multiselect return; } this.open_text_chat(); }); if(!settings.static(Settings.KEY_DISABLE_CONTEXT_MENU, false)) { this.tag.on("contextmenu", (event) => { event.preventDefault(); if($.isArray(this.channelTree.currently_selected)) { //Multiselect (this.channelTree.currently_selected_context_callback || ((_) => null))(event); return; } this.channelTree.onSelect(this, true); this.showContextMenu(event.pageX, event.pageY, () => {}); return false; }); } this.tag.on('mousedown', event => { if(event.which != 1) return; //Only the left button let clients = this.channelTree.currently_selected as (ClientEntry | ClientEntry[]); if(ppt.key_pressed(ppt.SpecialKey.SHIFT)) { if(clients != this && !($.isArray(clients) && clients.indexOf(this) != -1)) clients = $.isArray(clients) ? [...clients, this] : [clients, this]; } else { clients = this; } this.channelTree.client_mover.activate(clients, target => { if(!target) return; for(const client of $.isArray(clients) ? clients : [clients]) { if(target == client._channel) continue; const source = client._channel; const self = this.channelTree.client.getClient(); this.channelTree.client.serverConnection.send_command("clientmove", { clid: client.clientId(), cid: target.getChannelId() }).then(event => { if(client.clientId() == this.channelTree.client.clientId) this.channelTree.client.sound.play(Sound.CHANNEL_JOINED); else if(target !== source && target != self.currentChannel()) this.channelTree.client.sound.play(Sound.USER_MOVED); }); } this.channelTree.onSelect(); }, event); }); } protected contextmenu_info() : contextmenu.MenuEntry[] { return [ { type: contextmenu.MenuEntryType.ENTRY, name: this.properties.client_type_exact === ClientType.CLIENT_MUSIC ? tr("Show bot info") : tr("Show client info"), callback: () => { this.channelTree.client.side_bar.show_client_info(this); }, icon_class: "client-about", visible: !settings.static_global(Settings.KEY_SWITCH_INSTANT_CLIENT) }, { callback: () => {}, type: contextmenu.MenuEntryType.HR, name: "", visible: !settings.static_global(Settings.KEY_SWITCH_INSTANT_CLIENT) } ] } protected assignment_context() : contextmenu.MenuEntry[] { let server_groups: contextmenu.MenuEntry[] = []; for(let group of this.channelTree.client.groups.serverGroups.sort(GroupManager.sorter())) { if(group.type != GroupType.NORMAL) continue; let entry: contextmenu.MenuEntry = {} as any; //TODO: May add the server group icon? entry.checkbox_checked = this.groupAssigned(group); entry.name = group.name + " [" + (group.properties.savedb ? "perm" : "tmp") + "]"; if(this.groupAssigned(group)) { entry.callback = () => { this.channelTree.client.serverConnection.send_command("servergroupdelclient", { sgid: group.id, cldbid: this.properties.client_database_id }); }; entry.disabled = !this.channelTree.client.permissions.neededPermission(PermissionType.I_GROUP_MEMBER_ADD_POWER).granted(group.requiredMemberRemovePower); } else { entry.callback = () => { this.channelTree.client.serverConnection.send_command("servergroupaddclient", { sgid: group.id, cldbid: this.properties.client_database_id }); }; entry.disabled = !this.channelTree.client.permissions.neededPermission(PermissionType.I_GROUP_MEMBER_REMOVE_POWER).granted(group.requiredMemberAddPower); } entry.type = contextmenu.MenuEntryType.CHECKBOX; server_groups.push(entry); } let channel_groups: contextmenu.MenuEntry[] = []; for(let group of this.channelTree.client.groups.channelGroups.sort(GroupManager.sorter())) { if(group.type != GroupType.NORMAL) continue; let entry: contextmenu.MenuEntry = {} as any; //TODO: May add the channel group icon? entry.checkbox_checked = this.assignedChannelGroup() == group.id; entry.name = group.name + " [" + (group.properties.savedb ? "perm" : "tmp") + "]"; entry.callback = () => { this.channelTree.client.serverConnection.send_command("setclientchannelgroup", { cldbid: this.properties.client_database_id, cgid: group.id, cid: this.currentChannel().channelId }); }; entry.disabled = !this.channelTree.client.permissions.neededPermission(PermissionType.I_GROUP_MEMBER_ADD_POWER).granted(group.requiredMemberRemovePower); entry.type = contextmenu.MenuEntryType.CHECKBOX; channel_groups.push(entry); } return [{ type: contextmenu.MenuEntryType.SUB_MENU, icon_class: "client-permission_server_groups", name: tr("Set server group"), sub_menu: [ { type: contextmenu.MenuEntryType.ENTRY, icon_class: "client-permission_server_groups", name: "Server groups dialog", callback: () => this.open_assignment_modal() }, contextmenu.Entry.HR(), ...server_groups ] },{ type: contextmenu.MenuEntryType.SUB_MENU, icon_class: "client-permission_channel", name: tr("Set channel group"), sub_menu: [ ...channel_groups ] },{ type: contextmenu.MenuEntryType.SUB_MENU, icon_class: "client-permission_client", name: tr("Permissions"), sub_menu: [ { type: contextmenu.MenuEntryType.ENTRY, icon_class: "client-permission_client", name: tr("Client permissions"), callback: () => Modals.spawnPermissionEdit(this.channelTree.client, "clp", {unique_id: this.clientUid()}).open() }, { type: contextmenu.MenuEntryType.ENTRY, icon_class: "client-permission_client", name: tr("Client channel permissions"), callback: () => Modals.spawnPermissionEdit(this.channelTree.client, "clchp", {unique_id: this.clientUid(), channel_id: this._channel ? this._channel.channelId : undefined }).open() } ] }]; } open_assignment_modal() { Modals.createServerGroupAssignmentModal(this, (groups, flag) => { if(groups.length == 0) return Promise.resolve(true); if(groups.length == 1) { if(flag) { return this.channelTree.client.serverConnection.send_command("servergroupaddclient", { sgid: groups[0], cldbid: this.properties.client_database_id }).then(result => true); } else return this.channelTree.client.serverConnection.send_command("servergroupdelclient", { sgid: groups[0], cldbid: this.properties.client_database_id }).then(result => true); } else { const data = groups.map(e => { return {sgid: e}; }); data[0]["cldbid"] = this.properties.client_database_id; if(flag) { return this.channelTree.client.serverConnection.send_command("clientaddservergroup", data, {flagset: ["continueonerror"]}).then(result => true); } else return this.channelTree.client.serverConnection.send_command("clientdelservergroup", data, {flagset: ["continueonerror"]}).then(result => true); } }); } open_text_chat() { const chat = this.channelTree.client.side_bar; const conversation = chat.private_conversations().find_conversation({ name: this.clientNickName(), client_id: this.clientId(), unique_id: this.clientUid() }, { attach: true, create: true }); chat.private_conversations().set_selected_conversation(conversation); chat.show_private_conversations(); chat.private_conversations().try_input_focus(); } showContextMenu(x: number, y: number, on_close: () => void = undefined) { let trigger_close = true; contextmenu.spawn_context_menu(x, y, ...this.contextmenu_info(), { type: contextmenu.MenuEntryType.ENTRY, icon_class: "client-change_nickname", name: (contextmenu.get_provider().html_format_enabled() ? "" : "") + tr("Open text chat") + (contextmenu.get_provider().html_format_enabled() ? "" : ""), callback: () => { this.open_text_chat(); } }, contextmenu.Entry.HR(), { type: contextmenu.MenuEntryType.ENTRY, icon_class: "client-about", name: tr("Show client info"), callback: () => Modals.openClientInfo(this) }, contextmenu.Entry.HR(), { type: contextmenu.MenuEntryType.ENTRY, icon_class: "client-poke", name: tr("Poke client"), callback: () => { createInputModal(tr("Poke client"), tr("Poke message:
"), text => true, result => { if(typeof(result) === "string") { //TODO tr console.log("Poking client " + this.clientNickName() + " with message " + result); this.channelTree.client.serverConnection.send_command("clientpoke", { clid: this.clientId(), msg: result }); } }, { width: 400, maxLength: 512 }).open(); } }, { type: contextmenu.MenuEntryType.ENTRY, icon_class: "client-edit", name: tr("Change description"), callback: () => { createInputModal(tr("Change client description"), tr("New description:
"), text => true, result => { if(typeof(result) === "string") { //TODO tr console.log("Changing " + this.clientNickName() + "'s description to " + result); this.channelTree.client.serverConnection.send_command("clientedit", { clid: this.clientId(), client_description: result }); } }, { width: 400, maxLength: 1024 }).open(); } }, contextmenu.Entry.HR(), ...this.assignment_context(), contextmenu.Entry.HR(), { type: contextmenu.MenuEntryType.ENTRY, icon_class: "client-move_client_to_own_channel", name: tr("Move client to your channel"), callback: () => { this.channelTree.client.serverConnection.send_command("clientmove", { clid: this.clientId(), cid: this.channelTree.client.getClient().currentChannel().getChannelId() }); } }, { type: contextmenu.MenuEntryType.ENTRY, icon_class: "client-kick_channel", name: tr("Kick client from channel"), callback: () => { createInputModal(tr("Kick client from channel"), tr("Kick reason:
"), text => true, result => { if(typeof(result) !== 'boolean' || result) { //TODO tr console.log("Kicking client " + this.clientNickName() + " from channel with reason " + result); this.channelTree.client.serverConnection.send_command("clientkick", { clid: this.clientId(), reasonid: ViewReasonId.VREASON_CHANNEL_KICK, reasonmsg: result }); } }, { width: 400, maxLength: 255 }).open(); } }, { type: contextmenu.MenuEntryType.ENTRY, icon_class: "client-kick_server", name: tr("Kick client fom server"), callback: () => { createInputModal(tr("Kick client from server"), tr("Kick reason:
"), text => true, result => { if(typeof(result) !== 'boolean' || result) { //TODO tr console.log("Kicking client " + this.clientNickName() + " from server with reason " + result); this.channelTree.client.serverConnection.send_command("clientkick", { clid: this.clientId(), reasonid: ViewReasonId.VREASON_SERVER_KICK, reasonmsg: result }); } }, { width: 400, maxLength: 255 }).open(); } }, { type: contextmenu.MenuEntryType.ENTRY, icon_class: "client-ban_client", name: tr("Ban client"), invalidPermission: !this.channelTree.client.permissions.neededPermission(PermissionType.I_CLIENT_BAN_MAX_BANTIME).granted(1), callback: () => { Modals.spawnBanClient(this.channelTree.client, [{ name: this.properties.client_nickname, unique_id: this.properties.client_unique_identifier }], (data) => { this.channelTree.client.serverConnection.send_command("banclient", { uid: this.properties.client_unique_identifier, banreason: data.reason, time: data.length }, { flagset: [data.no_ip ? "no-ip" : "", data.no_hwid ? "no-hardware-id" : "", data.no_name ? "no-nickname" : ""] }).then(() => { this.channelTree.client.sound.play(Sound.USER_BANNED); }); }); } }, contextmenu.Entry.HR(), /* { type: MenuEntryType.ENTRY, icon: "client-kick_server", name: "Add group to client", invalidPermission: true, //!this.channelTree.client.permissions.neededPermission(PermissionType.I_CLIENT_BAN_MAX_BANTIME).granted(1), callback: () => { Modals.spawnBanClient(this.properties.client_nickname, (duration, reason) => { this.channelTree.client.serverConnection.send_command("banclient", { uid: this.properties.client_unique_identifier, banreason: reason, time: duration }); }); } }, MenuEntry.HR(), */ { type: contextmenu.MenuEntryType.ENTRY, icon_class: "client-volume", name: tr("Change Volume"), callback: () => { Modals.spawnChangeVolume(this, true, this._audio_volume, undefined, volume => { this._audio_volume = volume; this.channelTree.client.settings.changeServer("volume_client_" + this.clientUid(), volume); if(this._audio_handle) this._audio_handle.set_volume(volume); //TODO: Update in info }); } }, { type: contextmenu.MenuEntryType.ENTRY, name: tr("Change playback latency"), callback: () => { Modals.spawnChangeLatency(this, this._audio_handle.latency_settings(), () => { this._audio_handle.reset_latency_settings(); return this._audio_handle.latency_settings(); }, settings => this._audio_handle.latency_settings(settings)); }, visible: this._audio_handle && this._audio_handle.support_latency_settings() }, { type: contextmenu.MenuEntryType.ENTRY, icon_class: "client-input_muted_local", name: tr("Mute client"), visible: !this._audio_muted, callback: () => this.set_muted(true, true) }, { type: contextmenu.MenuEntryType.ENTRY, icon_class: "client-input_muted_local", name: tr("Unmute client"), visible: this._audio_muted, callback: () => this.set_muted(false, true) }, contextmenu.Entry.CLOSE(() => trigger_close ? on_close() : {}) ); } get tag() : JQuery { if(this._tag) return this._tag; let container_client = $.spawn("div") .addClass("tree-entry client") .attr("client-id", this.clientId()); /* unread marker */ { container_client.append( $.spawn("div") .addClass("marker-text-unread hidden") .attr("private-conversation", this._clientId) ); } container_client.append( $.spawn("div") .addClass("icon_client_state") .attr("title", "Client state") ); container_client.append( $.spawn("div") .addClass("group-prefix") .attr("title", "Server groups prefixes") .hide() ); container_client.append( $.spawn("div") .addClass("client-name") .text(this.clientNickName()) ); container_client.append( $.spawn("div") .addClass("group-suffix") .attr("title", "Server groups suffix") .hide() ); container_client.append( $.spawn("div") .addClass("client-away-message") .text(this.clientNickName()) ); let container_icons = $.spawn("div").addClass("container-icons"); container_icons.append( $.spawn("div") .addClass("icon icon_talk_power client-input_muted") .hide() ); container_icons.append( $.spawn("div") .addClass("container-icons-group") ); container_icons.append( $.spawn("div") .addClass("container-icon-client") ); container_client.append(container_icons); this._tag = container_client; this.initializeListener(); return this._tag; } static bbcodeTag(id: number, name: string, uid: string) : string { return "[url=client://" + id + "/" + uid + "~" + encodeURIComponent(name) + "]" + name + "[/url]"; } static chatTag(id: number, name: string, uid: string, braces: boolean = false) : JQuery { return $(htmltags.generate_client({ client_name: name, client_id: id, client_unique_id: uid, add_braces: braces })); } create_bbcode() : string { return ClientEntry.bbcodeTag(this.clientId(), this.clientNickName(), this.clientUid()); } createChatTag(braces: boolean = false) : JQuery { return ClientEntry.chatTag(this.clientId(), this.clientNickName(), this.clientUid(), braces); } set speaking(flag) { if(flag === this._speaking) return; this._speaking = flag; this.updateClientSpeakIcon(); } updateClientStatusIcons() { let talk_power = this.properties.client_talk_power >= this._channel.properties.channel_needed_talk_power; if(talk_power) this.tag.find(".icon_talk_power").hide(); else this.tag.find(".icon_talk_power").show(); } updateClientSpeakIcon() { let icon: string = ""; let clicon: string = ""; if(this.properties.client_type_exact == ClientType.CLIENT_QUERY) { icon = "client-server_query"; console.log("Server query!"); } else { if (this.properties.client_away) { icon = "client-away"; } else if (this._audio_muted && !(this instanceof LocalClientEntry)) { icon = "client-input_muted_local"; } else if(!this.properties.client_output_hardware) { icon = "client-hardware_output_muted"; } else if(this.properties.client_output_muted) { icon = "client-output_muted"; } else if(!this.properties.client_input_hardware) { icon = "client-hardware_input_muted"; } else if(this.properties.client_input_muted) { icon = "client-input_muted"; } else { if(this._speaking) { if(this.properties.client_is_channel_commander) clicon = "client_cc_talk"; else clicon = "client_talk"; } else { if(this.properties.client_is_channel_commander) clicon = "client_cc_idle"; else clicon = "client_idle"; } } } if(clicon.length > 0) this.tag.find(".icon_client_state").attr('class', 'icon_client_state clicon ' + clicon); else if(icon.length > 0) this.tag.find(".icon_client_state").attr('class', 'icon_client_state icon ' + icon); else this.tag.find(".icon_client_state").attr('class', 'icon_client_state icon_empty'); } updateAwayMessage() { let tag = this.tag.find(".client-away-message"); if(this.properties.client_away == true && this.properties.client_away_message){ tag.text("[" + this.properties.client_away_message + "]"); tag.show(); } else { tag.hide(); } } updateVariables(...variables: {key: string, value: string}[]) { let group = log.group(log.LogType.DEBUG, LogCategory.CLIENT, tr("Update properties (%i) of %s (%i)"), variables.length, this.clientNickName(), this.clientId()); let update_icon_status = false; let update_icon_speech = false; let update_away = false; let reorder_channel = false; let update_avatar = false; { const entries = []; for(const variable of variables) entries.push({ key: variable.key, value: variable.value, type: typeof (this.properties[variable.key]) }); log.table(LogType.DEBUG, LogCategory.PERMISSIONS, "Client update properties", entries); } for(const variable of variables) { const old_value = this._properties[variable.key]; JSON.map_field_to(this._properties, variable.value, variable.key); if(variable.key == "client_nickname") { if(variable.value !== old_value && typeof(old_value) === "string") { if(!(this instanceof LocalClientEntry)) { /* own changes will be logged somewhere else */ this.channelTree.client.log.log(log.server.Type.CLIENT_NICKNAME_CHANGED, { own_client: false, client: this.log_data(), new_name: variable.value, old_name: old_value }); } } this.tag.find(".client-name").text(variable.value); const chat = this.channelTree.client.side_bar; const conversation = chat.private_conversations().find_conversation({ name: this.clientNickName(), client_id: this.clientId(), unique_id: this.clientUid() }, { attach: false, create: false }); if(conversation) conversation.set_client_name(variable.value); reorder_channel = true; } if( variable.key == "client_away" || variable.key == "client_input_hardware" || variable.key == "client_output_hardware" || variable.key == "client_output_muted" || variable.key == "client_input_muted" || variable.key == "client_is_channel_commander"){ update_icon_speech = true; } if(variable.key == "client_away_message" || variable.key == "client_away") { update_away = true; } if(variable.key == "client_unique_identifier") { this._audio_volume = parseFloat(this.channelTree.client.settings.server("volume_client_" + this.clientUid(), "1")); const mute_status = this.channelTree.client.settings.server("mute_client_" + this.clientUid(), false); this.set_muted(mute_status, false, mute_status); /* force only needed when we want to mute the client */ if(this._audio_handle) this._audio_handle.set_volume(this._audio_muted ? 0 : this._audio_volume); update_icon_speech = true; log.debug(LogCategory.CLIENT, tr("Loaded client (%s) server specific properties. Volume: %o Muted: %o."), this.clientUid(), this._audio_volume, this._audio_muted); } if(variable.key == "client_talk_power") { reorder_channel = true; update_icon_status = true; } if(variable.key == "client_icon_id") { /* yeah we like javascript. Due to JS wiered integer behaviour parsing for example fails for 18446744073409829863. * parseInt("18446744073409829863") evaluates to 18446744073409829000. * In opposite "18446744073409829863" >>> 0 evaluates to 3995244544, which is the icon id :) */ this.properties.client_icon_id = variable.value as any >>> 0; this.updateClientIcon(); } if(variable.key =="client_channel_group_id" || variable.key == "client_servergroups") this.update_displayed_client_groups(); else if(variable.key == "client_flag_avatar") update_avatar = true; } /* process updates after variables have been set */ if(this._channel && reorder_channel) this._channel.reorderClients(); if(update_icon_speech) this.updateClientSpeakIcon(); if(update_icon_status) this.updateClientStatusIcons(); if(update_away) this.updateAwayMessage(); const side_bar = this.channelTree.client.side_bar; { const client_info = side_bar.client_info(); if(client_info.current_client() === this) client_info.set_current_client(this, true); /* force an update */ } if(update_avatar) { this.channelTree.client.fileManager.avatars.update_cache(this.avatarId(), this.properties.client_flag_avatar); const conversations = side_bar.private_conversations(); const conversation = conversations.find_conversation({name: this.clientNickName(), unique_id: this.clientUid(), client_id: this.clientId()}, {create: false, attach: false}); if(conversation) conversation.update_avatar(); } group.end(); } update_displayed_client_groups() { this.tag.find(".container-icons-group").children().remove(); for(let id of this.assignedServerGroupIds()) this.updateGroupIcon(this.channelTree.client.groups.serverGroup(id)); this.update_group_icon_order(); this.updateGroupIcon(this.channelTree.client.groups.channelGroup(this.properties.client_channel_group_id)); let prefix_groups: string[] = []; let suffix_groups: string[] = []; for(const group_id of this.assignedServerGroupIds()) { const group = this.channelTree.client.groups.serverGroup(group_id); if(!group) continue; if(group.properties.namemode == 1) prefix_groups.push(group.name); else if(group.properties.namemode == 2) suffix_groups.push(group.name); } const tag_group_prefix = this.tag.find(".group-prefix"); const tag_group_suffix = this.tag.find(".group-suffix"); if(prefix_groups.length > 0) { tag_group_prefix.text("[" + prefix_groups.join("][") + "]").show(); } else { tag_group_prefix.hide() } if(suffix_groups.length > 0) { tag_group_suffix.text("[" + suffix_groups.join("][") + "]").show(); } else { tag_group_suffix.hide() } } updateClientVariables(force_update?: boolean) : Promise { if(Date.now() - 10 * 60 * 1000 < this._info_variables_promise_timestamp && this._info_variables_promise && (typeof(force_update) !== "boolean" || force_update)) return this._info_variables_promise; this._info_variables_promise_timestamp = Date.now(); return (this._info_variables_promise = new Promise((resolve, reject) => { this.channelTree.client.serverConnection.send_command("clientgetvariables", {clid: this.clientId()}).then(() => resolve()).catch(error => { this._info_connection_promise_timestamp = 0; /* not succeeded */ reject(error); }); })); } updateClientIcon() { this.tag.find(".container-icon-client").children().remove(); if(this.properties.client_icon_id > 0) { this.channelTree.client.fileManager.icons.generateTag(this.properties.client_icon_id).attr("title", "Client icon") .appendTo(this.tag.find(".container-icon-client")); } } updateGroupIcon(group: Group) { if(!group) return; const container = this.tag.find(".container-icons-group"); container.find(".icon_group_" + group.id).remove(); if (group.properties.iconid > 0) { container.append( $.spawn("div").attr('group-power', group.properties.sortid) .addClass("container-group-icon icon_group_" + group.id) .append(this.channelTree.client.fileManager.icons.generateTag(group.properties.iconid)).attr("title", group.name) ); } } update_group_icon_order() { const container = this.tag.find(".container-icons-group"); container.append(...[...container.children()].sort((a, b) => parseInt(a.getAttribute("group-power")) - parseInt(b.getAttribute("group-power")))); } assignedServerGroupIds() : number[] { let result = []; for(let id of this.properties.client_servergroups.split(",")){ if(id.length == 0) continue; result.push(Number.parseInt(id)); } return result; } assignedChannelGroup() : number { return this.properties.client_channel_group_id; } groupAssigned(group: Group) : boolean { if(group.target == GroupTarget.SERVER) { for(let id of this.assignedServerGroupIds()) if(id == group.id) return true; return false; } else return group.id == this.assignedChannelGroup(); } onDelete() { } calculateOnlineTime() : number { return Date.now() / 1000 - this.properties.client_lastconnected; } avatarId?() : string { function str2ab(str) { let buf = new ArrayBuffer(str.length); // 2 bytes for each char let bufView = new Uint8Array(buf); for (let i=0, strLen = str.length; i= '0' && c <= '9') offset = c.charCodeAt(0) - '0'.charCodeAt(0); else if(c >= 'A' && c <= 'F') offset = c.charCodeAt(0) - 'A'.charCodeAt(0) + 0x0A; else if(c >= 'a' && c <= 'f') offset = c.charCodeAt(0) - 'a'.charCodeAt(0) + 0x0A; result += String.fromCharCode('a'.charCodeAt(0) + offset); } return result; } catch (e) { //invalid base 64 (like music bot etc) return undefined; } } update_family_index() { if(!this._channel) return; const index = this._channel.calculate_family_index(); this.tag.css('padding-left', (5 + (index + 2) * 16) + "px"); } log_data() : log.server.base.Client { return { client_unique_id: this.properties.client_unique_identifier, client_name: this.clientNickName(), client_id: this._clientId } } /* max 1s ago, so we could update every second */ request_connection_info() : Promise { if(Date.now() - 900 < this._info_connection_promise_timestamp && this._info_connection_promise) return this._info_connection_promise; if(this._info_connection_promise_reject) this._info_connection_promise_resolve("timeout"); let _local_reject; /* to ensure we're using the right resolve! */ this._info_connection_promise = new Promise((resolve, reject) => { this._info_connection_promise_resolve = resolve; this._info_connection_promise_reject = reject; _local_reject = reject; }); this._info_connection_promise_timestamp = Date.now(); this.channelTree.client.serverConnection.send_command("getconnectioninfo", {clid: this._clientId}).catch(error => _local_reject(error)); return this._info_connection_promise; } set_connection_info(info: ClientConnectionInfo) { if(!this._info_connection_promise_resolve) return; this._info_connection_promise_resolve(info); this._info_connection_promise_resolve = undefined; this._info_connection_promise_reject = undefined; } set flag_text_unread(flag: boolean) { this._tag.find(".marker-text-unread").toggleClass("hidden", !flag); } } class LocalClientEntry extends ClientEntry { handle: ConnectionHandler; private renaming: boolean; constructor(handle: ConnectionHandler) { super(0, "local client"); this.handle = handle; } showContextMenu(x: number, y: number, on_close: () => void = undefined): void { const _self = this; contextmenu.spawn_context_menu(x, y, ...this.contextmenu_info(), { name: (contextmenu.get_provider().html_format_enabled() ? "" : "") + tr("Change name") + (contextmenu.get_provider().html_format_enabled() ? "" : ""), icon_class: "client-change_nickname", callback: () =>_self.openRename(), type: contextmenu.MenuEntryType.ENTRY }, { name: tr("Change description"), icon_class: "client-edit", callback: () => { createInputModal(tr("Change own description"), tr("New description:
"), text => true, result => { if(result) { console.log(tr("Changing own description to %s"), result); _self.channelTree.client.serverConnection.send_command("clientedit", { clid: _self.clientId(), client_description: result }); } }, { width: 400, maxLength: 1024 }).open(); }, type: contextmenu.MenuEntryType.ENTRY }, contextmenu.Entry.HR(), ...this.assignment_context(), contextmenu.Entry.CLOSE(on_close) ); } initializeListener(): void { if(this._listener_initialized) this.tag.off(); this._listener_initialized = false; /* could there be a better system */ super.initializeListener(); this.tag.find(".client-name").addClass("client-name-own"); this.tag.on('dblclick', () => { if(Array.isArray(this.channelTree.currently_selected)) { //Multiselect return; } this.openRename(); }); } openRename() : void { this.channelTree.client_mover.enabled = false; const elm = this.tag.find(".client-name"); elm.attr("contenteditable", "true"); elm.removeClass("client-name-own"); elm.css("background-color", "white"); elm.focus(); this.renaming = true; elm.on('keypress', event => { if(event.keyCode == KeyCode.KEY_RETURN) { $(event.target).trigger("focusout"); return false; } }); elm.on('focusout', event => { this.channelTree.client_mover.enabled = true; if(!this.renaming) return; this.renaming = false; elm.css("background-color", ""); elm.removeAttr("contenteditable"); elm.addClass("client-name-own"); let text = elm.text().toString(); if(this.clientNickName() == text) return; elm.text(this.clientNickName()); const old_name = this.clientNickName(); this.handle.serverConnection.command_helper.updateClient("client_nickname", text).then((e) => { settings.changeGlobal(Settings.KEY_CONNECT_USERNAME, text); this.channelTree.client.log.log(log.server.Type.CLIENT_NICKNAME_CHANGED, { client: this.log_data(), old_name: old_name, new_name: text, own_client: true }); }).catch((e: CommandResult) => { this.channelTree.client.log.log(log.server.Type.CLIENT_NICKNAME_CHANGE_FAILED, { reason: e.extra_message }); this.openRename(); }); }); } } class MusicClientProperties extends ClientProperties { player_state: number = 0; player_volume: number = 0; client_playlist_id: number = 0; client_disabled: boolean = false; } class MusicClientPlayerInfo { bot_id: number = 0; player_state: number = 0; player_buffered_index: number = 0; player_replay_index: number = 0; player_max_index: number = 0; player_seekable: boolean = false; player_title: string = ""; player_description: string = ""; song_id: number = 0; song_url: string = ""; song_invoker: number = 0; song_loaded: boolean = false; song_title: string = ""; song_thumbnail: string = ""; song_length: number = 0; } class MusicClientEntry extends ClientEntry { private _info_promise: Promise; private _info_promise_age: number = 0; private _info_promise_resolve: any; private _info_promise_reject: any; constructor(clientId, clientName) { super(clientId, clientName, new MusicClientProperties()); } destroy() { super.destroy(); this._info_promise = undefined; this._info_promise_reject = undefined; this._info_promise_resolve = undefined; } get properties() : MusicClientProperties { return this._properties as MusicClientProperties; } showContextMenu(x: number, y: number, on_close: () => void = undefined): void { let trigger_close = true; contextmenu.spawn_context_menu(x, y, ...this.contextmenu_info(), { name: (contextmenu.get_provider().html_format_enabled() ? "" : "") + tr("Change bot name") + (contextmenu.get_provider().html_format_enabled() ? "" : ""), icon_class: "client-change_nickname", disabled: false, callback: () => { createInputModal(tr("Change music bots nickname"), tr("New nickname:
"), text => text.length >= 3 && text.length <= 31, result => { if(result) { this.channelTree.client.serverConnection.send_command("clientedit", { clid: this.clientId(), client_nickname: result }); } }, { width: "40em", min_width: "10em", maxLength: 255 }).open(); }, type: contextmenu.MenuEntryType.ENTRY }, { name: tr("Change bot description"), icon_class: "client-edit", disabled: false, callback: () => { createInputModal(tr("Change music bots description"), tr("New description:
"), text => true, result => { if(typeof(result) === 'string') { this.channelTree.client.serverConnection.send_command("clientedit", { clid: this.clientId(), client_description: result }); } }, { width: "60em", min_width: "10em", maxLength: 255 }).open(); }, type: contextmenu.MenuEntryType.ENTRY }, /* { name: tr("Open music panel"), icon: "client-edit", disabled: true, callback: () => {}, type: MenuEntryType.ENTRY }, */ { name: tr("Open bot's playlist"), icon_class: "client-edit", disabled: false, callback: () => { this.channelTree.client.serverConnection.command_helper.request_playlist_list().then(lists => { for(const entry of lists) { if(entry.playlist_id == this.properties.client_playlist_id) { Modals.spawnPlaylistEdit(this.channelTree.client, entry); return; } } createErrorModal(tr("Invalid permissions"), tr("You dont have to see the bots playlist.")).open(); }).catch(error => { createErrorModal(tr("Failed to query playlist."), tr("Failed to query playlist info.")).open(); }); }, type: contextmenu.MenuEntryType.ENTRY }, { name: tr("Quick url replay"), icon_class: "client-edit", disabled: false, callback: () => { createInputModal(tr("Please enter the URL"), tr("URL:"), text => true, result => { if(result) { this.channelTree.client.serverConnection.send_command("musicbotqueueadd", { bot_id: this.properties.client_database_id, type: "yt", //Its a hint not a force! url: result }).catch(error => { if(error instanceof CommandResult) { error = error.extra_message || error.message; } //TODO tr createErrorModal(tr("Failed to replay url"), "Failed to enqueue url:
" + error).open(); }); } }, { width: 400, maxLength: 255 }).open(); }, type: contextmenu.MenuEntryType.ENTRY }, contextmenu.Entry.HR(), ...super.assignment_context(), contextmenu.Entry.HR(),{ type: contextmenu.MenuEntryType.ENTRY, icon_class: "client-move_client_to_own_channel", name: tr("Move client to your channel"), callback: () => { this.channelTree.client.serverConnection.send_command("clientmove", { clid: this.clientId(), cid: this.channelTree.client.getClient().currentChannel().getChannelId() }); } }, { type: contextmenu.MenuEntryType.ENTRY, icon_class: "client-kick_channel", name: tr("Kick client from channel"), callback: () => { createInputModal(tr("Kick client from channel"), tr("Kick reason:
"), text => true, result => { if(typeof(result) !== 'boolean' || result) { console.log(tr("Kicking client %o from channel with reason %o"), this.clientNickName(), result); this.channelTree.client.serverConnection.send_command("clientkick", { clid: this.clientId(), reasonid: ViewReasonId.VREASON_CHANNEL_KICK, reasonmsg: result }); } }, { width: 400, maxLength: 255 }).open(); } }, contextmenu.Entry.HR(), { type: contextmenu.MenuEntryType.ENTRY, icon_class: "client-volume", name: tr("Change local volume"), callback: () => { Modals.spawnChangeVolume(this, true, this._audio_handle.get_volume(), undefined, volume => { this.channelTree.client.settings.changeServer("volume_client_" + this.clientUid(), volume); this._audio_handle.set_volume(volume); }); } }, { type: contextmenu.MenuEntryType.ENTRY, icon_class: "client-volume", name: tr("Change remote volume"), callback: () => { let max_volume = this.channelTree.client.permissions.neededPermission(PermissionType.I_CLIENT_MUSIC_CREATE_MODIFY_MAX_VOLUME).value; if(max_volume < 0) max_volume = 100; Modals.spawnChangeVolume(this, false, this.properties.player_volume, max_volume / 100, value => { if(typeof(value) !== "number") return; this.channelTree.client.serverConnection.send_command("clientedit", { clid: this.clientId(), player_volume: value, }).then(() => { //TODO: Update in info }); }); } }, { type: contextmenu.MenuEntryType.ENTRY, name: tr("Change playback latency"), callback: () => { Modals.spawnChangeLatency(this, this._audio_handle.latency_settings(), () => { this._audio_handle.reset_latency_settings(); return this._audio_handle.latency_settings(); }, settings => this._audio_handle.latency_settings(settings)); }, visible: this._audio_handle && this._audio_handle.support_latency_settings() }, contextmenu.Entry.HR(), { name: tr("Delete bot"), icon_class: "client-delete", disabled: false, callback: () => { const tag = $.spawn("div").append(MessageHelper.formatMessage(tr("Do you really want to delete {0}"), this.createChatTag(false))); Modals.spawnYesNo(tr("Are you sure?"), $.spawn("div").append(tag), result => { if(result) { this.channelTree.client.serverConnection.send_command("musicbotdelete", { bot_id: this.properties.client_database_id }); } }); }, type: contextmenu.MenuEntryType.ENTRY }, contextmenu.Entry.CLOSE(() => trigger_close && on_close()) ); } initializeListener(): void { super.initializeListener(); } handlePlayerInfo(json) { if(json) { const info = new MusicClientPlayerInfo(); JSON.map_to(info, json); if(this._info_promise_resolve) this._info_promise_resolve(info); this._info_promise_reject = undefined; } if(this._info_promise) { if(this._info_promise_reject) this._info_promise_reject("timeout"); this._info_promise = undefined; this._info_promise_age = undefined; this._info_promise_reject = undefined; this._info_promise_resolve = undefined; } } requestPlayerInfo(max_age: number = 1000) : Promise { if(this._info_promise && this._info_promise_age && Date.now() - max_age <= this._info_promise_age) return this._info_promise; this._info_promise_age = Date.now(); this._info_promise = new Promise((resolve, reject) => { this._info_promise_reject = reject; this._info_promise_resolve = resolve; }); this.channelTree.client.serverConnection.send_command("musicbotplayerinfo", {bot_id: this.properties.client_database_id }); return this._info_promise; } }