import {tra} from "tc-shared/i18n/localize"; import {PermissionInfo} from "tc-shared/permission/PermissionManager"; import {ConnectionHandler, ViewReasonId} from "tc-shared/ConnectionHandler"; import * as htmltags from "tc-shared/ui/htmltags"; import {bbcode_chat, format_time, formatMessage} from "tc-shared/ui/frames/chat"; import {formatDate} from "tc-shared/MessageFormatter"; 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", CONNECTION_VOICE_SETUP_FAILED = "connection_voice_setup_failed", CONNECTION_COMMAND_ERROR = "connection_command_error", 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_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_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", CHANNEL_CREATE = "channel_create", CHANNEL_DELETE = "channel_delete", 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; } export type ReconnectScheduled = { timeout: 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: 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; } this.auto_follow = (this._html_tag[0].scrollTop + this._html_tag[0].clientHeight + this._html_tag[0].clientHeight * .125) > this._html_tag[0].scrollHeight; }); } log(type: T, data: 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: 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); } /* build message data */ { const builder = MessageBuilders[message.type]; if(!builder) { 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); /* 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()); 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 */ 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: event.ConnectBegin, options) => { return 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) => formatMessage(tr("Resolving hostname")); MessageBuilders["connection_hostname_resolved"] = (data: event.ConnectionHostnameResolved, options) => 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) => formatMessage(tr("Failed to resolve hostname. Connecting to given hostname. Error: {0}"), data.message); MessageBuilders["connection_login"] = (data: event.ConnectionLogin, options) => formatMessage(tr("Logging in...")); MessageBuilders["connection_failed"] = (data: event.ConnectionFailed, options) => formatMessage(tr("Connect failed.")); MessageBuilders["connection_connected"] = (data: event.ConnectionConnected, options) => formatMessage(tr("Connected as {0}"), client_tag(data.own_client, true)); MessageBuilders["connection_voice_setup_failed"] = (data: event.ConnectionVoiceSetupFailed, options) => { return 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 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 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 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 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 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 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 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 + ")") : "" ); } } 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 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 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 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 + ")")]; }; MessageBuilders["client_view_leave"] = (data: event.ClientLeave, options) => { if(data.reason == ViewReasonId.VREASON_USER_ACTION) { return 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 formatMessage(tr("{0} left the server{1}"), client_tag(data.client), data.message ? (" (" + data.message + ")") : ""); } else if(data.reason == ViewReasonId.VREASON_SERVER_KICK) { return 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 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 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 formatMessage(tr("{0} timed out{1}"), client_tag(data.client), data.message ? (" (" + data.message + ")") : ""); } else if(data.reason == ViewReasonId.VREASON_MOVED) { return 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 leave reason id (" + data.reason + ")")]; }; MessageBuilders["server_welcome_message"] = (data: event.WelcomeMessage, options) => { return bbcode_chat("[color=green]" + data.message + "[/color]"); }; MessageBuilders["server_host_message"] = (data: event.WelcomeMessage, options) => { return bbcode_chat("[color=green]" + data.message + "[/color]"); }; MessageBuilders["client_nickname_changed"] = (data: event.ClientNicknameChanged, options) => { if(data.own_client) { return formatMessage(tr("Nickname successfully changed.")); } else { return formatMessage(tr("{0} changed his nickname from \"{1}\" to \"{2}\""), client_tag(data.client), data.old_name, data.new_name); } }; MessageBuilders["global_message"] = (data: event.GlobalMessage, options) => { return []; /* we do not show global messages within log */ }; MessageBuilders["disconnected"] = () => formatMessage(tr("Disconnected from server")); MessageBuilders["reconnect_scheduled"] = (data: event.ReconnectScheduled, options) => { return tra("Reconnecting in {0}.", format_time(data.timeout, tr("now"))) }; MessageBuilders["reconnect_canceled"] = (data: event.ReconnectCanceled, options) => { return tra("Canceled reconnect.") }; MessageBuilders["reconnect_execute"] = (data: event.ReconnectExecute, options) => { return tra("Reconnecting...") }; MessageBuilders["server_banned"] = (data: event.ServerBanned, options) => { let result: JQuery[]; const time = data.time == 0 ? tr("ever") : 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")); };