diff --git a/asm/make_opus.sh b/asm/make_opus.sh new file mode 100755 index 00000000..b3efd4bf --- /dev/null +++ b/asm/make_opus.sh @@ -0,0 +1,11 @@ +#!/usr/bin/env bash + +base_dir=`pwd` +cd "$(dirname $0)/libraries/opus/" + +git checkout v1.1.2 +./autogen.sh +emconfigure ./configure --disable-extra-programs --disable-doc --disable-rtcd +emmake make + +cd ${base_dir} \ No newline at end of file diff --git a/documentation/file-structure.md b/documentation/file-structure.md new file mode 100644 index 00000000..aeecbcaf --- /dev/null +++ b/documentation/file-structure.md @@ -0,0 +1,42 @@ +# File structure +The TeaSpeak web client is separated into 2 different parts. + +## I) Application files +Application files are all files which directly belong to the app itself. +Like the javascript files who handle the UI stuff or even translation templates. +Theses files are separated into two type of files. +1. [Shared application files](#1-shared-application-files) +2. [Web application files](#2-web-application-files) + +### 1. Shared application files +Containing all files used by the TeaSpeak client and the Web client. +All of these files will be found within the folder `shared`. +This folder follows the general application file structure. +More information could be found [here](#application-file-structure) + +### 2. Web application files +All files which only belong to a browser only instance. +All of these files will be found within the folder `web`. +This folder follows the general application file structure. +More information could be found [here](#application-file-structure) + +### application file structure +Every application root contains several subfolders. +In the following list will be listed which files belong to which folder + +| Folder | Description | +| --- | --- | +| `audio` | This folder contains all audio files used by the application. More information could be found [here](). | +| `css` | This folder contains all style sheets used by the application. More information could be found [here](). | +| `js` | This folder contains all javascript files used by the application. More information could be found [here](). | +| `html` | This folder contains all HTML and PHP files used by the application. More information could be found [here](). | +| `i18n` | This folder contains all default translations. Information about the translation system could be found [here](). | +| `img` | This folder contains all image files. | + +## I) Additional tools + +## Environment builder +The environment builder is one of the most important tools of the entire project. +This tool, basically implemented in the file `files.php`, will be your helper while live developing. +What this tool does is, it creates a final environment where you could navigate to with your browser. +It merges all the type separated files, which had been listed above ([here](#application-file-structure)). \ No newline at end of file diff --git a/shared/js/stats.ts b/shared/js/stats.ts new file mode 100644 index 00000000..49c52e80 --- /dev/null +++ b/shared/js/stats.ts @@ -0,0 +1,232 @@ +namespace stats { + const LOG_PREFIX = "[Statistics] "; + + export enum CloseCodes { + UNSET = 3000, + RECONNECT = 3001, + INTERNAL_ERROR = 3002, + + BANNED = 3100, + } + + enum ConnectionState { + CONNECTING, + INITIALIZING, + CONNECTED, + UNSET + } + + export class SessionConfig { + /* + * All collected statistics will only be cached by the stats server. + * No data will be saved. + */ + volatile_collection_only?: boolean; + + /* + * Anonymize all IP addresses which will be provided while the stats collection. + * This option is quite useless when volatile_collection_only is active. + */ + anonymize_ip_addresses?: boolean; + } + + export class Config extends SessionConfig { + verbose?: boolean; + + reconnect_interval?: number; + } + + export interface UserCountData { + online_users: number; + unique_online_users: number; + } + + export type UserCountListener = (data: UserCountData) => any; + + let reconnect_timer: NodeJS.Timer; + let current_config: Config; + + let last_user_count_update: number; + let user_count_listener: UserCountListener[] = []; + + const DEFAULT_CONFIG: Config = { + verbose: true, + reconnect_interval: 5000, + anonymize_ip_addresses: true, + volatile_collection_only: false + }; + + function initialize_config_object(target_object: any, source_object: any) : any { + for(const key of Object.keys(source_object)) { + if(typeof(source_object[key]) === 'object') + initialize_config_object(target_object[key] || (target_object[key] = {}), source_object[key]); + + if(typeof(target_object[key]) !== 'undefined') + continue; + + target_object[key] = source_object[key]; + } + + return target_object; + } + + export function initialize(config: Config) { + current_config = initialize_config_object(config || {}, DEFAULT_CONFIG); + if(current_config.verbose) + console.log(LOG_PREFIX + tr("Initializing statistics with this config: %o"), current_config); + + connection.start_connection(); + } + + export function register_user_count_listener(listener: UserCountListener) { + user_count_listener.push(listener); + } + + export function all_user_count_listener() : UserCountListener[] { + return user_count_listener; + } + + export function deregister_user_count_listener(listener: UserCountListener) { + user_count_listener.remove(listener); + } + + namespace connection { + let connection: WebSocket; + export let connection_state: ConnectionState = ConnectionState.UNSET; + + export function start_connection() { + cancel_reconnect(); + + if(connection) { + const connection_copy = connection; + connection = undefined; + + connection_copy.close(3001); + } + + connection_state = ConnectionState.CONNECTING; + connection = new WebSocket('wss://web-stats.teaspeak.de:1774'); + { + const connection_copy = connection; + connection.onclose = (event: CloseEvent) => { + if(connection_copy !== connection) return; + + if(current_config.verbose) + console.log(LOG_PREFIX + tr("Lost connection to statistics server (Connection closed). Reason: %o. Event object: %o"), CloseCodes[event.code] || event.code, event); + + if(event.code != CloseCodes.BANNED) + invoke_reconnect(); + }; + + connection.onopen = () => { + if(connection_copy !== connection) return; + + if(current_config.verbose) + console.log(LOG_PREFIX + tr("Successfully connected to server. Initializing session.")); + + connection_state = ConnectionState.INITIALIZING; + initialize_session(); + }; + + connection.onerror = (event: ErrorEvent) => { + if(connection_copy !== connection) return; + + if(current_config.verbose) + console.log(LOG_PREFIX + tr("Received an error. Closing connection. Object: %o"), event); + + connection.close(CloseCodes.INTERNAL_ERROR); + this.invoke_reconnect(); + }; + + connection.onmessage = (event: MessageEvent) => { + if(connection_copy !== connection) return; + + if(typeof(event.data) !== 'string') { + if(current_config.verbose) + console.warn(LOG_PREFIX + tr("Received an message which isn't a string. Event object: %o"), event); + return; + } + + handle_message(event.data as string); + }; + } + } + + function invoke_reconnect() { + if(reconnect_timer) { + clearTimeout(reconnect_timer); + reconnect_timer = undefined; + } + + if(current_config.verbose) + console.log(LOG_PREFIX + tr("Scheduled reconnect in %dms"), current_config.reconnect_interval); + + reconnect_timer = setTimeout(() => { + if(current_config.verbose) + console.log(LOG_PREFIX + tr("Reconnecting")); + start_connection(); + }, current_config.reconnect_interval); + } + + export function cancel_reconnect() { + if(reconnect_timer) { + clearTimeout(reconnect_timer); + reconnect_timer = undefined; + } + } + + function send_message(type: string, data: any) { + connection.send(JSON.stringify({ + type: type, + data: data + })); + } + + function initialize_session() { + const config_object = {}; + for(const key in SessionConfig) { + if(SessionConfig.hasOwnProperty(key)) + config_object[key] = current_config[key]; + } + + send_message('initialize', { + config: config_object + }) + } + + function handle_message(message: string) { + const data_object = JSON.parse(message); + const type = data_object.type as string; + const data = data_object.data; + + if(typeof(handler[type]) === 'function') { + if(current_config.verbose) + console.debug(LOG_PREFIX + tr("Handling message of type %s"), type); + handler[type](data); + } else if(current_config.verbose) { + console.warn(LOG_PREFIX + tr("Received message with an unknown type (%s). Dropping message. Full message: %o"), type, data_object); + } + } + + namespace handler { + interface NotifyUserCount extends UserCountData { } + + function handle_notify_user_count(data: NotifyUserCount) { + last_user_count_update = Date.now(); + for(const listener of [...user_count_listener]) + listener(data); + } + + interface NotifyInitialized {} + function handle_notify_initialized(json: NotifyInitialized) { + if(current_config.verbose) + console.log(LOG_PREFIX + tr("Session successfully initialized.")); + + connection_state = ConnectionState.CONNECTED; + } + + handler["notifyinitialized"] = handle_notify_initialized; + handler["notifyusercount"] = handle_notify_user_count; + } + } +} \ No newline at end of file