Refactored the app to use webpack

canary
WolverinDEV 2020-03-30 13:44:18 +02:00
parent 447e84ed0f
commit 0e3a415983
153 changed files with 34926 additions and 34209 deletions

169
file.ts
View File

@ -444,6 +444,7 @@ const CLIENT_APP_FILE_LIST = [
...APP_FILE_LIST_CLIENT_SOURCE
];
/*
const WEB_APP_FILE_LIST = [
...APP_FILE_LIST_SHARED_SOURCE,
...APP_FILE_LIST_SHARED_VENDORS,
@ -451,6 +452,174 @@ const WEB_APP_FILE_LIST = [
...APP_FILE_LIST_WEB_TEASPEAK,
...CERTACCEPT_FILE_LIST,
];
*/
const WEB_APP_FILE_LIST = [
...APP_FILE_LIST_SHARED_VENDORS,
{ /* shared html and php files */
"type": "html",
"search-pattern": /^.*([a-zA-Z]+)\.(html|php|json)$/,
"build-target": "dev|rel",
"path": "./",
"local-path": "./shared/html/"
},
{ /* javascript loader for releases */
"type": "js",
"search-pattern": /.*$/,
"build-target": "dev|rel",
"path": "js/",
"local-path": "./dist/"
},
{ /* shared javascript files (WebRTC adapter) */
"type": "js",
"search-pattern": /.*\.js$/,
"build-target": "dev|rel",
"path": "adapter/",
"local-path": "./shared/adapter/"
},
{ /* shared generated worker codec */
"type": "js",
"search-pattern": /(WorkerPOW.js)$/,
"build-target": "dev|rel",
"path": "js/workers/",
"local-path": "./shared/js/workers/"
},
{ /* shared developer single css files */
"type": "css",
"search-pattern": /.*\.css$/,
"build-target": "dev",
"path": "css/",
"local-path": "./shared/css/"
},
{ /* shared css mapping files (development mode only) */
"type": "css",
"search-pattern": /.*\.(css.map|scss)$/,
"build-target": "dev",
"path": "css/",
"local-path": "./shared/css/",
"req-parm": ["--mappings"]
},
{ /* shared release css files */
"type": "css",
"search-pattern": /.*\.css$/,
"build-target": "rel",
"path": "css/",
"local-path": "./shared/generated/"
},
{ /* shared release css files */
"type": "css",
"search-pattern": /.*\.css$/,
"build-target": "rel",
"path": "css/loader/",
"local-path": "./shared/css/loader/"
},
{ /* shared release css files */
"type": "css",
"search-pattern": /.*\.css$/,
"build-target": "dev|rel",
"path": "css/theme/",
"local-path": "./shared/css/theme/"
},
{ /* shared sound files */
"type": "wav",
"search-pattern": /.*\.wav$/,
"build-target": "dev|rel",
"path": "audio/",
"local-path": "./shared/audio/"
},
{ /* shared data sound files */
"type": "json",
"search-pattern": /.*\.json/,
"build-target": "dev|rel",
"path": "audio/",
"local-path": "./shared/audio/"
},
{ /* shared image files */
"type": "img",
"search-pattern": /.*\.(svg|png)/,
"build-target": "dev|rel",
"path": "img/",
"local-path": "./shared/img/"
},
{ /* own webassembly files */
"type": "wasm",
"search-pattern": /.*\.(wasm)/,
"build-target": "dev|rel",
"path": "wat/",
"local-path": "./shared/wat/"
},
/* web specific */
{ /* generated assembly files */
"web-only": true,
"type": "wasm",
"search-pattern": /.*\.(wasm)/,
"build-target": "dev|rel",
"path": "wasm/",
"local-path": "./asm/generated/"
},
{ /* generated assembly javascript files */
"web-only": true,
"type": "js",
"search-pattern": /.*\.(js)/,
"build-target": "dev|rel",
"path": "wasm/",
"local-path": "./asm/generated/"
},
{ /* web generated worker codec */
"web-only": true,
"type": "js",
"search-pattern": /(WorkerCodec.js)$/,
"build-target": "dev|rel",
"path": "js/workers/",
"local-path": "./web/js/workers/"
},
{ /* web css files */
"web-only": true,
"type": "css",
"search-pattern": /.*\.css$/,
"build-target": "dev|rel",
"path": "css/",
"local-path": "./web/css/"
},
{ /* web html files */
"web-only": true,
"type": "html",
"search-pattern": /.*\.(php|html)/,
"build-target": "dev|rel",
"path": "./",
"local-path": "./web/html/"
},
{ /* translations */
"web-only": true, /* Only required for the web client */
"type": "i18n",
"search-pattern": /.*\.(translation|json)/,
"build-target": "dev|rel",
"path": "i18n/",
"local-path": "./shared/i18n/"
}
] as any;
//@ts-ignore
declare module "fs-extra" {

View File

@ -1,41 +1,39 @@
/// <reference path="loader.ts" />
import * as loader from "./loader";
interface Window {
$: JQuery;
declare global {
interface Window {
native_client: boolean;
}
}
namespace app {
export enum Type {
UNKNOWN,
CLIENT_RELEASE,
CLIENT_DEBUG,
WEB_DEBUG,
WEB_RELEASE
}
export let type: Type = Type.UNKNOWN;
export function is_web() {
return type == Type.WEB_RELEASE || type == Type.WEB_DEBUG;
}
let _ui_version;
export function ui_version() {
if(typeof(_ui_version) !== "string") {
const version_node = document.getElementById("app_version");
if(!version_node) return undefined;
const version = version_node.hasAttribute("value") ? version_node.getAttribute("value") : undefined;
if(!version) return undefined;
return (_ui_version = version);
}
return _ui_version;
const node_require: typeof require = window.require;
let _ui_version;
export function ui_version() {
if(typeof(_ui_version) !== "string") {
const version_node = document.getElementById("app_version");
if(!version_node) return undefined;
const version = version_node.hasAttribute("value") ? version_node.getAttribute("value") : undefined;
if(!version) return undefined;
return (_ui_version = version);
}
return _ui_version;
}
/* all javascript loaders */
const loader_javascript = {
detect_type: async () => {
//TODO: Detect real version!
loader.set_version({
backend: "-",
ui: ui_version(),
debug_mode: true,
type: "web"
});
window.native_client = false;
return;
if(window.require) {
const request = new Request("js/proto.js");
let file_path = request.url;
@ -43,11 +41,11 @@ const loader_javascript = {
throw "Invalid file path (" + file_path + ")";
file_path = file_path.substring(process.platform === "win32" ? 8 : 7);
const fs = require('fs');
const fs = node_require('fs');
if(fs.existsSync(file_path)) {
app.type = app.Type.CLIENT_DEBUG;
//type = Type.CLIENT_DEBUG;
} else {
app.type = app.Type.CLIENT_RELEASE;
//type = Type.CLIENT_RELEASE;
}
} else {
/* test if js/proto.js is available. If so we're in debug mode */
@ -58,9 +56,9 @@ const loader_javascript = {
request.onreadystatechange = () => {
if (request.readyState === 4){
if (request.status === 404) {
app.type = app.Type.WEB_RELEASE;
//type = Type.WEB_RELEASE;
} else {
app.type = app.Type.WEB_DEBUG;
//type = Type.WEB_DEBUG;
}
resolve();
}
@ -101,7 +99,7 @@ const loader_javascript = {
["vendor/emoji-picker/src/jquery.lsxemojipicker.js"]
]);
if(app.type == app.Type.WEB_RELEASE || app.type == app.Type.CLIENT_RELEASE) {
if(!loader.version().debug_mode) {
loader.register_task(loader.Stage.JAVASCRIPT, {
name: "scripts release",
priority: 20,
@ -116,176 +114,8 @@ const loader_javascript = {
}
},
load_scripts_debug: async () => {
/* test if we're loading as TeaClient or WebClient */
if(!window.require) {
loader.register_task(loader.Stage.JAVASCRIPT, {
name: "javascript web",
priority: 10,
function: loader_javascript.load_scripts_debug_web
});
} else {
loader.register_task(loader.Stage.JAVASCRIPT, {
name: "javascript client",
priority: 10,
function: loader_javascript.load_scripts_debug_client
});
}
/* load some extends classes */
await loader.load_scripts([
["js/connection/ConnectionBase.js"]
]);
/* load the main app */
await loader.load_scripts([
//Load general API's
"js/proto.js",
"js/i18n/localize.js",
"js/i18n/country.js",
"js/log.js",
"js/events.js",
"js/sound/Sounds.js",
"js/utils/helpers.js",
"js/crypto/sha.js",
"js/crypto/hex.js",
"js/crypto/asn1.js",
"js/crypto/crc32.js",
//load the profiles
"js/profiles/ConnectionProfile.js",
"js/profiles/Identity.js",
"js/profiles/identities/teaspeak-forum.js",
//Basic UI elements
"js/ui/elements/context_divider.js",
"js/ui/elements/context_menu.js",
"js/ui/elements/modal.js",
"js/ui/elements/tab.js",
"js/ui/elements/slider.js",
"js/ui/elements/tooltip.js",
"js/ui/elements/net_graph.js",
//Load permissions
"js/permission/PermissionManager.js",
"js/permission/GroupManager.js",
//Load UI
"js/ui/modal/ModalAbout.js",
"js/ui/modal/ModalAvatar.js",
"js/ui/modal/ModalAvatarList.js",
"js/ui/modal/ModalClientInfo.js",
"js/ui/modal/ModalChannelInfo.js",
"js/ui/modal/ModalServerInfo.js",
"js/ui/modal/ModalServerInfoBandwidth.js",
"js/ui/modal/ModalQuery.js",
"js/ui/modal/ModalQueryManage.js",
"js/ui/modal/ModalPlaylistList.js",
"js/ui/modal/ModalPlaylistEdit.js",
"js/ui/modal/ModalBookmarks.js",
"js/ui/modal/ModalConnect.js",
"js/ui/modal/ModalSettings.js",
"js/ui/modal/ModalNewcomer.js",
"js/ui/modal/ModalCreateChannel.js",
"js/ui/modal/ModalServerEdit.js",
"js/ui/modal/ModalChangeVolume.js",
"js/ui/modal/ModalChangeLatency.js",
"js/ui/modal/ModalBanClient.js",
"js/ui/modal/ModalIconSelect.js",
"js/ui/modal/ModalInvite.js",
"js/ui/modal/ModalIdentity.js",
"js/ui/modal/ModalBanList.js",
"js/ui/modal/ModalMusicManage.js",
"js/ui/modal/ModalYesNo.js",
"js/ui/modal/ModalPoke.js",
"js/ui/modal/ModalKeySelect.js",
"js/ui/modal/ModalGroupAssignment.js",
"js/ui/modal/permission/ModalPermissionEdit.js",
{url: "js/ui/modal/permission/SenselessPermissions.js", depends: ["js/permission/PermissionManager.js"]},
{url: "js/ui/modal/permission/CanvasPermissionEditor.js", depends: ["js/ui/modal/permission/ModalPermissionEdit.js"]},
{url: "js/ui/modal/permission/HTMLPermissionEditor.js", depends: ["js/ui/modal/permission/ModalPermissionEdit.js"]},
"js/channel-tree/channel.js",
"js/channel-tree/client.js",
"js/channel-tree/server.js",
"js/channel-tree/view.js",
"js/ui/client_move.js",
"js/ui/htmltags.js",
"js/ui/frames/side/chat_helper.js",
"js/ui/frames/side/chat_box.js",
"js/ui/frames/side/client_info.js",
"js/ui/frames/side/music_info.js",
"js/ui/frames/side/conversations.js",
"js/ui/frames/side/private_conversations.js",
"js/ui/frames/ControlBar.js",
"js/ui/frames/chat.js",
"js/ui/frames/chat_frame.js",
"js/ui/frames/connection_handlers.js",
"js/ui/frames/server_log.js",
"js/ui/frames/hostbanner.js",
"js/ui/frames/MenuBar.js",
"js/ui/frames/image_preview.js",
//Load audio
"js/voice/RecorderBase.js",
"js/voice/RecorderProfile.js",
//Load general stuff
"js/settings.js",
"js/bookmarks.js",
"js/FileManager.js",
"js/ConnectionHandler.js",
"js/BrowserIPC.js",
"js/MessageFormatter.js",
//Connection
"js/connection/CommandHandler.js",
"js/connection/CommandHelper.js",
"js/connection/HandshakeHandler.js",
"js/connection/ServerConnectionDeclaration.js",
"js/stats.js",
"js/PPTListener.js",
"js/profiles/identities/NameIdentity.js", //Depends on Identity
"js/profiles/identities/TeaForumIdentity.js", //Depends on Identity
"js/profiles/identities/TeamSpeakIdentity.js", //Depends on Identity
]);
await loader.load_script("js/main.js");
await loader.load_scripts(["js/shared-app.js"])
},
load_scripts_debug_web: async () => {
await loader.load_scripts([
"js/audio/AudioPlayer.js",
"js/audio/sounds.js",
"js/audio/WebCodec.js",
"js/WebPPTListener.js",
"js/voice/AudioResampler.js",
"js/voice/JavascriptRecorder.js",
"js/voice/VoiceHandler.js",
"js/voice/VoiceClient.js",
//Connection
"js/connection/ServerConnection.js",
"js/dns_impl.js",
//Load codec
"js/codec/Codec.js",
"js/codec/BasicCodec.js",
{url: "js/codec/CodecWrapperWorker.js", depends: ["js/codec/BasicCodec.js"]},
]);
},
load_scripts_debug_client: async () => {
await loader.load_scripts([
]);
},
load_release: async () => {
console.log("Load for release!");
@ -329,7 +159,7 @@ const loader_style = {
["vendor/highlight/styles/darcula.css", ""], /* empty string means not required */
]);
if(app.type == app.Type.WEB_DEBUG || app.type == app.Type.CLIENT_DEBUG) {
if(loader.version().debug_mode) {
await loader_style.load_style_debug();
} else {
await loader_style.load_style_release();
@ -494,23 +324,10 @@ loader.register_task(loader.Stage.TEMPLATES, {
loader.register_task(loader.Stage.LOADED, {
name: "loaded handler",
function: async () => {
fadeoutLoader();
},
function: async () => loader.hide_overlay(),
priority: 5
});
loader.register_task(loader.Stage.LOADED, {
name: "error task",
function: async () => {
if(Settings.instance.static(Settings.KEY_LOAD_DUMMY_ERROR, false)) {
loader.critical_error("The tea is cold!", "Argh, this is evil! Cold tea dosn't taste good.");
throw "The tea is cold!";
}
},
priority: 20
});
loader.register_task(loader.Stage.JAVASCRIPT_INITIALIZING, {
name: "lsx emoji picker setup",
function: async () => await (window as any).setup_lsx_emoji_picker({twemoji: typeof(window.twemoji) !== "undefined"}),
@ -576,31 +393,26 @@ loader.register_task(loader.Stage.SETUP, {
priority: 100
});
loader.register_task(loader.Stage.JAVASCRIPT_INITIALIZING, {
name: "log enabled initialisation",
function: async () => log.initialize(app.type === app.Type.CLIENT_DEBUG || app.type === app.Type.WEB_DEBUG ? log.LogType.TRACE : log.LogType.INFO),
priority: 150
});
export function run() {
window["Module"] = (window["Module"] || {}) as any;
/* TeaClient */
if(node_require) {
const path = node_require("path");
const remote = node_require('electron').remote;
module.paths.push(path.join(remote.app.getAppPath(), "/modules"));
module.paths.push(path.join(path.dirname(remote.getGlobal("browser-root")), "js"));
window["Module"] = (window["Module"] || {}) as any;
/* TeaClient */
if(window.require) {
const path = require("path");
const remote = require('electron').remote;
module.paths.push(path.join(remote.app.getAppPath(), "/modules"));
module.paths.push(path.join(path.dirname(remote.getGlobal("browser-root")), "js"));
//TODO: HERE!
const connector = node_require("renderer");
loader.register_task(loader.Stage.INITIALIZING, {
name: "teaclient initialize",
function: connector.initialize,
priority: 40
});
}
const connector = require("renderer");
console.log(connector);
loader.register_task(loader.Stage.INITIALIZING, {
name: "teaclient initialize",
function: connector.initialize,
priority: 40
});
}
if(!loader.running()) {
/* we know that we want to load the app */
loader.execute_managed();
if(!loader.running()) {
/* we know that we want to load the app */
loader.execute_managed();
}
}

View File

@ -1,4 +1,4 @@
/// <reference path="loader.ts" />
import * as loader from "./loader";
let is_debug = false;
@ -25,34 +25,8 @@ const loader_javascript = {
load_scripts: async () => {
await loader.load_script(["vendor/jquery/jquery.min.js"]);
if(!is_debug) {
loader.register_task(loader.Stage.JAVASCRIPT, {
name: "scripts release",
priority: 20,
function: loader_javascript.load_release
});
} else {
loader.register_task(loader.Stage.JAVASCRIPT, {
name: "scripts debug",
priority: 20,
function: loader_javascript.load_scripts_debug
});
}
},
load_scripts_debug: async () => {
await loader.load_scripts([
["js/proto.js"],
["js/log.js"],
["js/BrowserIPC.js"],
["js/settings.js"],
["js/main.js"]
]);
},
load_release: async () => {
await loader.load_scripts([
["js/certaccept.min.js", "js/certaccept.js"]
["dist/certificate-popup.js"],
]);
}
};
@ -99,9 +73,7 @@ loader.register_task(loader.Stage.STYLE, {
loader.register_task(loader.Stage.LOADED, {
name: "loaded handler",
function: async () => {
fadeoutLoader();
},
function: async () => loader.hide_overlay(),
priority: 0
});
@ -123,25 +95,6 @@ loader.register_task(loader.Stage.INITIALIZING, {
priority: 50
});
loader.register_task(loader.Stage.JAVASCRIPT_INITIALIZING, {
name: "settings initialisation",
function: async () => Settings.initialize(),
priority: 200
});
loader.register_task(loader.Stage.JAVASCRIPT_INITIALIZING, {
name: "bipc initialisation",
function: async () => bipc.setup(),
priority: 100
});
loader.register_task(loader.Stage.JAVASCRIPT_INITIALIZING, {
name: "log enabled initialisation",
function: async () => log.initialize(is_debug ? log.LogType.TRACE : log.LogType.INFO),
priority: 150
});
if(!loader.running()) {
/* we know that we want to load the app */
loader.execute_managed();

5
loader/app/index.ts Normal file
View File

@ -0,0 +1,5 @@
import * as loader from "./app";
import * as loader_base from "./loader";
export = loader_base;
loader.run();

712
loader/app/loader.ts Normal file
View File

@ -0,0 +1,712 @@
import {AppVersion} from "../exports/loader";
import {type} from "os";
declare global {
interface Window {
tr(message: string) : string;
tra(message: string, ...args: any[]);
log: any;
StaticSettings: any;
}
}
export interface Config {
loader_groups: boolean;
verbose: boolean;
error: boolean;
}
export let config: Config = {
loader_groups: false,
verbose: false,
error: true
};
export type Task = {
name: string,
priority: number, /* tasks with the same priority will be executed in sync */
function: () => Promise<void>
};
export enum Stage {
/*
loading loader required files (incl this)
*/
INITIALIZING,
/*
setting up the loading process
*/
SETUP,
/*
loading all style sheet files
*/
STYLE,
/*
loading all javascript files
*/
JAVASCRIPT,
/*
loading all template files
*/
TEMPLATES,
/*
initializing static/global stuff
*/
JAVASCRIPT_INITIALIZING,
/*
finalizing load process
*/
FINALIZING,
/*
invoking main task
*/
LOADED,
DONE
}
let cache_tag: string | undefined;
let current_stage: Stage = undefined;
const tasks: {[key:number]:Task[]} = {};
/* test if all files shall be load from cache or fetch again */
function loader_cache_tag() {
const app_version = (() => {
const version_node = document.getElementById("app_version");
if(!version_node) return undefined;
const version = version_node.hasAttribute("value") ? version_node.getAttribute("value") : undefined;
if(!version) return undefined;
if(!version || version == "unknown" || version.replace(/0+/, "").length == 0)
return undefined;
return version;
})();
if(config.verbose) console.log("Found current app version: %o", app_version);
if(!app_version) {
/* TODO add warning */
cache_tag = "?_ts=" + Date.now();
return;
}
const cached_version = localStorage.getItem("cached_version");
if(!cached_version || cached_version != app_version) {
register_task(Stage.LOADED, {
priority: 0,
name: "cached version updater",
function: async () => {
localStorage.setItem("cached_version", app_version);
}
});
}
cache_tag = "?_version=" + app_version;
}
export function get_cache_version() { return cache_tag; }
export function finished() {
return current_stage == Stage.DONE;
}
export function running() { return typeof(current_stage) !== "undefined"; }
export function register_task(stage: Stage, task: Task) {
if(current_stage > stage) {
if(config.error)
console.warn("Register loading task, but it had already been finished. Executing task anyways!");
task.function().catch(error => {
if(config.error) {
console.error("Failed to execute delayed loader task!");
console.log(" - %s: %o", task.name, error);
}
critical_error(error);
});
return;
}
const task_array = tasks[stage] || [];
task_array.push(task);
tasks[stage] = task_array.sort((a, b) => a.priority - b.priority);
}
export async function execute() {
document.getElementById("loader-overlay").classList.add("started");
loader_cache_tag();
const load_begin = Date.now();
let begin: number = 0;
let end: number = Date.now();
while(current_stage <= Stage.LOADED || typeof(current_stage) === "undefined") {
let current_tasks: Task[] = [];
while((tasks[current_stage] || []).length > 0) {
if(current_tasks.length == 0 || current_tasks[0].priority == tasks[current_stage][0].priority) {
current_tasks.push(tasks[current_stage].pop());
} else break;
}
const errors: {
error: any,
task: Task
}[] = [];
const promises: Promise<void>[] = [];
for(const task of current_tasks) {
try {
if(config.verbose) console.debug("Executing loader %s (%d)", task.name, task.priority);
promises.push(task.function().catch(error => {
errors.push({
task: task,
error: error
});
return Promise.resolve();
}));
} catch(error) {
errors.push({
task: task,
error: error
});
}
}
if(promises.length > 0) {
await Promise.all([...promises]);
}
if(errors.length > 0) {
if(config.loader_groups) console.groupEnd();
console.error("Failed to execute loader. The following tasks failed (%d):", errors.length);
for(const error of errors)
console.error(" - %s: %o", error.task.name, error.error);
throw "failed to process step " + Stage[current_stage];
}
if(current_tasks.length == 0) {
if(typeof(current_stage) === "undefined") {
current_stage = -1;
if(config.verbose) console.debug("[loader] Booting app");
} else if(current_stage < Stage.INITIALIZING) {
if(config.loader_groups) console.groupEnd();
if(config.verbose) console.debug("[loader] Entering next state (%s). Last state took %dms", Stage[current_stage + 1], (end = Date.now()) - begin);
} else {
if(config.loader_groups) console.groupEnd();
if(config.verbose) console.debug("[loader] Finish invoke took %dms", (end = Date.now()) - begin);
}
begin = end;
current_stage += 1;
if(current_stage != Stage.DONE && config.loader_groups)
console.groupCollapsed("Executing loading stage %s", Stage[current_stage]);
}
}
/* cleanup */
{
_script_promises = {};
}
if(config.verbose) console.debug("[loader] finished loader. (Total time: %dms)", Date.now() - load_begin);
}
export function execute_managed() {
execute().then(() => {
if(config.verbose) {
let message;
if(typeof(window.tr) !== "undefined")
message = tr("App loaded successfully!");
else
message = "App loaded successfully!";
if(typeof(window.log) !== "undefined") {
/* We're having our log module */
window.log.info(window.log.LogCategory.GENERAL, message);
} else {
console.log(message);
}
}
}).catch(error => {
console.error("App loading failed: %o", error);
critical_error("Failed to execute loader", "Lookup the console for more detail");
});
}
export type DependSource = {
url: string;
depends: string[];
}
export type SourcePath = string | DependSource | string[];
function script_name(path: SourcePath, html: boolean) {
if(Array.isArray(path)) {
let buffer = "";
let _or = " or ";
for(let entry of path)
buffer += _or + script_name(entry, html);
return buffer.slice(_or.length);
} else if(typeof(path) === "string")
return html ? "<code>" + path + "</code>" : path;
else
return html ? "<code>" + path.url + "</code>" : path.url;
}
class SyntaxError {
source: any;
constructor(source: any) {
this.source = source;
}
}
let _script_promises: {[key: string]: Promise<void>} = {};
export async function load_script(path: SourcePath) : Promise<void> {
if(Array.isArray(path)) { //We have some fallback
return load_script(path[0]).catch(error => {
console.log(typeof error + " - " + (error instanceof SyntaxError));
if(error instanceof SyntaxError)
return Promise.reject(error);
if(path.length > 1)
return load_script(path.slice(1));
return Promise.reject(error);
});
} else {
const source = typeof(path) === "string" ? {url: path, depends: []} : path;
if(source.url.length == 0) return Promise.resolve();
return _script_promises[source.url] = (async () => {
/* await depends */
for(const depend of source.depends) {
if(!_script_promises[depend])
throw "Missing dependency " + depend;
await _script_promises[depend];
}
const tag: HTMLScriptElement = document.createElement("script");
await new Promise((resolve, reject) => {
let error = false;
const error_handler = (event: ErrorEvent) => {
if(event.filename == tag.src && event.message.indexOf("Illegal constructor") == -1) { //Our tag throw an uncaught error
if(config.verbose) console.log("msg: %o, url: %o, line: %o, col: %o, error: %o", event.message, event.filename, event.lineno, event.colno, event.error);
window.removeEventListener('error', error_handler as any);
reject(new SyntaxError(event.error));
event.preventDefault();
error = true;
}
};
window.addEventListener('error', error_handler as any);
const cleanup = () => {
tag.onerror = undefined;
tag.onload = undefined;
clearTimeout(timeout_handle);
window.removeEventListener('error', error_handler as any);
};
const timeout_handle = setTimeout(() => {
cleanup();
reject("timeout");
}, 5000);
tag.type = "application/javascript";
tag.async = true;
tag.defer = true;
tag.onerror = error => {
cleanup();
tag.remove();
reject(error);
};
tag.onload = () => {
cleanup();
if(config.verbose) console.debug("Script %o loaded", path);
setTimeout(resolve, 100);
};
document.getElementById("scripts").appendChild(tag);
tag.src = source.url + (cache_tag || "");
});
})();
}
}
export async function load_scripts(paths: SourcePath[]) : Promise<void> {
const promises: Promise<void>[] = [];
const errors: {
script: SourcePath,
error: any
}[] = [];
for(const script of paths)
promises.push(load_script(script).catch(error => {
errors.push({
script: script,
error: error
});
return Promise.resolve();
}));
await Promise.all([...promises]);
if(errors.length > 0) {
if(config.error) {
console.error("Failed to load the following scripts:");
for(const script of errors) {
const sname = script_name(script.script, false);
if(script.error instanceof SyntaxError) {
const source = script.error.source as Error;
if(source.name === "TypeError") {
let prefix = "";
while(prefix.length < sname.length + 7) prefix += " ";
console.log(" - %s: %s:\n%s", sname, source.message, source.stack.split("\n").map(e => prefix + e.trim()).slice(1).join("\n"));
} else {
console.log(" - %s: %o", sname, source);
}
} else {
console.log(" - %s: %o", sname, script.error);
}
}
}
critical_error("Failed to load script " + script_name(errors[0].script, true) + " <br>" + "View the browser console for more information!");
throw "failed to load script " + script_name(errors[0].script, false);
}
}
export async function load_style(path: SourcePath) : Promise<void> {
if(Array.isArray(path)) { //We have some fallback
return load_style(path[0]).catch(error => {
if(error instanceof SyntaxError)
return Promise.reject(error.source);
if(path.length > 1)
return load_script(path.slice(1));
return Promise.reject(error);
});
} else {
if(!path) {
return Promise.resolve();
}
return new Promise<void>((resolve, reject) => {
const tag: HTMLLinkElement = document.createElement("link");
let error = false;
const error_handler = (event: ErrorEvent) => {
if(config.verbose) console.log("msg: %o, url: %o, line: %o, col: %o, error: %o", event.message, event.filename, event.lineno, event.colno, event.error);
if(event.filename == tag.href) { //FIXME!
window.removeEventListener('error', error_handler as any);
reject(new SyntaxError(event.error));
event.preventDefault();
error = true;
}
};
window.addEventListener('error', error_handler as any);
tag.type = "text/css";
tag.rel = "stylesheet";
const cleanup = () => {
tag.onerror = undefined;
tag.onload = undefined;
clearTimeout(timeout_handle);
window.removeEventListener('error', error_handler as any);
};
const timeout_handle = setTimeout(() => {
cleanup();
reject("timeout");
}, 5000);
tag.onerror = error => {
cleanup();
tag.remove();
if(config.error)
console.error("File load error for file %s: %o", path, error);
reject("failed to load file " + path);
};
tag.onload = () => {
cleanup();
{
const css: CSSStyleSheet = tag.sheet as CSSStyleSheet;
const rules = css.cssRules;
const rules_remove: number[] = [];
const rules_add: string[] = [];
for(let index = 0; index < rules.length; index++) {
const rule = rules.item(index);
let rule_text = rule.cssText;
if(rule.cssText.indexOf("%%base_path%%") != -1) {
rules_remove.push(index);
rules_add.push(rule_text.replace("%%base_path%%", document.location.origin + document.location.pathname));
}
}
for(const index of rules_remove.sort((a, b) => b > a ? 1 : 0)) {
if(css.removeRule)
css.removeRule(index);
else
css.deleteRule(index);
}
for(const rule of rules_add)
css.insertRule(rule, rules_remove[0]);
}
if(config.verbose) console.debug("Style sheet %o loaded", path);
setTimeout(resolve, 100);
};
document.getElementById("style").appendChild(tag);
tag.href = path + (cache_tag || "");
});
}
}
export async function load_styles(paths: SourcePath[]) : Promise<void> {
const promises: Promise<void>[] = [];
const errors: {
sheet: SourcePath,
error: any
}[] = [];
for(const sheet of paths)
promises.push(load_style(sheet).catch(error => {
errors.push({
sheet: sheet,
error: error
});
return Promise.resolve();
}));
await Promise.all([...promises]);
if(errors.length > 0) {
if(config.error) {
console.error("Failed to load the following style sheet:");
for(const sheet of errors)
console.log(" - %o: %o", sheet.sheet, sheet.error);
}
critical_error("Failed to load style sheet " + script_name(errors[0].sheet, true) + " <br>" + "View the browser console for more information!");
throw "failed to load style sheet " + script_name(errors[0].sheet, false);
}
}
export async function load_template(path: SourcePath) : Promise<void> {
try {
const response = await $.ajax(path + (cache_tag || ""));
let node = document.createElement("html");
node.innerHTML = response;
let tags: HTMLCollection;
if(node.getElementsByTagName("body").length > 0)
tags = node.getElementsByTagName("body")[0].children;
else
tags = node.children;
let root = document.getElementById("templates");
if(!root) {
critical_error("Failed to find template tag!");
throw "Failed to find template tag";
}
while(tags.length > 0){
let tag = tags.item(0);
root.appendChild(tag);
}
} catch(error) {
let msg;
if('responseText' in error)
msg = error.responseText;
else if(error instanceof Error)
msg = error.message;
critical_error("failed to load template " + script_name(path, true), msg);
throw "template error";
}
}
export async function load_templates(paths: SourcePath[]) : Promise<void> {
const promises: Promise<void>[] = [];
const errors: {
template: SourcePath,
error: any
}[] = [];
for(const template of paths)
promises.push(load_template(template).catch(error => {
errors.push({
template: template,
error: error
});
return Promise.resolve();
}));
await Promise.all([...promises]);
if(errors.length > 0) {
if (config.error) {
console.error("Failed to load the following templates:");
for (const sheet of errors)
console.log(" - %s: %o", script_name(sheet.template, false), sheet.error);
}
critical_error("Failed to load template " + script_name(errors[0].template, true) + " <br>" + "View the browser console for more information!");
throw "failed to load template " + script_name(errors[0].template, false);
}
}
let version_: AppVersion;
export function version() : AppVersion { return version_; }
export function set_version(version: AppVersion) { version_ = version; }
export type ErrorHandler = (message: string, detail: string) => void;
let _callback_critical_error: ErrorHandler;
let _callback_critical_called: boolean = false;
export function critical_error(message: string, detail?: string) {
if(_callback_critical_called) {
console.warn("[CRITICAL] %s", message);
if(typeof(detail) === "string")
console.warn("[CRITICAL] %s", detail);
return;
}
_callback_critical_called = true;
if(_callback_critical_error) {
_callback_critical_error(message, detail);
return;
}
/* default handling */
let tag = document.getElementById("critical-load");
{
const error_tags = tag.getElementsByClassName("error");
error_tags[0].innerHTML = message;
}
if(typeof(detail) === "string") {
let node_detail = tag.getElementsByClassName("detail")[0];
node_detail.innerHTML = detail;
}
tag.classList.add("shown");
}
export function critical_error_handler(handler?: ErrorHandler, override?: boolean) : ErrorHandler {
if((typeof(handler) === "object" && handler !== _callback_critical_error) || override)
_callback_critical_error = handler;
return _callback_critical_error;
}
let _fadeout_warned;
export function hide_overlay() {
if(typeof($) === "undefined") {
if(!_fadeout_warned)
console.warn("Could not fadeout loader screen. Missing jquery functions.");
_fadeout_warned = true;
return;
}
const animation_duration = 750;
$(".loader .bookshelf_wrapper").animate({top: 0, opacity: 0}, animation_duration);
$(".loader .half").animate({width: 0}, animation_duration, () => {
$(".loader").detach();
});
}
{
const hello_world = () => {
const clog = console.log;
const print_security = () => {
{
const css = [
"display: block",
"text-align: center",
"font-size: 42px",
"font-weight: bold",
"-webkit-text-stroke: 2px black",
"color: red"
].join(";");
clog("%c ", "font-size: 100px;");
clog("%cSecurity warning:", css);
}
{
const css = [
"display: block",
"text-align: center",
"font-size: 18px",
"font-weight: bold"
].join(";");
clog("%cPasting anything in here could give attackers access to your data.", css);
clog("%cUnless you understand exactly what you are doing, close this window and stay safe.", css);
clog("%c ", "font-size: 100px;");
}
};
/* print the hello world */
{
const css = [
"display: block",
"text-align: center",
"font-size: 72px",
"font-weight: bold",
"-webkit-text-stroke: 2px black",
"color: #18BC9C"
].join(";");
clog("%cHey, hold on!", css);
}
{
const css = [
"display: block",
"text-align: center",
"font-size: 26px",
"font-weight: bold"
].join(";");
const css_2 = [
"display: block",
"text-align: center",
"font-size: 26px",
"font-weight: bold",
"color: blue"
].join(";");
const display_detect = /./;
display_detect.toString = function() { print_security(); return ""; };
clog("%cLovely to see you using and debugging the TeaSpeak-Web client.", css);
clog("%cIf you have some good ideas or already done some incredible changes,", css);
clog("%cyou'll be may interested to share them here: %chttps://github.com/TeaSpeak/TeaWeb", css, css_2);
clog("%c ", display_detect);
}
};
try { /* lets try to print it as VM code :)*/
let hello_world_code = hello_world.toString();
hello_world_code = hello_world_code.substr(hello_world_code.indexOf('() => {') + 8);
hello_world_code = hello_world_code.substring(0, hello_world_code.lastIndexOf("}"));
//Look aheads are not possible with firefox
//hello_world_code = hello_world_code.replace(/(?<!const|let)(?<=^([^"'/]|"[^"]*"|'[^']*'|`[^`]*`|\/[^/]*\/)*) /gm, ""); /* replace all spaces */
hello_world_code = hello_world_code.replace(/[\n\r]/g, ""); /* replace as new lines */
eval(hello_world_code);
} catch(e) {
console.error(e);
hello_world();
}
}

85
loader/exports/loader.d.ts vendored Normal file
View File

@ -0,0 +1,85 @@
export interface Config {
loader_groups: boolean;
verbose: boolean;
error: boolean;
}
export enum BackendType {
WEB,
NATIVE
}
export interface AppVersion {
ui: string;
backend: string;
type: "web" | "native";
debug_mode: boolean;
}
export let config: Config;
export type Task = {
name: string,
priority: number, /* tasks with the same priority will be executed in sync */
function: () => Promise<void>
};
export enum Stage {
/*
loading loader required files (incl this)
*/
INITIALIZING,
/*
setting up the loading process
*/
SETUP,
/*
loading all style sheet files
*/
STYLE,
/*
loading all javascript files
*/
JAVASCRIPT,
/*
loading all template files
*/
TEMPLATES,
/*
initializing static/global stuff
*/
JAVASCRIPT_INITIALIZING,
/*
finalizing load process
*/
FINALIZING,
/*
invoking main task
*/
LOADED,
DONE
}
export function version() : AppVersion;
export function finished();
export function running();
export function register_task(stage: Stage, task: Task);
export function execute() : Promise<void>;
export function execute_managed();
export type DependSource = {
url: string;
depends: string[];
}
export type SourcePath = string | DependSource | string[];
export function load_script(path: SourcePath) : Promise<void>;
export function load_scripts(paths: SourcePath[]) : Promise<void>;
export function load_style(path: SourcePath) : Promise<void>;
export function load_styles(paths: SourcePath[]) : Promise<void>;
export function load_template(path: SourcePath) : Promise<void>;
export function load_templates(paths: SourcePath[]) : Promise<void>;
export type ErrorHandler = (message: string, detail: string) => void;
export function critical_error(message: string, detail?: string);
export function critical_error_handler(handler?: ErrorHandler, override?: boolean);
export function hide_overlay();

62
loader/webpack.config.js Normal file
View File

@ -0,0 +1,62 @@
const path = require('path');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const isDevelopment = process.env.NODE_ENV === 'development';
module.exports = {
entry: path.join(__dirname, "app/index.ts"),
devtool: 'inline-source-map',
mode: "development",
plugins: [
new MiniCssExtractPlugin({
filename: isDevelopment ? '[name].css' : '[name].[hash].css',
chunkFilename: isDevelopment ? '[id].css' : '[id].[hash].css'
})
],
module: {
rules: [
{
test: /\.s[ac]ss$/,
loader: [
//isDevelopment ? 'style-loader' : MiniCssExtractPlugin.loader,
'style-loader',
{
loader: 'css-loader',
options: {
modules: true,
sourceMap: isDevelopment
}
},
{
loader: 'sass-loader',
options: {
sourceMap: isDevelopment
}
}
]
},
{
test: /\.tsx?$/,
exclude: /node_modules/,
loader: [
{
loader: 'ts-loader',
options: {
transpileOnly: true
}
}
]
},
],
},
resolve: {
extensions: ['.tsx', '.ts', '.js', ".scss"],
},
output: {
filename: 'loader.js',
path: path.resolve(__dirname, '../dist'),
library: "loader",
//libraryTarget: "umd" //"var" | "assign" | "this" | "window" | "self" | "global" | "commonjs" | "commonjs2" | "commonjs-module" | "amd" | "amd-require" | "umd" | "umd2" | "jsonp" | "system"
},
optimization: { }
};

5
package-lock.json generated
View File

@ -1087,6 +1087,11 @@
"safe-buffer": "^5.0.1"
}
},
"circular-dependency-plugin": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/circular-dependency-plugin/-/circular-dependency-plugin-5.2.0.tgz",
"integrity": "sha512-7p4Kn/gffhQaavNfyDFg7LS5S/UT1JAjyGd4UqR2+jzoYF02eDkj0Ec3+48TsIa4zghjLY87nQHIh/ecK9qLdw=="
},
"clap": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/clap/-/clap-1.2.3.tgz",

View File

@ -19,7 +19,9 @@
"minify-web-rel-file": "terser --compress --mangle --ecma 6 --keep_classnames --keep_fnames --output",
"start": "npm run compile-file-helper && node file.js ndevelop",
"build": "webpack --config webpack.config.js",
"watch": "webpack --watch"
"build-loader": "webpack --config loader/webpack.config.js",
"watch": "webpack --watch",
"watch-loader": "webpack --watch --config loader/webpack.config.js"
},
"author": "TeaSpeak (WolverinDEV)",
"license": "ISC",
@ -63,6 +65,7 @@
"homepage": "https://www.teaspeak.de",
"dependencies": {
"@types/fs-extra": "^8.0.1",
"circular-dependency-plugin": "^5.2.0",
"react": "^16.13.1",
"react-dom": "^16.13.1"
}

19
shared/backend.d/audio/player.d.ts vendored Normal file
View File

@ -0,0 +1,19 @@
import {Device} from "tc-shared/audio/player";
export function initialize() : boolean;
export function initialized() : boolean;
export function context() : AudioContext;
export function get_master_volume() : number;
export function set_master_volume(volume: number);
export function destination() : AudioNode;
export function on_ready(cb: () => any);
export function available_devices() : Promise<Device[]>;
export function set_device(device_id: string) : Promise<void>;
export function current_device() : Device;
export function initializeFromGesture();

9
shared/backend.d/audio/recorder.d.ts vendored Normal file
View File

@ -0,0 +1,9 @@
import {AbstractInput, InputDevice, LevelMeter} from "tc-shared/voice/RecorderBase";
export function devices() : InputDevice[];
export function device_refresh_available() : boolean;
export function refresh_devices() : Promise<void>;
export function create_input() : AbstractInput;
export function create_levelmeter(device: InputDevice) : Promise<LevelMeter>;

3
shared/backend.d/audio/sounds.d.ts vendored Normal file
View File

@ -0,0 +1,3 @@
import {SoundFile} from "tc-shared/sound/Sounds";
export function play_sound(file: SoundFile) : Promise<void>;

5
shared/backend.d/connection.d.ts vendored Normal file
View File

@ -0,0 +1,5 @@
import {ConnectionHandler} from "tc-shared/ConnectionHandler";
import {AbstractServerConnection} from "tc-shared/connection/ConnectionBase";
export function spawn_server_connection(handle: ConnectionHandler) : AbstractServerConnection;
export function destroy_server_connection(handle: AbstractServerConnection);

5
shared/backend.d/dns.d.ts vendored Normal file
View File

@ -0,0 +1,5 @@
import {AddressTarget, ResolveOptions} from "tc-shared/dns";
import {ServerAddress} from "tc-shared/ui/server";
export function supported();
export function resolve_address(address: ServerAddress, options?: ResolveOptions) : Promise<AddressTarget>;

12
shared/backend.d/ppt.d.ts vendored Normal file
View File

@ -0,0 +1,12 @@
import {KeyEvent, KeyHook, SpecialKey} from "tc-shared/PPTListener";
export function initialize() : Promise<void>;
export function finalize(); // most the times not really required
export function register_key_listener(listener: (_: KeyEvent) => any);
export function unregister_key_listener(listener: (_: KeyEvent) => any);
export function register_key_hook(hook: KeyHook);
export function unregister_key_hook(hook: KeyHook);
export function key_pressed(code: string | SpecialKey) : boolean;

View File

@ -1,35 +0,0 @@
declare namespace audio {
export namespace player {
export function initialize() : boolean;
export function initialized() : boolean;
export function context() : AudioContext;
export function get_master_volume() : number;
export function set_master_volume(volume: number);
export function destination() : AudioNode;
export function on_ready(cb: () => any);
export function available_devices() : Promise<Device[]>;
export function set_device(device_id: string) : Promise<void>;
export function current_device() : Device;
export function initializeFromGesture();
}
export namespace recorder {
export function devices() : InputDevice[];
export function device_refresh_available() : boolean;
export function refresh_devices() : Promise<void>;
export function create_input() : AbstractInput;
export function create_levelmeter(device: InputDevice) : Promise<LevelMeter>;
}
export namespace sounds {
export function play_sound(file: sound.SoundFile) : Promise<void>;
}
}

View File

@ -1,4 +0,0 @@
declare namespace connection {
export function spawn_server_connection(handle: ConnectionHandler) : AbstractServerConnection;
export function destroy_server_connection(handle: AbstractServerConnection);
}

View File

@ -1,4 +0,0 @@
declare namespace dns {
export function supported();
export function resolve_address(address: ServerAddress, options?: ResolveOptions) : Promise<AddressTarget>;
}

View File

@ -1,12 +0,0 @@
declare namespace ppt {
export function initialize() : Promise<void>;
export function finalize(); // most the times not really required
export function register_key_listener(listener: (_: KeyEvent) => any);
export function unregister_key_listener(listener: (_: KeyEvent) => any);
export function register_key_hook(hook: KeyHook);
export function unregister_key_hook(hook: KeyHook);
export function key_pressed(code: string | SpecialKey) : boolean;
}

View File

@ -94,7 +94,96 @@
}
&.channel {
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_text_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 {
display: flex;
}
}
.container-clients {
display: flex;
flex-direction: column;
}
}
&.client {
@ -183,6 +272,40 @@
}
}
&.channel .container-channel, &.client, &.server {
.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);
}
}
}
}

View File

@ -145,9 +145,11 @@
<meta name="app-loader-target" content="app">
<div id="scripts">
<!--
<script type="application/javascript" src="loader/loader_app.min.js" async defer></script>
<script type="application/javascript" src="loader/loader_app.js" async defer></script>
<script type="application/javascript" src="loader/loader.js?_<?php echo time() ?>" async defer></script>
-->
<script type="application/javascript" src="js/loader.js?_<?php echo time() ?>" async defer></script>
</div>
<!-- Loading screen -->

View File

@ -2736,13 +2736,13 @@
</div>
{{else type == "default" }}
<div class="entry default {{if selected}}selected{{/if}}">
<div class="country flag-gb" title="{{*:i18n.country_name('gb')}}"></div>
<div class="country flag-gb" title="{{>fallback_country_name}}"></div>
<div class="name">{{tr "English (Default / Fallback)" /}}</div>
</div>
{{else}}
<div class="entry translation {{if selected}}selected{{/if}}" parent-repository="{{:id}}">
<div class="country flag-{{:country_code}}"
title="{{*:i18n.country_name(data.country_code || 'XX')}}"></div>
title="{{>country_name}}"></div>
<div class="name">{{> name}}</div>
<div class="button button-info">
<div class="icon client-about"></div>

File diff suppressed because it is too large Load Diff

View File

@ -1,14 +1,38 @@
/// <reference path="log.ts" />
/// <reference path="proto.ts" />
/// <reference path="ui/view.ts" />
/// <reference path="settings.ts" />
/// <reference path="FileManager.ts" />
/// <reference path="permission/PermissionManager.ts" />
/// <reference path="permission/GroupManager.ts" />
/// <reference path="ui/frames/ControlBar.ts" />
/// <reference path="connection/ConnectionBase.ts" />
import {ChannelTree} from "tc-shared/ui/view";
import {AbstractServerConnection} from "tc-shared/connection/ConnectionBase";
import {PermissionManager} from "tc-shared/permission/PermissionManager";
import {GroupManager} from "tc-shared/permission/GroupManager";
import {ServerSettings, Settings, StaticSettings} from "tc-shared/settings";
import {Sound, SoundManager} from "tc-shared/sound/Sounds";
import {LocalClientEntry} from "tc-shared/ui/client";
import {ServerLog} from "tc-shared/ui/frames/server_log";
import {ConnectionProfile, default_profile, find_profile} from "tc-shared/profiles/ConnectionProfile";
import {ServerAddress} from "tc-shared/ui/server";
import * as log from "tc-shared/log";
import {LogCategory} from "tc-shared/log";
import * as server_log from "tc-shared/ui/frames/server_log";
import {createErrorModal, createInfoModal, createInputModal, Modal} from "tc-shared/ui/elements/Modal";
import {hashPassword} from "tc-shared/utils/helpers";
import {HandshakeHandler} from "tc-shared/connection/HandshakeHandler";
import * as htmltags from "./ui/htmltags";
import {ChannelEntry} from "tc-shared/ui/channel";
import {InputStartResult, InputState} from "tc-shared/voice/RecorderBase";
import {CommandResult, ErrorID} from "tc-shared/connection/ServerConnectionDeclaration";
import {guid} from "tc-shared/crypto/uid";
import * as bipc from "./BrowserIPC";
import {FileManager, spawn_upload_transfer, UploadKey} from "tc-shared/FileManager";
import {RecorderProfile} from "tc-shared/voice/RecorderProfile";
import {Frame} from "tc-shared/ui/frames/chat_frame";
import {Hostbanner} from "tc-shared/ui/frames/hostbanner";
import {server_connections} from "tc-shared/ui/frames/connection_handlers";
import {connection_log, Regex} from "tc-shared/ui/modal/ModalConnect";
import {control_bar} from "tc-shared/ui/frames/ControlBar";
import {formatMessage} from "tc-shared/ui/frames/chat";
import {spawnAvatarUpload} from "tc-shared/ui/modal/ModalAvatar";
import * as connection from "tc-backend/connection";
import * as dns from "tc-backend/dns";
enum DisconnectReason {
export enum DisconnectReason {
HANDLER_DESTROYED,
REQUESTED,
DNS_FAILED,
@ -28,7 +52,7 @@ enum DisconnectReason {
UNKNOWN
}
enum ConnectionState {
export enum ConnectionState {
UNCONNECTED,
CONNECTING,
INITIALISING,
@ -36,7 +60,7 @@ enum ConnectionState {
DISCONNECTING
}
enum ViewReasonId {
export enum ViewReasonId {
VREASON_USER_ACTION = 0,
VREASON_MOVED = 1,
VREASON_SYSTEM = 2,
@ -51,7 +75,7 @@ enum ViewReasonId {
VREASON_SERVER_SHUTDOWN = 11
}
interface VoiceStatus {
export interface VoiceStatus {
input_hardware: boolean;
input_muted: boolean;
output_muted: boolean;
@ -68,7 +92,7 @@ interface VoiceStatus {
queries_visible: boolean;
}
interface ConnectParameters {
export interface ConnectParameters {
nickname?: string;
channel?: {
target: string | number;
@ -79,20 +103,21 @@ interface ConnectParameters {
auto_reconnect_attempt?: boolean;
}
class ConnectionHandler {
declare const native_client;
export class ConnectionHandler {
channelTree: ChannelTree;
serverConnection: connection.AbstractServerConnection;
serverConnection: AbstractServerConnection;
fileManager: FileManager;
permissions: PermissionManager;
groups: GroupManager;
side_bar: chat.Frame;
side_bar: Frame;
settings: ServerSettings;
sound: sound.SoundManager;
sound: SoundManager;
hostbanner: Hostbanner;
@ -121,15 +146,15 @@ class ConnectionHandler {
};
invoke_resized_on_activate: boolean = false;
log: log.ServerLog;
log: ServerLog;
constructor() {
this.settings = new ServerSettings();
this.log = new log.ServerLog(this);
this.log = new ServerLog(this);
this.channelTree = new ChannelTree(this);
this.side_bar = new chat.Frame(this);
this.sound = new sound.SoundManager(this);
this.side_bar = new Frame(this);
this.sound = new SoundManager(this);
this.hostbanner = new Hostbanner(this);
this.serverConnection = connection.spawn_server_connection(this);
@ -169,7 +194,7 @@ class ConnectionHandler {
setup() { }
async startConnection(addr: string, profile: profiles.ConnectionProfile, user_action: boolean, parameters: ConnectParameters) {
async startConnection(addr: string, profile: ConnectionProfile, user_action: boolean, parameters: ConnectParameters) {
this.tab_set_name(tr("Connecting"));
this.cancel_reconnect(false);
this._reconnect_attempt = parameters.auto_reconnect_attempt || false;
@ -192,7 +217,7 @@ class ConnectionHandler {
}
}
log.info(LogCategory.CLIENT, tr("Start connection to %s:%d"), server_address.host, server_address.port);
this.log.log(log.server.Type.CONNECTION_BEGIN, {
this.log.log(server_log.Type.CONNECTION_BEGIN, {
address: {
server_hostname: server_address.host,
server_port: server_address.port
@ -203,7 +228,7 @@ class ConnectionHandler {
if(parameters.password && !parameters.password.hashed){
try {
const password = await helpers.hashPassword(parameters.password.password);
const password = await hashPassword(parameters.password.password);
parameters.password = {
hashed: true,
password: password
@ -221,9 +246,9 @@ class ConnectionHandler {
}
const original_address = {host: server_address.host, port: server_address.port};
if(dns.supported() && !server_address.host.match(Modals.Regex.IP_V4) && !server_address.host.match(Modals.Regex.IP_V6)) {
if(dns.supported() && !server_address.host.match(Regex.IP_V4) && !server_address.host.match(Regex.IP_V6)) {
const id = ++this._connect_initialize_id;
this.log.log(log.server.Type.CONNECTION_HOSTNAME_RESOLVE, {});
this.log.log(server_log.Type.CONNECTION_HOSTNAME_RESOLVE, {});
try {
const resolved = await dns.resolve_address(server_address, { timeout: 5000 }) || {} as any;
if(id != this._connect_initialize_id)
@ -231,7 +256,7 @@ class ConnectionHandler {
server_address.host = typeof(resolved.target_ip) === "string" ? resolved.target_ip : server_address.host;
server_address.port = typeof(resolved.target_port) === "number" ? resolved.target_port : server_address.port;
this.log.log(log.server.Type.CONNECTION_HOSTNAME_RESOLVED, {
this.log.log(server_log.Type.CONNECTION_HOSTNAME_RESOLVED, {
address: {
server_port: server_address.port,
server_hostname: server_address.host
@ -245,7 +270,7 @@ class ConnectionHandler {
}
}
await this.serverConnection.connect(server_address, new connection.HandshakeHandler(profile, parameters));
await this.serverConnection.connect(server_address, new HandshakeHandler(profile, parameters));
setTimeout(() => {
const connected = this.serverConnection.connected();
if(user_action && connected) {
@ -270,7 +295,7 @@ class ConnectionHandler {
return this._clientId;
}
getServerConnection() : connection.AbstractServerConnection { return this.serverConnection; }
getServerConnection() : AbstractServerConnection { return this.serverConnection; }
/**
@ -380,7 +405,7 @@ class ConnectionHandler {
popup.close(); /* no need, but nicer */
const profile = profiles.find_profile(properties.connect_profile) || profiles.default_profile();
const profile = find_profile(properties.connect_profile) || default_profile();
const cprops = this.reconnect_properties(profile);
this.startConnection(properties.connect_address, profile, true, cprops);
});
@ -418,12 +443,12 @@ class ConnectionHandler {
case DisconnectReason.HANDLER_DESTROYED:
if(data) {
this.sound.play(Sound.CONNECTION_DISCONNECTED);
this.log.log(log.server.Type.DISCONNECTED, {});
this.log.log(server_log.Type.DISCONNECTED, {});
}
break;
case DisconnectReason.DNS_FAILED:
log.error(LogCategory.CLIENT, tr("Failed to resolve hostname: %o"), data);
this.log.log(log.server.Type.CONNECTION_HOSTNAME_RESOLVE_ERROR, {
this.log.log(server_log.Type.CONNECTION_HOSTNAME_RESOLVE_ERROR, {
message: data as any
});
this.sound.play(Sound.CONNECTION_REFUSED);
@ -431,7 +456,7 @@ class ConnectionHandler {
case DisconnectReason.CONNECT_FAILURE:
if(this._reconnect_attempt) {
auto_reconnect = true;
this.log.log(log.server.Type.CONNECTION_FAILED, {});
this.log.log(server_log.Type.CONNECTION_FAILED, {});
break;
}
if(data)
@ -452,7 +477,7 @@ class ConnectionHandler {
this._certificate_modal = createErrorModal(
tr("Could not connect"),
MessageHelper.formatMessage(tr(error_message_format), this.generate_ssl_certificate_accept())
formatMessage(tr(error_message_format), this.generate_ssl_certificate_accept())
);
this._certificate_modal.close_listener.push(() => this._certificate_modal = undefined);
this._certificate_modal.open();
@ -470,7 +495,7 @@ class ConnectionHandler {
case DisconnectReason.HANDSHAKE_TEAMSPEAK_REQUIRED:
createErrorModal(
tr("Target server is a TeamSpeak server"),
MessageHelper.formatMessage(tr("The target server is a TeamSpeak 3 server!{:br:}Only TeamSpeak 3 based identities are able to connect.{:br:}Please select another profile or change the identify type."))
formatMessage(tr("The target server is a TeamSpeak 3 server!{:br:}Only TeamSpeak 3 based identities are able to connect.{:br:}Please select another profile or change the identify type."))
).open();
this.sound.play(Sound.CONNECTION_DISCONNECTED);
auto_reconnect = false;
@ -478,7 +503,7 @@ class ConnectionHandler {
case DisconnectReason.IDENTITY_TOO_LOW:
createErrorModal(
tr("Identity level is too low"),
MessageHelper.formatMessage(tr("You've been disconnected, because your Identity level is too low.{:br:}You need at least a level of {0}"), data["extra_message"])
formatMessage(tr("You've been disconnected, because your Identity level is too low.{:br:}You need at least a level of {0}"), data["extra_message"])
).open();
this.sound.play(Sound.CONNECTION_DISCONNECTED);
@ -506,7 +531,7 @@ class ConnectionHandler {
break;
case DisconnectReason.SERVER_CLOSED:
this.log.log(log.server.Type.SERVER_CLOSED, {message: data.reasonmsg});
this.log.log(server_log.Type.SERVER_CLOSED, {message: data.reasonmsg});
createErrorModal(
tr("Server closed"),
@ -518,7 +543,7 @@ class ConnectionHandler {
auto_reconnect = true;
break;
case DisconnectReason.SERVER_REQUIRES_PASSWORD:
this.log.log(log.server.Type.SERVER_REQUIRES_PASSWORD, {});
this.log.log(server_log.Type.SERVER_REQUIRES_PASSWORD, {});
createInputModal(tr("Server password"), tr("Enter server password:"), password => password.length != 0, password => {
if(!(typeof password === "string")) return;
@ -540,7 +565,7 @@ class ConnectionHandler {
const have_invoker = typeof(data["invokerid"]) !== "undefined" && parseInt(data["invokerid"]) !== 0;
const modal = createErrorModal(
tr("You've been kicked"),
MessageHelper.formatMessage(
formatMessage(
have_invoker ? tr("You've been kicked from the server by {0}:{:br:}{1}") : tr("You've been kicked from the server:{:br:}{1}"),
have_invoker ?
htmltags.generate_client_object({ client_id: parseInt(data["invokerid"]), client_unique_id: data["invokeruid"], client_name: data["invokername"]}) :
@ -559,7 +584,7 @@ class ConnectionHandler {
this.sound.play(Sound.CONNECTION_BANNED);
break;
case DisconnectReason.CLIENT_BANNED:
this.log.log(log.server.Type.SERVER_BANNED, {
this.log.log(server_log.Type.SERVER_BANNED, {
invoker: {
client_name: data["invokername"],
client_id: parseInt(data["invokerid"]),
@ -592,7 +617,7 @@ class ConnectionHandler {
log.info(LogCategory.NETWORKING, tr("Allowed to auto reconnect but cant reconnect because we dont have any information left..."));
return;
}
this.log.log(log.server.Type.RECONNECT_SCHEDULED, {timeout: 5000});
this.log.log(server_log.Type.RECONNECT_SCHEDULED, {timeout: 5000});
log.info(LogCategory.NETWORKING, tr("Allowed to auto reconnect. Reconnecting in 5000ms"));
const server_address = this.serverConnection.remote_address();
@ -600,7 +625,7 @@ class ConnectionHandler {
this._reconnect_timer = setTimeout(() => {
this._reconnect_timer = undefined;
this.log.log(log.server.Type.RECONNECT_EXECUTE, {});
this.log.log(server_log.Type.RECONNECT_EXECUTE, {});
log.info(LogCategory.NETWORKING, tr("Reconnecting..."));
this.startConnection(server_address.host + ":" + server_address.port, profile, false, Object.assign(this.reconnect_properties(profile), {auto_reconnect_attempt: true}));
@ -610,7 +635,7 @@ class ConnectionHandler {
cancel_reconnect(log_event: boolean) {
if(this._reconnect_timer) {
if(log_event) this.log.log(log.server.Type.RECONNECT_CANCELED, {});
if(log_event) this.log.log(server_log.Type.RECONNECT_CANCELED, {});
clearTimeout(this._reconnect_timer);
this._reconnect_timer = undefined;
}
@ -665,7 +690,7 @@ class ConnectionHandler {
if(Object.keys(property_update).length > 0) {
this.serverConnection.send_command("clientupdate", property_update).catch(error => {
log.warn(LogCategory.GENERAL, tr("Failed to update client audio hardware properties. Error: %o"), error);
this.log.log(log.server.Type.ERROR_CUSTOM, {message: tr("Failed to update audio hardware properties.")});
this.log.log(server_log.Type.ERROR_CUSTOM, {message: tr("Failed to update audio hardware properties.")});
/* Update these properties anyways (for case the server fails to handle the command) */
const updates = [];
@ -708,15 +733,15 @@ class ConnectionHandler {
const input = vconnection.voice_recorder().input;
if(input) {
if(active && this.serverConnection.connected()) {
if(input.current_state() === audio.recorder.InputState.PAUSED) {
if(input.current_state() === InputState.PAUSED) {
input.start().then(result => {
if(result != audio.recorder.InputStartResult.EOK)
if(result != InputStartResult.EOK)
throw result;
}).catch(error => {
log.warn(LogCategory.VOICE, tr("Failed to start microphone input (%s)."), error);
if(Date.now() - (this._last_record_error_popup || 0) > 10 * 1000) {
this._last_record_error_popup = Date.now();
createErrorModal(tr("Failed to start recording"), MessageHelper.formatMessage(tr("Microphone start failed.{:br:}Error: {}"), error)).open();
createErrorModal(tr("Failed to start recording"), formatMessage(tr("Microphone start failed.{:br:}Error: {}"), error)).open();
}
});
}
@ -741,7 +766,7 @@ class ConnectionHandler {
client_output_hardware: this.client_status.sound_playback_supported
}).catch(error => {
log.warn(LogCategory.GENERAL, tr("Failed to sync handler state with server. Error: %o"), error);
this.log.log(log.server.Type.ERROR_CUSTOM, {message: tr("Failed to sync handler state with server.")});
this.log.log(server_log.Type.ERROR_CUSTOM, {message: tr("Failed to sync handler state with server.")});
});
}
@ -761,7 +786,7 @@ class ConnectionHandler {
client_away_message: typeof(this.client_status.away) === "string" ? this.client_status.away : "",
}).catch(error => {
log.warn(LogCategory.GENERAL, tr("Failed to update away status. Error: %o"), error);
this.log.log(log.server.Type.ERROR_CUSTOM, {message: tr("Failed to update away status.")});
this.log.log(server_log.Type.ERROR_CUSTOM, {message: tr("Failed to update away status.")});
});
control_bar.update_button_away();
@ -781,10 +806,10 @@ class ConnectionHandler {
});
}
reconnect_properties(profile?: profiles.ConnectionProfile) : ConnectParameters {
reconnect_properties(profile?: ConnectionProfile) : ConnectParameters {
const name = (this.getClient() ? this.getClient().clientNickName() : "") ||
(this.serverConnection && this.serverConnection.handshake_handler() ? this.serverConnection.handshake_handler().parameters.nickname : "") ||
settings.static_global(Settings.KEY_CONNECT_USERNAME, profile ? profile.default_username : undefined) ||
StaticSettings.instance.static(Settings.KEY_CONNECT_USERNAME, profile ? profile.default_username : undefined) ||
"Another TeaSpeak user";
const channel = (this.getClient() && this.getClient().currentChannel() ? this.getClient().currentChannel().channelId : 0) ||
(this.serverConnection && this.serverConnection.handshake_handler() ? (this.serverConnection.handshake_handler().parameters.channel || {} as any).target : "");
@ -798,7 +823,7 @@ class ConnectionHandler {
}
update_avatar() {
Modals.spawnAvatarUpload(data => {
spawnAvatarUpload(data => {
if(typeof(data) === "undefined")
return;
if(data === null) {
@ -814,16 +839,16 @@ class ConnectionHandler {
let message;
if(error instanceof CommandResult)
message = MessageHelper.formatMessage(tr("Failed to delete avatar.{:br:}Error: {0}"), error.extra_message || error.message);
message = formatMessage(tr("Failed to delete avatar.{:br:}Error: {0}"), error.extra_message || error.message);
if(!message)
message = MessageHelper.formatMessage(tr("Failed to delete avatar.{:br:}Lookup the console for more details"));
message = formatMessage(tr("Failed to delete avatar.{:br:}Lookup the console for more details"));
createErrorModal(tr("Failed to delete avatar"), message).open();
return;
});
} else {
log.info(LogCategory.CLIENT, tr("Uploading new avatar"));
(async () => {
let key: transfer.UploadKey;
let key: UploadKey;
try {
key = await this.fileManager.upload_file({
size: data.byteLength,
@ -840,28 +865,28 @@ class ConnectionHandler {
//TODO: Resolve permission name
//i_client_max_avatar_filesize
if(error.id == ErrorID.PERMISSION_ERROR) {
message = MessageHelper.formatMessage(tr("Failed to initialize avatar upload.{:br:}Missing permission {0}"), error["failed_permid"]);
message = formatMessage(tr("Failed to initialize avatar upload.{:br:}Missing permission {0}"), error["failed_permid"]);
} else {
message = MessageHelper.formatMessage(tr("Failed to initialize avatar upload.{:br:}Error: {0}"), error.extra_message || error.message);
message = formatMessage(tr("Failed to initialize avatar upload.{:br:}Error: {0}"), error.extra_message || error.message);
}
}
if(!message)
message = MessageHelper.formatMessage(tr("Failed to initialize avatar upload.{:br:}Lookup the console for more details"));
message = formatMessage(tr("Failed to initialize avatar upload.{:br:}Lookup the console for more details"));
createErrorModal(tr("Failed to upload avatar"), message).open();
return;
}
try {
await transfer.spawn_upload_transfer(key).put_data(data);
await spawn_upload_transfer(key).put_data(data);
} catch(error) {
log.error(LogCategory.GENERAL, tr("Failed to upload avatar: %o"), error);
let message;
if(typeof(error) === "string")
message = MessageHelper.formatMessage(tr("Failed to upload avatar.{:br:}Error: {0}"), error);
message = formatMessage(tr("Failed to upload avatar.{:br:}Error: {0}"), error);
if(!message)
message = MessageHelper.formatMessage(tr("Failed to initialize avatar upload.{:br:}Lookup the console for more details"));
message = formatMessage(tr("Failed to initialize avatar upload.{:br:}Lookup the console for more details"));
createErrorModal(tr("Failed to upload avatar"), message).open();
return;
}
@ -874,9 +899,9 @@ class ConnectionHandler {
let message;
if(error instanceof CommandResult)
message = MessageHelper.formatMessage(tr("Failed to update avatar flag.{:br:}Error: {0}"), error.extra_message || error.message);
message = formatMessage(tr("Failed to update avatar flag.{:br:}Error: {0}"), error.extra_message || error.message);
if(!message)
message = MessageHelper.formatMessage(tr("Failed to update avatar flag.{:br:}Lookup the console for more details"));
message = formatMessage(tr("Failed to update avatar flag.{:br:}Lookup the console for more details"));
createErrorModal(tr("Failed to set avatar"), message).open();
return;
}

View File

@ -1,76 +1,80 @@
/// <reference path="connection/CommandHandler.ts" />
/// <reference path="connection/ConnectionBase.ts" />
import * as log from "tc-shared/log";
import {LogCategory} from "tc-shared/log";
import {ChannelEntry} from "tc-shared/ui/channel";
import {ConnectionHandler} from "tc-shared/ConnectionHandler";
import {ServerCommand} from "tc-shared/connection/ConnectionBase";
import {CommandResult} from "tc-shared/connection/ServerConnectionDeclaration";
import {ClientEntry} from "tc-shared/ui/client";
import {AbstractCommandHandler} from "tc-shared/connection/AbstractCommandHandler";
class FileEntry {
export class FileEntry {
name: string;
datetime: number;
type: number;
size: number;
}
class FileListRequest {
export class FileListRequest {
path: string;
entries: FileEntry[];
callback: (entries: FileEntry[]) => void;
}
namespace transfer {
export interface TransferKey {
client_transfer_id: number;
server_transfer_id: number;
export interface TransferKey {
client_transfer_id: number;
server_transfer_id: number;
key: string;
key: string;
file_path: string;
file_name: string;
file_path: string;
file_name: string;
peer: {
hosts: string[],
port: number;
};
peer: {
hosts: string[],
port: number;
};
total_size: number;
}
export interface UploadOptions {
name: string;
path: string;
channel?: ChannelEntry;
channel_password?: string;
size: number;
overwrite: boolean;
}
export interface DownloadTransfer {
get_key() : DownloadKey;
request_file() : Promise<Response>;
}
export interface UploadTransfer {
get_key(): UploadKey;
put_data(data: BlobPart | File) : Promise<void>;
}
export type DownloadKey = TransferKey;
export type UploadKey = TransferKey;
export function spawn_download_transfer(key: DownloadKey) : DownloadTransfer {
return new RequestFileDownload(key);
}
export function spawn_upload_transfer(key: UploadKey) : UploadTransfer {
return new RequestFileUpload(key);
}
total_size: number;
}
class RequestFileDownload implements transfer.DownloadTransfer {
readonly transfer_key: transfer.DownloadKey;
export interface UploadOptions {
name: string;
path: string;
constructor(key: transfer.DownloadKey) {
channel?: ChannelEntry;
channel_password?: string;
size: number;
overwrite: boolean;
}
export interface DownloadTransfer {
get_key() : DownloadKey;
request_file() : Promise<Response>;
}
export interface UploadTransfer {
get_key(): UploadKey;
put_data(data: BlobPart | File) : Promise<void>;
}
export type DownloadKey = TransferKey;
export type UploadKey = TransferKey;
export function spawn_download_transfer(key: DownloadKey) : DownloadTransfer {
return new RequestFileDownload(key);
}
export function spawn_upload_transfer(key: UploadKey) : UploadTransfer {
return new RequestFileUpload(key);
}
export class RequestFileDownload implements DownloadTransfer {
readonly transfer_key: DownloadKey;
constructor(key: DownloadKey) {
this.transfer_key = key;
}
@ -97,18 +101,18 @@ class RequestFileDownload implements transfer.DownloadTransfer {
return response;
}
get_key(): transfer.DownloadKey {
get_key(): DownloadKey {
return this.transfer_key;
}
}
class RequestFileUpload implements transfer.UploadTransfer {
readonly transfer_key: transfer.UploadKey;
constructor(key: transfer.DownloadKey) {
export class RequestFileUpload implements UploadTransfer {
readonly transfer_key: UploadKey;
constructor(key: DownloadKey) {
this.transfer_key = key;
}
get_key(): transfer.UploadKey {
get_key(): UploadKey {
return this.transfer_key;
}
@ -152,14 +156,14 @@ class RequestFileUpload implements transfer.UploadTransfer {
}
}
class FileManager extends connection.AbstractCommandHandler {
export class FileManager extends AbstractCommandHandler {
handle: ConnectionHandler;
icons: IconManager;
avatars: AvatarManager;
private listRequests: FileListRequest[] = [];
private pending_download_requests: transfer.DownloadKey[] = [];
private pending_upload_requests: transfer.UploadKey[] = [];
private pending_download_requests: DownloadKey[] = [];
private pending_upload_requests: UploadKey[] = [];
private transfer_counter : number = 1;
@ -191,7 +195,7 @@ class FileManager extends connection.AbstractCommandHandler {
this.avatars = undefined;
}
handle_command(command: connection.ServerCommand): boolean {
handle_command(command: ServerCommand): boolean {
switch (command.command) {
case "notifyfilelist":
this.notifyFileList(command.arguments);
@ -276,15 +280,15 @@ class FileManager extends connection.AbstractCommandHandler {
/******************************** File download/upload ********************************/
download_file(path: string, file: string, channel?: ChannelEntry, password?: string) : Promise<transfer.DownloadKey> {
const transfer_data: transfer.DownloadKey = {
download_file(path: string, file: string, channel?: ChannelEntry, password?: string) : Promise<DownloadKey> {
const transfer_data: DownloadKey = {
file_name: file,
file_path: path,
client_transfer_id: this.transfer_counter++
} as any;
this.pending_download_requests.push(transfer_data);
return new Promise<transfer.DownloadKey>((resolve, reject) => {
return new Promise<DownloadKey>((resolve, reject) => {
transfer_data["_callback"] = resolve;
this.handle.serverConnection.send_command("ftinitdownload", {
"path": path,
@ -301,8 +305,8 @@ class FileManager extends connection.AbstractCommandHandler {
});
}
upload_file(options: transfer.UploadOptions) : Promise<transfer.UploadKey> {
const transfer_data: transfer.UploadKey = {
upload_file(options: UploadOptions) : Promise<UploadKey> {
const transfer_data: UploadKey = {
file_path: options.path,
file_name: options.name,
client_transfer_id: this.transfer_counter++,
@ -310,7 +314,7 @@ class FileManager extends connection.AbstractCommandHandler {
} as any;
this.pending_upload_requests.push(transfer_data);
return new Promise<transfer.UploadKey>((resolve, reject) => {
return new Promise<UploadKey>((resolve, reject) => {
transfer_data["_callback"] = resolve;
this.handle.serverConnection.send_command("ftinitupload", {
"path": options.path,
@ -333,7 +337,7 @@ class FileManager extends connection.AbstractCommandHandler {
json = json[0];
let clientftfid = parseInt(json["clientftfid"]);
let transfer: transfer.DownloadKey;
let transfer: DownloadKey;
for(let e of this.pending_download_requests)
if(e.client_transfer_id == clientftfid) {
transfer = e;
@ -355,14 +359,14 @@ class FileManager extends connection.AbstractCommandHandler {
if(transfer.peer.hosts[0].length == 0 || transfer.peer.hosts[0] == '0.0.0.0')
transfer.peer.hosts[0] = this.handle.serverConnection.remote_address().host;
(transfer["_callback"] as (val: transfer.DownloadKey) => void)(transfer);
(transfer["_callback"] as (val: DownloadKey) => void)(transfer);
this.pending_download_requests.remove(transfer);
}
private notifyStartUpload(json) {
json = json[0];
let transfer: transfer.UploadKey;
let transfer: UploadKey;
let clientftfid = parseInt(json["clientftfid"]);
for(let e of this.pending_upload_requests)
if(e.client_transfer_id == clientftfid) {
@ -384,7 +388,7 @@ class FileManager extends connection.AbstractCommandHandler {
if(transfer.peer.hosts[0].length == 0 || transfer.peer.hosts[0] == '0.0.0.0')
transfer.peer.hosts[0] = this.handle.serverConnection.remote_address().host;
(transfer["_callback"] as (val: transfer.UploadKey) => void)(transfer);
(transfer["_callback"] as (val: UploadKey) => void)(transfer);
this.pending_upload_requests.remove(transfer);
}
@ -411,12 +415,12 @@ class FileManager extends connection.AbstractCommandHandler {
}
}
class Icon {
export class Icon {
id: number;
url: string;
}
enum ImageType {
export enum ImageType {
UNKNOWN,
BITMAP,
PNG,
@ -425,7 +429,7 @@ enum ImageType {
JPEG
}
function media_image_type(type: ImageType, file?: boolean) {
export function media_image_type(type: ImageType, file?: boolean) {
switch (type) {
case ImageType.BITMAP:
return "bmp";
@ -442,7 +446,7 @@ function media_image_type(type: ImageType, file?: boolean) {
}
}
function image_type(encoded_data: string | ArrayBuffer, base64_encoded?: boolean) {
export function image_type(encoded_data: string | ArrayBuffer, base64_encoded?: boolean) {
const ab2str10 = () => {
const buf = new Uint8Array(encoded_data as ArrayBuffer);
if(buf.byteLength < 10)
@ -472,7 +476,7 @@ function image_type(encoded_data: string | ArrayBuffer, base64_encoded?: boolean
return ImageType.UNKNOWN;
}
class CacheManager {
export class CacheManager {
readonly cache_name: string;
private _cache_category: Cache;
@ -547,7 +551,7 @@ class CacheManager {
}
}
class IconManager {
export class IconManager {
private static cache: CacheManager = new CacheManager("icons");
handle: FileManager;
@ -590,7 +594,7 @@ class IconManager {
return this.handle.requestFileList("/icons");
}
create_icon_download(id: number) : Promise<transfer.DownloadKey> {
create_icon_download(id: number) : Promise<DownloadKey> {
return this.handle.download_file("", "/icon_" + id);
}
@ -665,7 +669,7 @@ class IconManager {
private async _load_icon(id: number) : Promise<Icon> {
try {
let download_key: transfer.DownloadKey;
let download_key: DownloadKey;
try {
download_key = await this.create_icon_download(id);
} catch(error) {
@ -673,7 +677,7 @@ class IconManager {
throw "Failed to request icon";
}
const downloader = transfer.spawn_download_transfer(download_key);
const downloader = spawn_download_transfer(download_key);
let response: Response;
try {
response = await downloader.request_file();
@ -801,14 +805,14 @@ class IconManager {
}
}
class Avatar {
export class Avatar {
client_avatar_id: string; /* the base64 uid thing from a-m */
avatar_id: string; /* client_flag_avatar */
url: string;
type: ImageType;
}
class AvatarManager {
export class AvatarManager {
handle: FileManager;
private static cache: CacheManager;
@ -867,14 +871,14 @@ class AvatarManager {
};
}
create_avatar_download(client_avatar_id: string) : Promise<transfer.DownloadKey> {
create_avatar_download(client_avatar_id: string) : Promise<DownloadKey> {
log.debug(LogCategory.GENERAL, "Requesting download for avatar %s", client_avatar_id);
return this.handle.download_file("", "/avatar_" + client_avatar_id);
}
private async _load_avatar(client_avatar_id: string, avatar_version: string) {
try {
let download_key: transfer.DownloadKey;
let download_key: DownloadKey;
try {
download_key = await this.create_avatar_download(client_avatar_id);
} catch(error) {
@ -882,7 +886,7 @@ class AvatarManager {
throw "failed to request avatar download";
}
const downloader = transfer.spawn_download_transfer(download_key);
const downloader = spawn_download_transfer(download_key);
let response: Response;
try {
response = await downloader.request_file();

View File

@ -1,250 +1,280 @@
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}) --]/;
const sanitizer_escaped_map: {[key: string]: string} = {};
import {Settings, settings} from "tc-shared/settings";
import * as contextmenu from "tc-shared/ui/elements/ContextMenu";
import {copy_to_clipboard} from "tc-shared/utils/helpers";
import {guid} from "tc-shared/crypto/uid";
import * as loader from "tc-loader";
import * as image_preview from "./ui/frames/image_preview"
const yt_url_regex = /^((?:https?:)?\/\/)?((?:www|m)\.)?((?:youtube\.com|youtu.be))(\/(?:[\w\-]+\?v=|embed\/|v\/)?)([\w\-]+)(\S+)?$/;
declare const xbbcode;
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}) --]/;
const sanitizer_escaped_map: {[key: string]: string} = {};
export interface FormatSettings {
is_chat_message?: boolean
}
const yt_url_regex = /^((?:https?:)?\/\/)?((?:www|m)\.)?((?:youtube\.com|youtu.be))(\/(?:[\w\-]+\?v=|embed\/|v\/)?)([\w\-]+)(\S+)?$/;
export function format(message: string, fsettings?: FormatSettings) : JQuery[] {
fsettings = fsettings || {};
single_url_parse:
if(fsettings.is_chat_message) {
/* try if its only one url */
const raw_url = message.replace(/\[url(=\S+)?](\S+)\[\/url]/, "$2");
let url: URL;
try {
url = new URL(raw_url);
} catch(error) {
break single_url_parse;
}
single_url_yt:
{
const result = raw_url.match(yt_url_regex);
if(!result) break single_url_yt;
return format("[yt]https://www.youtube.com/watch?v=" + result[5] + "[/yt]");
}
single_url_image:
{
const ext_index = url.pathname.lastIndexOf(".");
if(ext_index == -1) break single_url_image;
const ext_name = url.pathname.substr(ext_index + 1).toLowerCase();
if([
"jpeg", "jpg",
"png", "bmp", "gif",
"tiff", "pdf", "svg"
].findIndex(e => e === ext_name) == -1) break single_url_image;
return format("[img]" + message + "[/img]");
}
}
const result = xbbcode.parse(message, {
tag_whitelist: [
"b", "big",
"i", "italic",
"u", "underlined",
"s", "strikethrough",
"color",
"url",
"code",
"i-code", "icode",
"sub", "sup",
"size",
"hr", "br",
"ul", "ol", "list",
"li",
"table",
"tr", "td", "th",
"yt", "youtube",
"img"
]
});
let html = result.build_html();
if(typeof(window.twemoji) !== "undefined" && settings.static_global(Settings.KEY_CHAT_COLORED_EMOJIES))
html = twemoji.parse(html);
const container = $.spawn("div");
let sanitized = DOMPurify.sanitize(html, {
ADD_ATTR: [
"x-highlight-type",
"x-code-type",
"x-image-url"
]
});
sanitized = sanitized.replace(sanitizer_escaped_regex, data => {
const uid = data.match(sanitizer_escaped_regex)[1];
const value = sanitizer_escaped_map[uid];
if(!value) return data;
delete sanitizer_escaped_map[uid];
return value;
});
container[0].innerHTML = sanitized;
container.find("a")
.attr('target', "_blank")
.on('contextmenu', event => {
if(event.isDefaultPrevented()) return;
event.preventDefault();
const url = $(event.target).attr("href");
contextmenu.spawn_context_menu(event.pageX, event.pageY, {
callback: () => {
const win = window.open(url, '_blank');
win.focus();
},
name: tr("Open URL"),
type: contextmenu.MenuEntryType.ENTRY,
icon_class: "client-browse-addon-online"
}, {
callback: () => {
//TODO
},
name: tr("Open URL in Browser"),
type: contextmenu.MenuEntryType.ENTRY,
visible: !app.is_web() && false // Currently not possible
}, contextmenu.Entry.HR(), {
callback: () => copy_to_clipboard(url),
name: tr("Copy URL to clipboard"),
type: contextmenu.MenuEntryType.ENTRY,
icon_class: "client-copy"
});
});
return [container.contents() as JQuery];
//return result.root_tag.content.map(e => e.build_html()).map((entry, idx, array) => $.spawn("a").css("display", (idx == 0 ? "inline" : "") + "block").html(entry == "" && idx != 0 ? "&nbsp;" : entry));
}
export function load_image(entry: HTMLImageElement) {
const url = decodeURIComponent(entry.getAttribute("x-image-url") || "");
const proxy_url = "https://images.weserv.nl/?url=" + encodeURIComponent(url);
entry.onload = undefined;
entry.src = proxy_url;
const parent = $(entry.parentElement);
parent.on('contextmenu', event => {
contextmenu.spawn_context_menu(event.pageX, event.pageY, {
callback: () => {
const win = window.open(url, '_blank');
win.focus();
},
name: tr("Open image in browser"),
type: contextmenu.MenuEntryType.ENTRY,
icon_class: "client-browse-addon-online"
}, contextmenu.Entry.HR(), {
callback: () => copy_to_clipboard(url),
name: tr("Copy image URL to clipboard"),
type: contextmenu.MenuEntryType.ENTRY,
icon_class: "client-copy"
})
});
parent.css("cursor", "pointer").on('click', event => image_preview.preview_image(proxy_url, url));
}
loader.register_task(loader.Stage.JAVASCRIPT_INITIALIZING, {
name: "XBBCode code tag init",
function: async () => {
/* override default parser */
xbbcode.register.register_parser({
tag: ["code", "icode", "i-code"],
content_tags_whitelist: [],
build_html(layer) : string {
const klass = layer.tag_normalized != 'code' ? "tag-hljs-inline-code" : "tag-hljs-code";
const language = (layer.options || "").replace("\"", "'").toLowerCase();
/* remove heading empty lines */
let text = layer.content.map(e => e.build_text())
.reduce((a, b) => a.length == 0 && b.replace(/[ \n\r\t]+/g, "").length == 0 ? "" : a + b, "")
.replace(/^([ \n\r\t]*)(?=\n)+/g, "");
if(text.startsWith("\r") || text.startsWith("\n"))
text = text.substr(1);
let result: HighlightJSResult;
if(window.hljs.getLanguage(language))
result = window.hljs.highlight(language, text, true);
else
result = window.hljs.highlightAuto(text);
let html = '<pre class="' + klass + '">';
html += '<code class="hljs" x-code-type="' + language + '" x-highlight-type="' + result.language + '">';
html += result.value;
return html + "</code></pre>";
}
});
/* override the yt parser */
const original_parser = xbbcode.register.find_parser("yt");
if(original_parser)
xbbcode.register.register_parser({
tag: ["yt", "youtube"],
build_html(layer): string {
const result = original_parser.build_html(layer);
if(!result.startsWith("<iframe")) return result;
const url = result.match(/src="(\S+)" /)[1];
const uid = guid();
sanitizer_escaped_map[uid] = "<iframe class=\"xbbcode-tag xbbcode-tag-video\" src=\"" + url + "\" frameborder=\"0\" allow=\"autoplay; encrypted-media\" allowfullscreen></iframe>";
return sanitizer_escaped(uid);
}
});
/* the image parse & displayer */
xbbcode.register.register_parser({
tag: ["img", "image"],
build_html(layer): string {
const uid = guid();
const fallback_value = "[img]" + layer.build_text() + "[/img]";
let target;
let content = layer.content.map(e => e.build_text()).join("");
if (!layer.options) {
target = content;
} else
target = layer.options;
let url: URL;
try {
url = new URL(target);
if(!url.hostname) throw "";
} catch(error) {
return fallback_value;
}
sanitizer_escaped_map[uid] = "<div class='xbbcode-tag-img'><img src='img/loading_image.svg' onload='messages.formatter.bbcode.load_image(this)' x-image-url='" + encodeURIComponent(target) + "' title='" + sanitize_text(target) + "' /></div>";
return sanitizer_escaped(uid);
}
})
},
priority: 10
});
export interface FormatSettings {
is_chat_message?: boolean
}
export function sanitize_text(text: string) : string {
return $(DOMPurify.sanitize("<a>" + text + "</a>", {
export function format(message: string, fsettings?: FormatSettings) : JQuery[] {
fsettings = fsettings || {};
single_url_parse:
if(fsettings.is_chat_message) {
/* try if its only one url */
const raw_url = message.replace(/\[url(=\S+)?](\S+)\[\/url]/, "$2");
let url: URL;
try {
url = new URL(raw_url);
} catch(error) {
break single_url_parse;
}
single_url_yt:
{
const result = raw_url.match(yt_url_regex);
if(!result) break single_url_yt;
return format("[yt]https://www.youtube.com/watch?v=" + result[5] + "[/yt]");
}
single_url_image:
{
const ext_index = url.pathname.lastIndexOf(".");
if(ext_index == -1) break single_url_image;
const ext_name = url.pathname.substr(ext_index + 1).toLowerCase();
if([
"jpeg", "jpg",
"png", "bmp", "gif",
"tiff", "pdf", "svg"
].findIndex(e => e === ext_name) == -1) break single_url_image;
return format("[img]" + message + "[/img]");
}
}
const result = xbbcode.parse(message, {
tag_whitelist: [
"b", "big",
"i", "italic",
"u", "underlined",
"s", "strikethrough",
"color",
"url",
"code",
"i-code", "icode",
"sub", "sup",
"size",
"hr", "br",
"ul", "ol", "list",
"li",
"table",
"tr", "td", "th",
"yt", "youtube",
"img"
]
});
let html = result.build_html();
if(typeof(window.twemoji) !== "undefined" && settings.static_global(Settings.KEY_CHAT_COLORED_EMOJIES))
html = twemoji.parse(html);
const container = $.spawn("div");
let sanitized = DOMPurify.sanitize(html, {
ADD_ATTR: [
"x-highlight-type",
"x-code-type",
"x-image-url"
]
})).text();
});
sanitized = sanitized.replace(sanitizer_escaped_regex, data => {
const uid = data.match(sanitizer_escaped_regex)[1];
const value = sanitizer_escaped_map[uid];
if(!value) return data;
delete sanitizer_escaped_map[uid];
return value;
});
container[0].innerHTML = sanitized;
container.find("a")
.attr('target', "_blank")
.on('contextmenu', event => {
if(event.isDefaultPrevented()) return;
event.preventDefault();
const url = $(event.target).attr("href");
contextmenu.spawn_context_menu(event.pageX, event.pageY, {
callback: () => {
const win = window.open(url, '_blank');
win.focus();
},
name: tr("Open URL"),
type: contextmenu.MenuEntryType.ENTRY,
icon_class: "client-browse-addon-online"
}, {
callback: () => {
//TODO
},
name: tr("Open URL in Browser"),
type: contextmenu.MenuEntryType.ENTRY,
visible: loader.version().type === "native" && false // Currently not possible
}, contextmenu.Entry.HR(), {
callback: () => copy_to_clipboard(url),
name: tr("Copy URL to clipboard"),
type: contextmenu.MenuEntryType.ENTRY,
icon_class: "client-copy"
});
});
return [container.contents() as JQuery];
//return result.root_tag.content.map(e => e.build_html()).map((entry, idx, array) => $.spawn("a").css("display", (idx == 0 ? "inline" : "") + "block").html(entry == "" && idx != 0 ? "&nbsp;" : entry));
}
export function load_image(entry: HTMLImageElement) {
const url = decodeURIComponent(entry.getAttribute("x-image-url") || "");
const proxy_url = "https://images.weserv.nl/?url=" + encodeURIComponent(url);
entry.onload = undefined;
entry.src = proxy_url;
const parent = $(entry.parentElement);
parent.on('contextmenu', event => {
contextmenu.spawn_context_menu(event.pageX, event.pageY, {
callback: () => {
const win = window.open(url, '_blank');
win.focus();
},
name: tr("Open image in browser"),
type: contextmenu.MenuEntryType.ENTRY,
icon_class: "client-browse-addon-online"
}, contextmenu.Entry.HR(), {
callback: () => copy_to_clipboard(url),
name: tr("Copy image URL to clipboard"),
type: contextmenu.MenuEntryType.ENTRY,
icon_class: "client-copy"
})
});
parent.css("cursor", "pointer").on('click', event => image_preview.preview_image(proxy_url, url));
}
loader.register_task(loader.Stage.JAVASCRIPT_INITIALIZING, {
name: "XBBCode code tag init",
function: async () => {
/* override default parser */
xbbcode.register.register_parser({
tag: ["code", "icode", "i-code"],
content_tags_whitelist: [],
build_html(layer) : string {
const klass = layer.tag_normalized != 'code' ? "tag-hljs-inline-code" : "tag-hljs-code";
const language = (layer.options || "").replace("\"", "'").toLowerCase();
/* remove heading empty lines */
let text = layer.content.map(e => e.build_text())
.reduce((a, b) => a.length == 0 && b.replace(/[ \n\r\t]+/g, "").length == 0 ? "" : a + b, "")
.replace(/^([ \n\r\t]*)(?=\n)+/g, "");
if(text.startsWith("\r") || text.startsWith("\n"))
text = text.substr(1);
let result: HighlightJSResult;
if(window.hljs.getLanguage(language))
result = window.hljs.highlight(language, text, true);
else
result = window.hljs.highlightAuto(text);
let html = '<pre class="' + klass + '">';
html += '<code class="hljs" x-code-type="' + language + '" x-highlight-type="' + result.language + '">';
html += result.value;
return html + "</code></pre>";
}
});
/* override the yt parser */
const original_parser = xbbcode.register.find_parser("yt");
if(original_parser)
xbbcode.register.register_parser({
tag: ["yt", "youtube"],
build_html(layer): string {
const result = original_parser.build_html(layer);
if(!result.startsWith("<iframe")) return result;
const url = result.match(/src="(\S+)" /)[1];
const uid = guid();
sanitizer_escaped_map[uid] = "<iframe class=\"xbbcode-tag xbbcode-tag-video\" src=\"" + url + "\" frameborder=\"0\" allow=\"autoplay; encrypted-media\" allowfullscreen></iframe>";
return sanitizer_escaped(uid);
}
});
/* the image parse & displayer */
xbbcode.register.register_parser({
tag: ["img", "image"],
build_html(layer): string {
const uid = guid();
const fallback_value = "[img]" + layer.build_text() + "[/img]";
let target;
let content = layer.content.map(e => e.build_text()).join("");
if (!layer.options) {
target = content;
} else
target = layer.options;
let url: URL;
try {
url = new URL(target);
if(!url.hostname) throw "";
} catch(error) {
return fallback_value;
}
sanitizer_escaped_map[uid] = "<div class='xbbcode-tag-img'><img src='img/loading_image.svg' onload='messages.formatter.bbcode.load_image(this)' x-image-url='" + encodeURIComponent(target) + "' title='" + sanitize_text(target) + "' /></div>";
return sanitizer_escaped(uid);
}
})
},
priority: 10
});
}
export function sanitize_text(text: string) : string {
return $(DOMPurify.sanitize("<a>" + text + "</a>", {
ADD_ATTR: [
"x-highlight-type",
"x-code-type",
"x-image-url"
]
})).text();
}
export function formatDate(secs: number) : string {
let years = Math.floor(secs / (60 * 60 * 24 * 365));
let days = Math.floor(secs / (60 * 60 * 24)) % 365;
let hours = Math.floor(secs / (60 * 60)) % 24;
let minutes = Math.floor(secs / 60) % 60;
let seconds = Math.floor(secs % 60);
let result = "";
if(years > 0)
result += years + " " + tr("years") + " ";
if(years > 0 || days > 0)
result += days + " " + tr("days") + " ";
if(years > 0 || days > 0 || hours > 0)
result += hours + " " + tr("hours") + " ";
if(years > 0 || days > 0 || hours > 0 || minutes > 0)
result += minutes + " " + tr("minutes") + " ";
if(years > 0 || days > 0 || hours > 0 || minutes > 0 || seconds > 0)
result += seconds + " " + tr("seconds") + " ";
else
result = tr("now") + " ";
return result.substr(0, result.length - 1);
}

View File

@ -1,4 +1,4 @@
enum KeyCode {
export enum KeyCode {
KEY_CANCEL = 3,
KEY_HELP = 6,
KEY_BACK_SPACE = 8,
@ -118,59 +118,57 @@ enum KeyCode {
KEY_META = 224
}
namespace ppt {
export enum EventType {
KEY_PRESS,
KEY_RELEASE,
KEY_TYPED
}
export enum EventType {
KEY_PRESS,
KEY_RELEASE,
KEY_TYPED
}
export enum SpecialKey {
CTRL,
WINDOWS,
SHIFT,
ALT
}
export enum SpecialKey {
CTRL,
WINDOWS,
SHIFT,
ALT
}
export interface KeyDescriptor {
key_code: string;
export interface KeyDescriptor {
key_code: string;
key_ctrl: boolean;
key_windows: boolean;
key_shift: boolean;
key_alt: boolean;
}
key_ctrl: boolean;
key_windows: boolean;
key_shift: boolean;
key_alt: boolean;
}
export interface KeyEvent extends KeyDescriptor {
readonly type: EventType;
export interface KeyEvent extends KeyDescriptor {
readonly type: EventType;
readonly key: string;
}
readonly key: string;
}
export interface KeyHook extends KeyDescriptor {
cancel: boolean;
export interface KeyHook extends KeyDescriptor {
cancel: boolean;
callback_press: () => any;
callback_release: () => any;
}
callback_press: () => any;
callback_release: () => any;
}
export function key_description(key: KeyDescriptor) {
let result = "";
if(key.key_shift)
result += " + " + tr("Shift");
if(key.key_alt)
result += " + " + tr("Alt");
if(key.key_ctrl)
result += " + " + tr("CTRL");
if(key.key_windows)
result += " + " + tr("Win");
export function key_description(key: KeyDescriptor) {
let result = "";
if(key.key_shift)
result += " + " + tr("Shift");
if(key.key_alt)
result += " + " + tr("Alt");
if(key.key_ctrl)
result += " + " + tr("CTRL");
if(key.key_windows)
result += " + " + tr("Win");
if(!result && !key.key_code)
return tr("unset");
if(!result && !key.key_code)
return tr("unset");
if(key.key_code)
result += " + " + key.key_code;
return result.substr(3);
}
if(key.key_code)
result += " + " + key.key_code;
return result.substr(3);
}

View File

@ -1,10 +0,0 @@
namespace audio {
export namespace player {
export interface Device {
device_id: string;
driver: string;
name: string;
}
}
}

View File

@ -0,0 +1,6 @@
export interface Device {
device_id: string;
driver: string;
name: string;
}

View File

@ -1,262 +1,260 @@
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();
import * as log from "tc-shared/log";
import {LogCategory} from "tc-shared/log";
import {guid} from "tc-shared/crypto/uid";
import {createErrorModal, createInfoModal, createInputModal} from "tc-shared/ui/elements/Modal";
import {default_profile, find_profile} from "tc-shared/profiles/ConnectionProfile";
import {server_connections} from "tc-shared/ui/frames/connection_handlers";
import {spawnConnectModal} from "tc-shared/ui/modal/ModalConnect";
import {control_bar} from "tc-shared/ui/frames/ControlBar";
import * as top_menu from "./ui/frames/MenuBar";
export const boorkmak_connect = (mark: Bookmark, new_tab?: boolean) => {
const profile = find_profile(mark.connect_profile) || default_profile();
if(profile.valid()) {
const connection = (typeof(new_tab) !== "boolean" || !new_tab) ? server_connections.active_connection_handler() : server_connections.spawn_server_connection_handler();
server_connections.set_active_connection_handler(connection);
connection.startConnection(
mark.server_properties.server_address + ":" + mark.server_properties.server_port,
profile,
true,
{
nickname: mark.nickname === "Another TeaSpeak user" || !mark.nickname ? profile.connect_username() : mark.nickname,
password: mark.server_properties.server_password_hash ? {
password: mark.server_properties.server_password_hash,
hashed: true
} : mark.server_properties.server_password ? {
hashed: false,
password: mark.server_properties.server_password
} : undefined
}
);
} else {
spawnConnectModal({}, {
url: mark.server_properties.server_address + ":" + mark.server_properties.server_port,
enforce: true
}, {
profile: profile,
enforce: true
})
}
};
export const boorkmak_connect = (mark: Bookmark, new_tab?: boolean) => {
const profile = profiles.find_profile(mark.connect_profile) || profiles.default_profile();
if(profile.valid()) {
const connection = (typeof(new_tab) !== "boolean" || !new_tab) ? server_connections.active_connection_handler() : server_connections.spawn_server_connection_handler();
server_connections.set_active_connection_handler(connection);
connection.startConnection(
mark.server_properties.server_address + ":" + mark.server_properties.server_port,
profile,
true,
{
nickname: mark.nickname === "Another TeaSpeak user" || !mark.nickname ? profile.connect_username() : mark.nickname,
password: mark.server_properties.server_password_hash ? {
password: mark.server_properties.server_password_hash,
hashed: true
} : mark.server_properties.server_password ? {
hashed: false,
password: mark.server_properties.server_password
} : undefined
}
);
} else {
Modals.spawnConnectModal({}, {
url: mark.server_properties.server_address + ":" + mark.server_properties.server_port,
enforce: true
}, {
profile: profile,
enforce: true
})
}
};
export interface ServerProperties {
server_address: string;
server_port: number;
server_password_hash?: string;
server_password?: string;
}
export interface ServerProperties {
server_address: string;
server_port: number;
server_password_hash?: string;
server_password?: string;
}
export enum BookmarkType {
ENTRY,
DIRECTORY
}
export enum BookmarkType {
ENTRY,
DIRECTORY
}
export interface Bookmark {
type: /* BookmarkType.ENTRY */ BookmarkType;
/* readonly */ parent: DirectoryBookmark;
export interface Bookmark {
type: /* BookmarkType.ENTRY */ BookmarkType;
/* readonly */ parent: DirectoryBookmark;
server_properties: ServerProperties;
display_name: string;
unique_id: string;
server_properties: ServerProperties;
display_name: string;
unique_id: string;
nickname: string;
default_channel?: number | string;
default_channel_password_hash?: string;
default_channel_password?: string;
nickname: string;
default_channel?: number | string;
default_channel_password_hash?: string;
default_channel_password?: string;
connect_profile: string;
connect_profile: string;
last_icon_id?: number;
}
last_icon_id?: number;
}
export interface DirectoryBookmark {
type: /* BookmarkType.DIRECTORY */ BookmarkType;
/* readonly */ parent: DirectoryBookmark;
export interface DirectoryBookmark {
type: /* BookmarkType.DIRECTORY */ BookmarkType;
/* readonly */ parent: DirectoryBookmark;
readonly content: (Bookmark | DirectoryBookmark)[];
unique_id: string;
display_name: string;
}
readonly content: (Bookmark | DirectoryBookmark)[];
unique_id: string;
display_name: string;
}
interface BookmarkConfig {
root_bookmark?: DirectoryBookmark;
default_added?: boolean;
}
interface BookmarkConfig {
root_bookmark?: DirectoryBookmark;
default_added?: boolean;
}
let _bookmark_config: BookmarkConfig;
function bookmark_config() : BookmarkConfig {
if(_bookmark_config)
return _bookmark_config;
let bookmark_json = localStorage.getItem("bookmarks");
let bookmarks;
try {
bookmarks = JSON.parse(bookmark_json) || {} as BookmarkConfig;
} catch(error) {
log.error(LogCategory.BOOKMARKS, tr("Failed to load bookmarks: %o"), error);
bookmarks = {} as any;
}
_bookmark_config = bookmarks;
_bookmark_config.root_bookmark = _bookmark_config.root_bookmark || { content: [], display_name: "root", type: BookmarkType.DIRECTORY} as DirectoryBookmark;
if(!_bookmark_config.default_added) {
_bookmark_config.default_added = true;
create_bookmark("TeaSpeak official Test-Server", _bookmark_config.root_bookmark, {
server_address: "ts.teaspeak.de",
server_port: 9987
}, undefined);
save_config();
}
const fix_parent = (parent: DirectoryBookmark, entry: Bookmark | DirectoryBookmark) => {
entry.parent = parent;
if(entry.type === BookmarkType.DIRECTORY)
for(const child of (entry as DirectoryBookmark).content)
fix_parent(entry as DirectoryBookmark, child);
};
for(const entry of _bookmark_config.root_bookmark.content)
fix_parent(_bookmark_config.root_bookmark, entry);
let _bookmark_config: BookmarkConfig;
function bookmark_config() : BookmarkConfig {
if(_bookmark_config)
return _bookmark_config;
let bookmark_json = localStorage.getItem("bookmarks");
let bookmarks;
try {
bookmarks = JSON.parse(bookmark_json) || {} as BookmarkConfig;
} catch(error) {
log.error(LogCategory.BOOKMARKS, tr("Failed to load bookmarks: %o"), error);
bookmarks = {} as any;
}
function save_config() {
localStorage.setItem("bookmarks", JSON.stringify(bookmark_config(), (key, value) => {
if(key === "parent")
return undefined;
return value;
}));
_bookmark_config = bookmarks;
_bookmark_config.root_bookmark = _bookmark_config.root_bookmark || { content: [], display_name: "root", type: BookmarkType.DIRECTORY} as DirectoryBookmark;
if(!_bookmark_config.default_added) {
_bookmark_config.default_added = true;
create_bookmark("TeaSpeak official Test-Server", _bookmark_config.root_bookmark, {
server_address: "ts.teaspeak.de",
server_port: 9987
}, undefined);
save_config();
}
export function bookmarks() : DirectoryBookmark {
return bookmark_config().root_bookmark;
}
const fix_parent = (parent: DirectoryBookmark, entry: Bookmark | DirectoryBookmark) => {
entry.parent = parent;
if(entry.type === BookmarkType.DIRECTORY)
for(const child of (entry as DirectoryBookmark).content)
fix_parent(entry as DirectoryBookmark, child);
};
for(const entry of _bookmark_config.root_bookmark.content)
fix_parent(_bookmark_config.root_bookmark, entry);
export function bookmarks_flat() : Bookmark[] {
const result: Bookmark[] = [];
const _flat = (bookmark: Bookmark | DirectoryBookmark) => {
if(bookmark.type == BookmarkType.DIRECTORY)
for(const book of (bookmark as DirectoryBookmark).content)
_flat(book);
else
result.push(bookmark as Bookmark);
};
_flat(bookmark_config().root_bookmark);
return result;
}
return _bookmark_config;
}
function find_bookmark_recursive(parent: DirectoryBookmark, uuid: string) : Bookmark | DirectoryBookmark {
for(const entry of parent.content) {
if(entry.unique_id == uuid)
return entry;
if(entry.type == BookmarkType.DIRECTORY) {
const result = find_bookmark_recursive(entry as DirectoryBookmark, uuid);
if(result) return result;
}
}
return undefined;
}
function save_config() {
localStorage.setItem("bookmarks", JSON.stringify(bookmark_config(), (key, value) => {
if(key === "parent")
return undefined;
return value;
}));
}
export function find_bookmark(uuid: string) : Bookmark | DirectoryBookmark | undefined {
return find_bookmark_recursive(bookmarks(), uuid);
}
export function bookmarks() : DirectoryBookmark {
return bookmark_config().root_bookmark;
}
export function parent_bookmark(bookmark: Bookmark) : DirectoryBookmark {
const books: (DirectoryBookmark | Bookmark)[] = [bookmarks()];
while(!books.length) {
const directory = books.pop_front();
if(directory.type == BookmarkType.DIRECTORY) {
const cast = <DirectoryBookmark>directory;
if(cast.content.indexOf(bookmark) != -1)
return cast;
books.push(...cast.content);
}
}
return bookmarks();
}
export function create_bookmark(display_name: string, directory: DirectoryBookmark, server_properties: ServerProperties, nickname: string) : Bookmark {
const bookmark = {
display_name: display_name,
server_properties: server_properties,
nickname: nickname,
type: BookmarkType.ENTRY,
connect_profile: "default",
unique_id: guid(),
parent: directory
} as Bookmark;
directory.content.push(bookmark);
return bookmark;
}
export function create_bookmark_directory(parent: DirectoryBookmark, name: string) : DirectoryBookmark {
const bookmark = {
type: BookmarkType.DIRECTORY,
display_name: name,
content: [],
unique_id: guid(),
parent: parent
} as DirectoryBookmark;
parent.content.push(bookmark);
return bookmark;
}
//TODO test if the new parent is within the old bookmark
export function change_directory(parent: DirectoryBookmark, bookmark: Bookmark | DirectoryBookmark) {
delete_bookmark(bookmark);
parent.content.push(bookmark)
}
export function save_bookmark(bookmark?: Bookmark | DirectoryBookmark) {
save_config(); /* nvm we dont give a fuck... saving everything */
}
function delete_bookmark_recursive(parent: DirectoryBookmark, bookmark: Bookmark | DirectoryBookmark) {
const index = parent.content.indexOf(bookmark);
if(index != -1)
parent.content.remove(bookmark);
export function bookmarks_flat() : Bookmark[] {
const result: Bookmark[] = [];
const _flat = (bookmark: Bookmark | DirectoryBookmark) => {
if(bookmark.type == BookmarkType.DIRECTORY)
for(const book of (bookmark as DirectoryBookmark).content)
_flat(book);
else
for(const entry of parent.content)
if(entry.type == BookmarkType.DIRECTORY)
delete_bookmark_recursive(entry as DirectoryBookmark, bookmark)
}
result.push(bookmark as Bookmark);
};
_flat(bookmark_config().root_bookmark);
return result;
}
export function delete_bookmark(bookmark: Bookmark | DirectoryBookmark) {
delete_bookmark_recursive(bookmarks(), bookmark)
}
export function add_current_server() {
const ch = server_connections.active_connection_handler();
if(ch && ch.connected) {
const ce = ch.getClient();
const name = ce ? ce.clientNickName() : undefined;
createInputModal(tr("Enter bookmarks name"), tr("Please enter the bookmarks name:<br>"), text => text.length > 0, result => {
if(result) {
const bookmark = create_bookmark(result as string, bookmarks(), {
server_port: ch.serverConnection.remote_address().port,
server_address: ch.serverConnection.remote_address().host,
server_password: "",
server_password_hash: ""
}, name);
save_bookmark(bookmark);
control_bar.update_bookmarks();
top_menu.rebuild_bookmarks();
createInfoModal(tr("Server added"), tr("Server has been successfully added to your bookmarks.")).open();
}
}).open();
} else {
createErrorModal(tr("You have to be connected"), tr("You have to be connected!")).open();
function find_bookmark_recursive(parent: DirectoryBookmark, uuid: string) : Bookmark | DirectoryBookmark {
for(const entry of parent.content) {
if(entry.unique_id == uuid)
return entry;
if(entry.type == BookmarkType.DIRECTORY) {
const result = find_bookmark_recursive(entry as DirectoryBookmark, uuid);
if(result) return result;
}
}
return undefined;
}
export function find_bookmark(uuid: string) : Bookmark | DirectoryBookmark | undefined {
return find_bookmark_recursive(bookmarks(), uuid);
}
export function parent_bookmark(bookmark: Bookmark) : DirectoryBookmark {
const books: (DirectoryBookmark | Bookmark)[] = [bookmarks()];
while(!books.length) {
const directory = books.pop_front();
if(directory.type == BookmarkType.DIRECTORY) {
const cast = <DirectoryBookmark>directory;
if(cast.content.indexOf(bookmark) != -1)
return cast;
books.push(...cast.content);
}
}
return bookmarks();
}
export function create_bookmark(display_name: string, directory: DirectoryBookmark, server_properties: ServerProperties, nickname: string) : Bookmark {
const bookmark = {
display_name: display_name,
server_properties: server_properties,
nickname: nickname,
type: BookmarkType.ENTRY,
connect_profile: "default",
unique_id: guid(),
parent: directory
} as Bookmark;
directory.content.push(bookmark);
return bookmark;
}
export function create_bookmark_directory(parent: DirectoryBookmark, name: string) : DirectoryBookmark {
const bookmark = {
type: BookmarkType.DIRECTORY,
display_name: name,
content: [],
unique_id: guid(),
parent: parent
} as DirectoryBookmark;
parent.content.push(bookmark);
return bookmark;
}
//TODO test if the new parent is within the old bookmark
export function change_directory(parent: DirectoryBookmark, bookmark: Bookmark | DirectoryBookmark) {
delete_bookmark(bookmark);
parent.content.push(bookmark)
}
export function save_bookmark(bookmark?: Bookmark | DirectoryBookmark) {
save_config(); /* nvm we dont give a fuck... saving everything */
}
function delete_bookmark_recursive(parent: DirectoryBookmark, bookmark: Bookmark | DirectoryBookmark) {
const index = parent.content.indexOf(bookmark);
if(index != -1)
parent.content.remove(bookmark);
else
for(const entry of parent.content)
if(entry.type == BookmarkType.DIRECTORY)
delete_bookmark_recursive(entry as DirectoryBookmark, bookmark)
}
export function delete_bookmark(bookmark: Bookmark | DirectoryBookmark) {
delete_bookmark_recursive(bookmarks(), bookmark)
}
export function add_current_server() {
const ch = server_connections.active_connection_handler();
if(ch && ch.connected) {
const ce = ch.getClient();
const name = ce ? ce.clientNickName() : undefined;
createInputModal(tr("Enter bookmarks name"), tr("Please enter the bookmarks name:<br>"), text => text.length > 0, result => {
if(result) {
const bookmark = create_bookmark(result as string, bookmarks(), {
server_port: ch.serverConnection.remote_address().port,
server_address: ch.serverConnection.remote_address().host,
server_password: "",
server_password_hash: ""
}, name);
save_bookmark(bookmark);
control_bar.update_bookmarks();
top_menu.rebuild_bookmarks();
createInfoModal(tr("Server added"), tr("Server has been successfully added to your bookmarks.")).open();
}
}).open();
} else {
createErrorModal(tr("You have to be connected"), tr("You have to be connected!")).open();
}
}

View File

@ -0,0 +1,98 @@
import {
AbstractServerConnection,
ServerCommand,
SingleCommandHandler
} from "tc-shared/connection/ConnectionBase";
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;
}
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;
}
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";
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;
}
this.command_handlers.remove(handler);
handler.handler_boss = undefined;
}
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);
}
}
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;
}
}

File diff suppressed because it is too large Load Diff

View File

@ -1,448 +1,461 @@
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)[]} = {};
import {ServerCommand, SingleCommandHandler} from "tc-shared/connection/ConnectionBase";
import * as log from "tc-shared/log";
import {LogCategory} from "tc-shared/log";
import {
ClientNameInfo,
CommandResult,
ErrorID, Playlist, PlaylistInfo, PlaylistSong,
QueryList,
QueryListEntry, ServerGroupClient
} from "tc-shared/connection/ServerConnectionDeclaration";
import {ChannelEntry} from "tc-shared/ui/channel";
import {ClientEntry} from "tc-shared/ui/client";
import {ChatType} from "tc-shared/ui/frames/chat";
import {AbstractCommandHandler} from "tc-shared/connection/AbstractCommandHandler";
constructor(connection) {
super(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)[]} = {};
this.volatile_handler_boss = false;
this.ignore_consumed = true;
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<CommandResult> {
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<CommandResult> {
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<CommandResult> {
let data = {};
data[key] = value;
return this.connection.send_command("clientupdate", data);
}
async info_from_uid(..._unique_ids: string[]) : Promise<ClientNameInfo[]> {
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));
}
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);
try {
await this.connection.send_command("clientgetnamefromuid", request);
} catch(error) {
if(error instanceof CommandResult && error.id == ErrorID.EMPTY_RESULT) {
/* nothing */
} else {
throw error;
}
this._awaiters_unique_ids = undefined;
} finally {
/* cleanup */
for(const unique_id of Object.keys(unique_id_resolvers))
(this._awaiters_unique_ids[unique_id] || []).remove(unique_id_resolvers[unique_id]);
}
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;
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<ClientNameInfo[]> {
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));
}
joinChannel(channel: ChannelEntry, password?: string) : Promise<CommandResult> {
return this.connection.send_command("clientmove", {
"clid": this.connection.client.getClientId(),
"cid": channel.getChannelId(),
"cpw": password || ""
});
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]);
}
sendMessage(message: string, type: ChatType, target?: ChannelEntry | ClientEntry) : Promise<CommandResult> {
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});
}
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<QueryList> {
return new Promise<QueryList>((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<CommandResult> {
let data = {};
data[key] = value;
return this.connection.send_command("clientupdate", data);
}
if(server_id !== undefined)
data["server_id"] = server_id;
async info_from_uid(..._unique_ids: string[]) : Promise<ClientNameInfo[]> {
const response: ClientNameInfo[] = [];
const request = [];
const unique_ids = new Set(_unique_ids);
if(!unique_ids.size) return [];
this.connection.send_command("querylist", data).catch(error => {
this.handler_boss.remove_single_handler(single_handler);
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));
}
try {
await this.connection.send_command("clientgetnamefromuid", request);
} catch(error) {
if(error instanceof CommandResult && error.id == ErrorID.EMPTY_RESULT) {
/* nothing */
} else {
throw error;
if(error instanceof CommandResult) {
if(error.id == ErrorID.EMPTY_RESULT) {
resolve(undefined);
return;
}
}
} finally {
/* cleanup */
for(const unique_id of Object.keys(unique_id_resolvers))
(this._awaiters_unique_ids[unique_id] || []).remove(unique_id_resolvers[unique_id]);
}
reject(error);
});
});
}
return response;
}
request_playlist_list() : Promise<Playlist[]> {
return new Promise((resolve, reject) => {
const single_handler: SingleCommandHandler = {
command: "notifyplaylistlist",
function: command => {
const json = command.arguments;
const result: Playlist[] = [];
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"])
};
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"],
const functions = this._awaiters_unique_dbid[info.client_database_id] || [];
delete this._awaiters_unique_dbid[info.client_database_id];
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);
}
}
for(const fn of functions)
fn(info);
}
}
async info_from_cldbid(..._cldbid: number[]) : Promise<ClientNameInfo[]> {
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;
resolve(result);
return true;
}
} finally {
/* cleanup */
for(const cldbid of Object.keys(unique_cldbid_resolvers))
(this._awaiters_unique_dbid[cldbid] || []).remove(unique_cldbid_resolvers[cldbid]);
}
};
this.handler_boss.register_single_handler(single_handler);
return response;
}
this.connection.send_command("playlistlist").catch(error => {
this.handler_boss.remove_single_handler(single_handler);
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<QueryList> {
return new Promise<QueryList>((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);
let data = {};
if(server_id !== undefined)
data["server_id"] = server_id;
this.connection.send_command("querylist", data).catch(error => {
this.handler_boss.remove_single_handler(single_handler);
if(error instanceof CommandResult) {
if(error.id == ErrorID.EMPTY_RESULT) {
resolve(undefined);
return;
}
}
reject(error);
});
});
}
request_playlist_list() : Promise<Playlist[]> {
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<PlaylistSong[]> {
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<number[]> {
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) {
if(error instanceof CommandResult) {
if(error.id == ErrorID.EMPTY_RESULT) {
resolve([]);
return;
}
reject(error);
})
});
}
}
reject(error);
})
});
}
async request_clients_by_server_group(group_id: number) : Promise<ServerGroupClient[]> {
//servergroupclientlist sgid=2
//notifyservergroupclientlist sgid=6 cldbid=2 client_nickname=WolverinDEV client_unique_identifier=xxjnc14LmvTk+Lyrm8OOeo4tOqw=
return new Promise<ServerGroupClient[]>((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;
}
request_playlist_songs(playlist_id: number) : Promise<PlaylistSong[]> {
return new Promise((resolve, reject) => {
const single_handler: SingleCommandHandler = {
command: "notifyplaylistsonglist",
function: command => {
const json = command.arguments;
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;
if(json[0]["playlist_id"] != playlist_id) {
log.error(LogCategory.NETWORKING, tr("Received invalid notification for playlist songs"));
return false;
}
};
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<PlaylistInfo> {
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;
}
const result: PlaylistSong[] = [];
for(const entry of json) {
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"]),
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"],
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"])
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 info: %o"), error);
reject("failed to parse info");
} catch(error) {
log.error(LogCategory.NETWORKING, tr("Failed to parse playlist song entry: %o"), error);
}
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);
})
});
}
resolve(result);
return true;
}
};
this.handler_boss.register_single_handler(single_handler);
/**
* @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<number> {
if(this._who_am_i)
return Promise.resolve(parseInt(this._who_am_i["virtualserver_id"]));
return new Promise<number>((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.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;
}
};
this.handler_boss.register_single_handler(single_handler);
}
reject(error);
})
});
}
this.connection.send_command("whoami").catch(error => {
this.handler_boss.remove_single_handler(single_handler);
reject(error);
});
request_playlist_client_list(playlist_id: number) : Promise<number[]> {
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;
}
reject(error);
})
});
}
async request_clients_by_server_group(group_id: number) : Promise<ServerGroupClient[]> {
//servergroupclientlist sgid=2
//notifyservergroupclientlist sgid=6 cldbid=2 client_nickname=WolverinDEV client_unique_identifier=xxjnc14LmvTk+Lyrm8OOeo4tOqw=
return new Promise<ServerGroupClient[]>((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<PlaylistInfo> {
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<number> {
if(this._who_am_i)
return Promise.resolve(parseInt(this._who_am_i["virtualserver_id"]));
return new Promise<number>((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);
});
}
});
}
}

View File

@ -1,216 +1,129 @@
namespace connection {
export interface CommandOptions {
flagset?: string[]; /* default: [] */
process_result?: boolean; /* default: true */
import {CommandHelper} from "tc-shared/connection/CommandHelper";
import {HandshakeHandler} from "tc-shared/connection/HandshakeHandler";
import {CommandResult} from "tc-shared/connection/ServerConnectionDeclaration";
import {ServerAddress} from "tc-shared/ui/server";
import {RecorderProfile} from "tc-shared/voice/RecorderProfile";
import {ConnectionHandler, ConnectionState} from "tc-shared/ConnectionHandler";
import {AbstractCommandHandlerBoss} from "tc-shared/connection/AbstractCommandHandler";
timeout?: number /* default: 1000 */;
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);
}
export const CommandOptionDefaults: CommandOptions = {
flagset: [],
process_result: true,
timeout: 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<void>;
abstract connected() : boolean;
abstract disconnect(reason?: string) : Promise<void>;
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<CommandResult>;
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 ConnectionStateListener = (old_state: ConnectionState, new_state: ConnectionState) => any;
export abstract class AbstractServerConnection {
readonly client: ConnectionHandler;
readonly command_helper: CommandHelper;
export namespace voice {
export enum PlayerState {
PREBUFFERING,
PLAYING,
BUFFERING,
STOPPING,
STOPPED
}
protected constructor(client: ConnectionHandler) {
this.client = client;
export type LatencySettings = {
min_buffer: number; /* milliseconds */
max_buffer: number; /* milliseconds */
}
this.command_helper = new CommandHelper(this);
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;
}
/* 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<void>;
abstract connected() : boolean;
abstract disconnect(reason?: string) : Promise<void>;
abstract encoding_supported(codec: number) : boolean;
abstract decoding_supported(codec: number) : boolean;
abstract support_voice() : boolean;
abstract voice_connection() : voice.AbstractVoiceConnection | undefined;
abstract register_client(client_id: number) : VoiceClient;
abstract available_clients() : VoiceClient[];
abstract unregister_client(client: VoiceClient) : Promise<void>;
abstract command_handler_boss() : AbstractCommandHandlerBoss;
abstract send_command(command: string, data?: any | any[], options?: CommandOptions) : Promise<CommandResult>;
abstract voice_recorder() : RecorderProfile;
abstract acquire_voice_recorder(recorder: RecorderProfile | undefined) : Promise<void>;
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
};
abstract get_encoder_codec() : number;
abstract set_encoder_codec(codec: number);
}
}
export namespace voice {
export enum PlayerState {
PREBUFFERING,
PLAYING,
BUFFERING,
STOPPING,
STOPPED
}
export class ServerCommand {
command: string;
arguments: any[];
}
export type LatencySettings = {
min_buffer: number; /* milliseconds */
max_buffer: number; /* milliseconds */
}
export interface SingleCommandHandler {
name?: string;
command?: string;
timeout?: 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 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<void>;
abstract voice_recorder() : RecorderProfile;
abstract acquire_voice_recorder(recorder: RecorderProfile | undefined) : Promise<void>;
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;
}
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;
}
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";
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;
}
this.command_handlers.remove(handler);
handler.handler_boss = undefined;
}
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);
}
}
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;
}
}
/* if the return is true then the command handler will be removed */
function: (command: ServerCommand) => boolean;
}

View File

@ -1,146 +1,153 @@
namespace connection {
export interface HandshakeIdentityHandler {
connection: AbstractServerConnection;
import {CommandResult} from "tc-shared/connection/ServerConnectionDeclaration";
import {IdentitifyType} from "tc-shared/profiles/Identity";
import {TeaSpeakIdentity} from "tc-shared/profiles/identities/TeamSpeakIdentity";
import {AbstractServerConnection} from "tc-shared/connection/ConnectionBase";
import {ConnectionProfile} from "tc-shared/profiles/ConnectionProfile";
import {settings} from "tc-shared/settings";
import {ConnectParameters, DisconnectReason} from "tc-shared/ConnectionHandler";
start_handshake();
register_callback(callback: (success: boolean, message?: string) => any);
export interface HandshakeIdentityHandler {
connection: AbstractServerConnection;
start_handshake();
register_callback(callback: (success: boolean, message?: string) => any);
}
declare const native_client;
export class HandshakeHandler {
private connection: AbstractServerConnection;
private handshake_handler: HandshakeIdentityHandler;
private failed = false;
readonly profile: ConnectionProfile;
readonly parameters: ConnectParameters;
constructor(profile: ConnectionProfile, parameters: ConnectParameters) {
this.profile = profile;
this.parameters = parameters;
}
export class HandshakeHandler {
private connection: AbstractServerConnection;
private handshake_handler: HandshakeIdentityHandler;
private failed = false;
setConnection(con: AbstractServerConnection) {
this.connection = con;
}
readonly profile: profiles.ConnectionProfile;
readonly parameters: ConnectParameters;
constructor(profile: profiles.ConnectionProfile, parameters: ConnectParameters) {
this.profile = profile;
this.parameters = parameters;
initialize() {
this.handshake_handler = this.profile.spawn_identity_handshake_handler(this.connection);
if(!this.handshake_handler) {
this.handshake_failed("failed to create identity handler");
return;
}
setConnection(con: AbstractServerConnection) {
this.connection = con;
}
initialize() {
this.handshake_handler = this.profile.spawn_identity_handshake_handler(this.connection);
if(!this.handshake_handler) {
this.handshake_failed("failed to create identity handler");
return;
}
this.handshake_handler.register_callback((flag, message) => {
if(flag)
this.handshake_finished();
else
this.handshake_failed(message);
});
}
get_identity_handler() : HandshakeIdentityHandler {
return this.handshake_handler;
}
startHandshake() {
this.handshake_handler.start_handshake();
}
on_teamspeak() {
const type = this.profile.selected_type();
if(type == profiles.identities.IdentitifyType.TEAMSPEAK)
this.handshake_handler.register_callback((flag, message) => {
if(flag)
this.handshake_finished();
else {
else
this.handshake_failed(message);
});
}
if(this.failed) return;
get_identity_handler() : HandshakeIdentityHandler {
return this.handshake_handler;
}
this.failed = true;
this.connection.client.handleDisconnect(DisconnectReason.HANDSHAKE_TEAMSPEAK_REQUIRED);
}
}
startHandshake() {
this.handshake_handler.start_handshake();
}
on_teamspeak() {
const type = this.profile.selected_type();
if(type == IdentitifyType.TEAMSPEAK)
this.handshake_finished();
else {
private handshake_failed(message: string) {
if(this.failed) return;
this.failed = true;
this.connection.client.handleDisconnect(DisconnectReason.HANDSHAKE_FAILED, message);
}
private handshake_finished(version?: string) {
const _native = window["native"];
if(native_client && _native && _native.client_version && !version) {
_native.client_version()
.then( this.handshake_finished.bind(this))
.catch(error => {
console.error(tr("Failed to get version:"));
console.error(error);
this.handshake_finished("?.?.?");
});
return;
}
const git_version = settings.static_global("version", "unknown");
const browser_name = (navigator.browserSpecs || {})["name"] || " ";
let data = {
client_nickname: this.parameters.nickname || "Another TeaSpeak user",
client_platform: (browser_name ? browser_name + " " : "") + navigator.platform,
client_version: "TeaWeb " + git_version + " (" + navigator.userAgent + ")",
client_version_sign: undefined,
client_default_channel: (this.parameters.channel || {} as any).target,
client_default_channel_password: (this.parameters.channel || {} as any).password,
client_default_token: this.parameters.token,
client_server_password: this.parameters.password ? this.parameters.password.password : undefined,
client_browser_engine: navigator.product,
client_input_hardware: this.connection.client.client_status.input_hardware,
client_output_hardware: false,
client_input_muted: this.connection.client.client_status.input_muted,
client_output_muted: this.connection.client.client_status.output_muted,
};
//0.0.1 [Build: 1549713549] Linux 7XvKmrk7uid2ixHFeERGqcC8vupeQqDypLtw2lY9slDNPojEv//F47UaDLG+TmVk4r6S0TseIKefzBpiRtLDAQ==
if(version) {
data.client_version = "TeaClient ";
data.client_version += " " + version;
const os = require("os");
const arch_mapping = {
"x32": "32bit",
"x64": "64bit"
};
data.client_version += " " + (arch_mapping[os.arch()] || os.arch());
const os_mapping = {
"win32": "Windows",
"linux": "Linux"
};
data.client_platform = (os_mapping[os.platform()] || os.platform());
}
/* required to keep compatibility */
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 => {
if(error instanceof CommandResult) {
if(error.id == 1028) {
this.connection.client.handleDisconnect(DisconnectReason.SERVER_REQUIRES_PASSWORD);
} else if(error.id == 783 || error.id == 519) {
error.extra_message = isNaN(parseInt(error.extra_message)) ? "8" : error.extra_message;
this.connection.client.handleDisconnect(DisconnectReason.IDENTITY_TOO_LOW, error);
} else if(error.id == 3329) {
this.connection.client.handleDisconnect(DisconnectReason.HANDSHAKE_BANNED, error);
} else {
this.connection.client.handleDisconnect(DisconnectReason.CLIENT_KICKED, error);
}
} else
this.connection.disconnect();
});
this.connection.client.handleDisconnect(DisconnectReason.HANDSHAKE_TEAMSPEAK_REQUIRED);
}
}
private handshake_failed(message: string) {
if(this.failed) return;
this.failed = true;
this.connection.client.handleDisconnect(DisconnectReason.HANDSHAKE_FAILED, message);
}
private handshake_finished(version?: string) {
const _native = window["native"];
if(native_client && _native && _native.client_version && !version) {
_native.client_version()
.then( this.handshake_finished.bind(this))
.catch(error => {
console.error(tr("Failed to get version:"));
console.error(error);
this.handshake_finished("?.?.?");
});
return;
}
const git_version = settings.static_global("version", "unknown");
const browser_name = (navigator.browserSpecs || {})["name"] || " ";
let data = {
client_nickname: this.parameters.nickname || "Another TeaSpeak user",
client_platform: (browser_name ? browser_name + " " : "") + navigator.platform,
client_version: "TeaWeb " + git_version + " (" + navigator.userAgent + ")",
client_version_sign: undefined,
client_default_channel: (this.parameters.channel || {} as any).target,
client_default_channel_password: (this.parameters.channel || {} as any).password,
client_default_token: this.parameters.token,
client_server_password: this.parameters.password ? this.parameters.password.password : undefined,
client_browser_engine: navigator.product,
client_input_hardware: this.connection.client.client_status.input_hardware,
client_output_hardware: false,
client_input_muted: this.connection.client.client_status.input_muted,
client_output_muted: this.connection.client.client_status.output_muted,
};
//0.0.1 [Build: 1549713549] Linux 7XvKmrk7uid2ixHFeERGqcC8vupeQqDypLtw2lY9slDNPojEv//F47UaDLG+TmVk4r6S0TseIKefzBpiRtLDAQ==
if(version) {
data.client_version = "TeaClient ";
data.client_version += " " + version;
const os = require("os");
const arch_mapping = {
"x32": "32bit",
"x64": "64bit"
};
data.client_version += " " + (arch_mapping[os.arch()] || os.arch());
const os_mapping = {
"win32": "Windows",
"linux": "Linux"
};
data.client_platform = (os_mapping[os.platform()] || os.platform());
}
/* required to keep compatibility */
if(this.profile.selected_type() === IdentitifyType.TEAMSPEAK) {
data["client_key_offset"] = (this.profile.selected_identity() as TeaSpeakIdentity).hash_number;
}
this.connection.send_command("clientinit", data).catch(error => {
if(error instanceof CommandResult) {
if(error.id == 1028) {
this.connection.client.handleDisconnect(DisconnectReason.SERVER_REQUIRES_PASSWORD);
} else if(error.id == 783 || error.id == 519) {
error.extra_message = isNaN(parseInt(error.extra_message)) ? "8" : error.extra_message;
this.connection.client.handleDisconnect(DisconnectReason.IDENTITY_TOO_LOW, error);
} else if(error.id == 3329) {
this.connection.client.handleDisconnect(DisconnectReason.HANDSHAKE_BANNED, error);
} else {
this.connection.client.handleDisconnect(DisconnectReason.CLIENT_KICKED, error);
}
} else
this.connection.disconnect();
});
}
}

View File

@ -1,4 +1,6 @@
enum ErrorID {
import {LaterPromise} from "tc-shared/utils/LaterPromise";
export enum ErrorID {
NOT_IMPLEMENTED = 0x2,
COMMAND_NOT_FOUND = 0x100,
@ -15,7 +17,7 @@ enum ErrorID {
CONVERSATION_IS_PRIVATE = 0x2202
}
class CommandResult {
export class CommandResult {
success: boolean;
id: number;
message: string;
@ -35,39 +37,39 @@ class CommandResult {
}
}
interface ClientNameInfo {
export interface ClientNameInfo {
//cluid=tYzKUryn\/\/Y8VBMf8PHUT6B1eiE= name=Exp clname=Exp cldbid=9
client_unique_id: string;
client_nickname: string;
client_database_id: number;
}
interface ClientNameFromUid {
export interface ClientNameFromUid {
promise: LaterPromise<ClientNameInfo[]>,
keys: string[],
response: ClientNameInfo[]
}
interface ServerGroupClient {
export interface ServerGroupClient {
client_nickname: string;
client_unique_identifier: string;
client_database_id: number;
}
interface QueryListEntry {
export interface QueryListEntry {
username: string;
unique_id: string;
bounded_server: number;
}
interface QueryList {
export interface QueryList {
flag_own: boolean;
flag_all: boolean;
queries: QueryListEntry[];
}
interface Playlist {
export interface Playlist {
playlist_id: number;
playlist_bot_id: number;
playlist_title: string;
@ -83,7 +85,7 @@ interface Playlist {
needed_power_song_remove: number;
}
interface PlaylistInfo {
export interface PlaylistInfo {
playlist_id: number,
playlist_title: string,
playlist_description: string,
@ -100,7 +102,7 @@ interface PlaylistInfo {
playlist_max_songs: number
}
interface PlaylistSong {
export interface PlaylistSong {
song_id: number;
song_previous_song_id: number;
song_invoker: string;

File diff suppressed because it is too large Load Diff

View File

@ -1,4 +1,4 @@
class Crc32 {
export class Crc32 {
private static readonly lookup = [
0x00000000, 0x77073096, 0xEE0E612C, 0x990951BA,
0x076DC419, 0x706AF48F, 0xE963A535, 0x9E6495A3,

View File

@ -1,20 +1,18 @@
namespace hex {
export function encode(buffer) {
let hexCodes = [];
let view = new DataView(buffer);
for (let i = 0; i < view.byteLength % 4; i ++) {
let value = view.getUint32(i * 4);
let stringValue = value.toString(16);
let padding = '00000000';
let paddedValue = (padding + stringValue).slice(-padding.length);
hexCodes.push(paddedValue);
}
for (let i = (view.byteLength % 4) * 4; i < view.byteLength; i++) {
let value = view.getUint8(i).toString(16);
let padding = '00';
hexCodes.push((padding + value).slice(-padding.length));
}
return hexCodes.join("");
export function encode(buffer) {
let hexCodes = [];
let view = new DataView(buffer);
for (let i = 0; i < view.byteLength % 4; i ++) {
let value = view.getUint32(i * 4);
let stringValue = value.toString(16);
let padding = '00000000';
let paddedValue = (padding + stringValue).slice(-padding.length);
hexCodes.push(paddedValue);
}
for (let i = (view.byteLength % 4) * 4; i < view.byteLength; i++) {
let value = view.getUint8(i).toString(16);
let padding = '00';
hexCodes.push((padding + value).slice(-padding.length));
}
return hexCodes.join("");
}

View File

@ -6,405 +6,367 @@ declare class _sha1 {
/*
interface Window {
TextEncoder: any;
TextEncoder: any;
}
*/
namespace sha {
/*
* [js-sha1]{@link https://github.com/emn178/js-sha1}
*
* @version 0.6.0
* @author Chen, Yi-Cyuan [emn178@gmail.com]
* @copyright Chen, Yi-Cyuan 2014-2017
* @license MIT
*/
/*jslint bitwise: true */
(function() {
'use strict';
/*
* [js-sha1]{@link https://github.com/emn178/js-sha1}
*
* @version 0.6.0
* @author Chen, Yi-Cyuan [emn178@gmail.com]
* @copyright Chen, Yi-Cyuan 2014-2017
* @license MIT
*/
/*jslint bitwise: true */
(function() {
'use strict';
let root: any = typeof window === 'object' ? window : {};
let NODE_JS = !root.JS_SHA1_NO_NODE_JS && typeof process === 'object' && process.versions && process.versions.node;
if (NODE_JS) {
root = global;
let root: any = typeof window === 'object' ? window : {};
let HEX_CHARS = '0123456789abcdef'.split('');
let EXTRA = [-2147483648, 8388608, 32768, 128];
let SHIFT = [24, 16, 8, 0];
let OUTPUT_TYPES = ['hex', 'array', 'digest', 'arrayBuffer'];
let blocks = [];
let createOutputMethod = function (outputType) {
return function (message) {
return new Sha1(true).update(message)[outputType]();
};
};
let createMethod = function () {
let method: any = createOutputMethod('hex');
method.create = function () {
return new (Sha1 as any)();
};
method.update = function (message) {
return method.create().update(message);
};
for (var i = 0; i < OUTPUT_TYPES.length; ++i) {
var type = OUTPUT_TYPES[i];
method[type] = createOutputMethod(type);
}
let COMMON_JS = !root.JS_SHA1_NO_COMMON_JS && typeof module === 'object' && module.exports;
let AMD = typeof define === 'function' && (define as any).amd;
let HEX_CHARS = '0123456789abcdef'.split('');
let EXTRA = [-2147483648, 8388608, 32768, 128];
let SHIFT = [24, 16, 8, 0];
let OUTPUT_TYPES = ['hex', 'array', 'digest', 'arrayBuffer'];
return method;
};
let blocks = [];
let createOutputMethod = function (outputType) {
return function (message) {
return new Sha1(true).update(message)[outputType]();
};
};
let createMethod = function () {
let method: any = createOutputMethod('hex');
if (NODE_JS) {
method = nodeWrap(method);
}
method.create = function () {
return new (Sha1 as any)();
};
method.update = function (message) {
return method.create().update(message);
};
for (var i = 0; i < OUTPUT_TYPES.length; ++i) {
var type = OUTPUT_TYPES[i];
method[type] = createOutputMethod(type);
}
return method;
};
var nodeWrap = function (method) {
var crypto = eval("require('crypto')");
var Buffer = eval("require('buffer').Buffer");
var nodeMethod = function (message) {
if (typeof message === 'string') {
return crypto.createHash('sha1').update(message, 'utf8').digest('hex');
} else if (message.constructor === ArrayBuffer) {
message = new Uint8Array(message);
} else if (message.length === undefined) {
return method(message);
}
return crypto.createHash('sha1').update(new Buffer(message)).digest('hex');
};
return nodeMethod;
};
function Sha1(sharedMemory) {
if (sharedMemory) {
blocks[0] = blocks[16] = blocks[1] = blocks[2] = blocks[3] =
blocks[4] = blocks[5] = blocks[6] = blocks[7] =
blocks[8] = blocks[9] = blocks[10] = blocks[11] =
blocks[12] = blocks[13] = blocks[14] = blocks[15] = 0;
this.blocks = blocks;
} else {
this.blocks = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
}
this.h0 = 0x67452301;
this.h1 = 0xEFCDAB89;
this.h2 = 0x98BADCFE;
this.h3 = 0x10325476;
this.h4 = 0xC3D2E1F0;
this.block = this.start = this.bytes = this.hBytes = 0;
this.finalized = this.hashed = false;
this.first = true;
function Sha1(sharedMemory) {
if (sharedMemory) {
blocks[0] = blocks[16] = blocks[1] = blocks[2] = blocks[3] =
blocks[4] = blocks[5] = blocks[6] = blocks[7] =
blocks[8] = blocks[9] = blocks[10] = blocks[11] =
blocks[12] = blocks[13] = blocks[14] = blocks[15] = 0;
this.blocks = blocks;
} else {
this.blocks = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
}
Sha1.prototype.update = function (message) {
if (this.finalized) {
return;
}
var notString = typeof(message) !== 'string';
if (notString && message.constructor === root.ArrayBuffer) {
message = new Uint8Array(message);
}
var code, index = 0, i, length = message.length || 0, blocks = this.blocks;
this.h0 = 0x67452301;
this.h1 = 0xEFCDAB89;
this.h2 = 0x98BADCFE;
this.h3 = 0x10325476;
this.h4 = 0xC3D2E1F0;
while (index < length) {
if (this.hashed) {
this.hashed = false;
blocks[0] = this.block;
blocks[16] = blocks[1] = blocks[2] = blocks[3] =
blocks[4] = blocks[5] = blocks[6] = blocks[7] =
blocks[8] = blocks[9] = blocks[10] = blocks[11] =
blocks[12] = blocks[13] = blocks[14] = blocks[15] = 0;
}
this.block = this.start = this.bytes = this.hBytes = 0;
this.finalized = this.hashed = false;
this.first = true;
}
if(notString) {
for (i = this.start; index < length && i < 64; ++index) {
blocks[i >> 2] |= message[index] << SHIFT[i++ & 3];
}
} else {
for (i = this.start; index < length && i < 64; ++index) {
code = message.charCodeAt(index);
if (code < 0x80) {
blocks[i >> 2] |= code << SHIFT[i++ & 3];
} else if (code < 0x800) {
blocks[i >> 2] |= (0xc0 | (code >> 6)) << SHIFT[i++ & 3];
blocks[i >> 2] |= (0x80 | (code & 0x3f)) << SHIFT[i++ & 3];
} else if (code < 0xd800 || code >= 0xe000) {
blocks[i >> 2] |= (0xe0 | (code >> 12)) << SHIFT[i++ & 3];
blocks[i >> 2] |= (0x80 | ((code >> 6) & 0x3f)) << SHIFT[i++ & 3];
blocks[i >> 2] |= (0x80 | (code & 0x3f)) << SHIFT[i++ & 3];
} else {
code = 0x10000 + (((code & 0x3ff) << 10) | (message.charCodeAt(++index) & 0x3ff));
blocks[i >> 2] |= (0xf0 | (code >> 18)) << SHIFT[i++ & 3];
blocks[i >> 2] |= (0x80 | ((code >> 12) & 0x3f)) << SHIFT[i++ & 3];
blocks[i >> 2] |= (0x80 | ((code >> 6) & 0x3f)) << SHIFT[i++ & 3];
blocks[i >> 2] |= (0x80 | (code & 0x3f)) << SHIFT[i++ & 3];
}
}
}
Sha1.prototype.update = function (message) {
if (this.finalized) {
return;
}
var notString = typeof(message) !== 'string';
if (notString && message.constructor === root.ArrayBuffer) {
message = new Uint8Array(message);
}
var code, index = 0, i, length = message.length || 0, blocks = this.blocks;
this.lastByteIndex = i;
this.bytes += i - this.start;
if (i >= 64) {
this.block = blocks[16];
this.start = i - 64;
this.hash();
this.hashed = true;
} else {
this.start = i;
}
}
if (this.bytes > 4294967295) {
this.hBytes += this.bytes / 4294967296 << 0;
this.bytes = this.bytes % 4294967296;
}
return this;
};
Sha1.prototype.finalize = function () {
if (this.finalized) {
return;
}
this.finalized = true;
var blocks = this.blocks, i = this.lastByteIndex;
blocks[16] = this.block;
blocks[i >> 2] |= EXTRA[i & 3];
this.block = blocks[16];
if (i >= 56) {
if (!this.hashed) {
this.hash();
}
while (index < length) {
if (this.hashed) {
this.hashed = false;
blocks[0] = this.block;
blocks[16] = blocks[1] = blocks[2] = blocks[3] =
blocks[4] = blocks[5] = blocks[6] = blocks[7] =
blocks[8] = blocks[9] = blocks[10] = blocks[11] =
blocks[12] = blocks[13] = blocks[14] = blocks[15] = 0;
}
blocks[14] = this.hBytes << 3 | this.bytes >>> 29;
blocks[15] = this.bytes << 3;
this.hash();
};
Sha1.prototype.hash = function () {
var a = this.h0, b = this.h1, c = this.h2, d = this.h3, e = this.h4;
var f, j, t, blocks = this.blocks;
for(j = 16; j < 80; ++j) {
t = blocks[j - 3] ^ blocks[j - 8] ^ blocks[j - 14] ^ blocks[j - 16];
blocks[j] = (t << 1) | (t >>> 31);
if(notString) {
for (i = this.start; index < length && i < 64; ++index) {
blocks[i >> 2] |= message[index] << SHIFT[i++ & 3];
}
} else {
for (i = this.start; index < length && i < 64; ++index) {
code = message.charCodeAt(index);
if (code < 0x80) {
blocks[i >> 2] |= code << SHIFT[i++ & 3];
} else if (code < 0x800) {
blocks[i >> 2] |= (0xc0 | (code >> 6)) << SHIFT[i++ & 3];
blocks[i >> 2] |= (0x80 | (code & 0x3f)) << SHIFT[i++ & 3];
} else if (code < 0xd800 || code >= 0xe000) {
blocks[i >> 2] |= (0xe0 | (code >> 12)) << SHIFT[i++ & 3];
blocks[i >> 2] |= (0x80 | ((code >> 6) & 0x3f)) << SHIFT[i++ & 3];
blocks[i >> 2] |= (0x80 | (code & 0x3f)) << SHIFT[i++ & 3];
} else {
code = 0x10000 + (((code & 0x3ff) << 10) | (message.charCodeAt(++index) & 0x3ff));
blocks[i >> 2] |= (0xf0 | (code >> 18)) << SHIFT[i++ & 3];
blocks[i >> 2] |= (0x80 | ((code >> 12) & 0x3f)) << SHIFT[i++ & 3];
blocks[i >> 2] |= (0x80 | ((code >> 6) & 0x3f)) << SHIFT[i++ & 3];
blocks[i >> 2] |= (0x80 | (code & 0x3f)) << SHIFT[i++ & 3];
}
}
}
for(j = 0; j < 20; j += 5) {
f = (b & c) | ((~b) & d);
t = (a << 5) | (a >>> 27);
e = t + f + e + 1518500249 + blocks[j] << 0;
b = (b << 30) | (b >>> 2);
f = (a & b) | ((~a) & c);
t = (e << 5) | (e >>> 27);
d = t + f + d + 1518500249 + blocks[j + 1] << 0;
a = (a << 30) | (a >>> 2);
f = (e & a) | ((~e) & b);
t = (d << 5) | (d >>> 27);
c = t + f + c + 1518500249 + blocks[j + 2] << 0;
e = (e << 30) | (e >>> 2);
f = (d & e) | ((~d) & a);
t = (c << 5) | (c >>> 27);
b = t + f + b + 1518500249 + blocks[j + 3] << 0;
d = (d << 30) | (d >>> 2);
f = (c & d) | ((~c) & e);
t = (b << 5) | (b >>> 27);
a = t + f + a + 1518500249 + blocks[j + 4] << 0;
c = (c << 30) | (c >>> 2);
}
for(; j < 40; j += 5) {
f = b ^ c ^ d;
t = (a << 5) | (a >>> 27);
e = t + f + e + 1859775393 + blocks[j] << 0;
b = (b << 30) | (b >>> 2);
f = a ^ b ^ c;
t = (e << 5) | (e >>> 27);
d = t + f + d + 1859775393 + blocks[j + 1] << 0;
a = (a << 30) | (a >>> 2);
f = e ^ a ^ b;
t = (d << 5) | (d >>> 27);
c = t + f + c + 1859775393 + blocks[j + 2] << 0;
e = (e << 30) | (e >>> 2);
f = d ^ e ^ a;
t = (c << 5) | (c >>> 27);
b = t + f + b + 1859775393 + blocks[j + 3] << 0;
d = (d << 30) | (d >>> 2);
f = c ^ d ^ e;
t = (b << 5) | (b >>> 27);
a = t + f + a + 1859775393 + blocks[j + 4] << 0;
c = (c << 30) | (c >>> 2);
}
for(; j < 60; j += 5) {
f = (b & c) | (b & d) | (c & d);
t = (a << 5) | (a >>> 27);
e = t + f + e - 1894007588 + blocks[j] << 0;
b = (b << 30) | (b >>> 2);
f = (a & b) | (a & c) | (b & c);
t = (e << 5) | (e >>> 27);
d = t + f + d - 1894007588 + blocks[j + 1] << 0;
a = (a << 30) | (a >>> 2);
f = (e & a) | (e & b) | (a & b);
t = (d << 5) | (d >>> 27);
c = t + f + c - 1894007588 + blocks[j + 2] << 0;
e = (e << 30) | (e >>> 2);
f = (d & e) | (d & a) | (e & a);
t = (c << 5) | (c >>> 27);
b = t + f + b - 1894007588 + blocks[j + 3] << 0;
d = (d << 30) | (d >>> 2);
f = (c & d) | (c & e) | (d & e);
t = (b << 5) | (b >>> 27);
a = t + f + a - 1894007588 + blocks[j + 4] << 0;
c = (c << 30) | (c >>> 2);
}
for(; j < 80; j += 5) {
f = b ^ c ^ d;
t = (a << 5) | (a >>> 27);
e = t + f + e - 899497514 + blocks[j] << 0;
b = (b << 30) | (b >>> 2);
f = a ^ b ^ c;
t = (e << 5) | (e >>> 27);
d = t + f + d - 899497514 + blocks[j + 1] << 0;
a = (a << 30) | (a >>> 2);
f = e ^ a ^ b;
t = (d << 5) | (d >>> 27);
c = t + f + c - 899497514 + blocks[j + 2] << 0;
e = (e << 30) | (e >>> 2);
f = d ^ e ^ a;
t = (c << 5) | (c >>> 27);
b = t + f + b - 899497514 + blocks[j + 3] << 0;
d = (d << 30) | (d >>> 2);
f = c ^ d ^ e;
t = (b << 5) | (b >>> 27);
a = t + f + a - 899497514 + blocks[j + 4] << 0;
c = (c << 30) | (c >>> 2);
}
this.h0 = this.h0 + a << 0;
this.h1 = this.h1 + b << 0;
this.h2 = this.h2 + c << 0;
this.h3 = this.h3 + d << 0;
this.h4 = this.h4 + e << 0;
};
Sha1.prototype.hex = function () {
this.finalize();
var h0 = this.h0, h1 = this.h1, h2 = this.h2, h3 = this.h3, h4 = this.h4;
return HEX_CHARS[(h0 >> 28) & 0x0F] + HEX_CHARS[(h0 >> 24) & 0x0F] +
HEX_CHARS[(h0 >> 20) & 0x0F] + HEX_CHARS[(h0 >> 16) & 0x0F] +
HEX_CHARS[(h0 >> 12) & 0x0F] + HEX_CHARS[(h0 >> 8) & 0x0F] +
HEX_CHARS[(h0 >> 4) & 0x0F] + HEX_CHARS[h0 & 0x0F] +
HEX_CHARS[(h1 >> 28) & 0x0F] + HEX_CHARS[(h1 >> 24) & 0x0F] +
HEX_CHARS[(h1 >> 20) & 0x0F] + HEX_CHARS[(h1 >> 16) & 0x0F] +
HEX_CHARS[(h1 >> 12) & 0x0F] + HEX_CHARS[(h1 >> 8) & 0x0F] +
HEX_CHARS[(h1 >> 4) & 0x0F] + HEX_CHARS[h1 & 0x0F] +
HEX_CHARS[(h2 >> 28) & 0x0F] + HEX_CHARS[(h2 >> 24) & 0x0F] +
HEX_CHARS[(h2 >> 20) & 0x0F] + HEX_CHARS[(h2 >> 16) & 0x0F] +
HEX_CHARS[(h2 >> 12) & 0x0F] + HEX_CHARS[(h2 >> 8) & 0x0F] +
HEX_CHARS[(h2 >> 4) & 0x0F] + HEX_CHARS[h2 & 0x0F] +
HEX_CHARS[(h3 >> 28) & 0x0F] + HEX_CHARS[(h3 >> 24) & 0x0F] +
HEX_CHARS[(h3 >> 20) & 0x0F] + HEX_CHARS[(h3 >> 16) & 0x0F] +
HEX_CHARS[(h3 >> 12) & 0x0F] + HEX_CHARS[(h3 >> 8) & 0x0F] +
HEX_CHARS[(h3 >> 4) & 0x0F] + HEX_CHARS[h3 & 0x0F] +
HEX_CHARS[(h4 >> 28) & 0x0F] + HEX_CHARS[(h4 >> 24) & 0x0F] +
HEX_CHARS[(h4 >> 20) & 0x0F] + HEX_CHARS[(h4 >> 16) & 0x0F] +
HEX_CHARS[(h4 >> 12) & 0x0F] + HEX_CHARS[(h4 >> 8) & 0x0F] +
HEX_CHARS[(h4 >> 4) & 0x0F] + HEX_CHARS[h4 & 0x0F];
};
Sha1.prototype.toString = Sha1.prototype.hex;
Sha1.prototype.digest = function () {
this.finalize();
var h0 = this.h0, h1 = this.h1, h2 = this.h2, h3 = this.h3, h4 = this.h4;
return [
(h0 >> 24) & 0xFF, (h0 >> 16) & 0xFF, (h0 >> 8) & 0xFF, h0 & 0xFF,
(h1 >> 24) & 0xFF, (h1 >> 16) & 0xFF, (h1 >> 8) & 0xFF, h1 & 0xFF,
(h2 >> 24) & 0xFF, (h2 >> 16) & 0xFF, (h2 >> 8) & 0xFF, h2 & 0xFF,
(h3 >> 24) & 0xFF, (h3 >> 16) & 0xFF, (h3 >> 8) & 0xFF, h3 & 0xFF,
(h4 >> 24) & 0xFF, (h4 >> 16) & 0xFF, (h4 >> 8) & 0xFF, h4 & 0xFF
];
};
Sha1.prototype.array = Sha1.prototype.digest;
Sha1.prototype.arrayBuffer = function () {
this.finalize();
var buffer = new ArrayBuffer(20);
var dataView = new DataView(buffer);
dataView.setUint32(0, this.h0);
dataView.setUint32(4, this.h1);
dataView.setUint32(8, this.h2);
dataView.setUint32(12, this.h3);
dataView.setUint32(16, this.h4);
return buffer;
};
var exports = createMethod();
if (COMMON_JS) {
module.exports = exports;
} else {
root._sha1 = exports;
if (AMD) {
define(function () {
return exports;
});
this.lastByteIndex = i;
this.bytes += i - this.start;
if (i >= 64) {
this.block = blocks[16];
this.start = i - 64;
this.hash();
this.hashed = true;
} else {
this.start = i;
}
}
})();
if (this.bytes > 4294967295) {
this.hBytes += this.bytes / 4294967296 << 0;
this.bytes = this.bytes % 4294967296;
}
return this;
};
export function encode_text(buffer: string) : ArrayBuffer {
if ((window as any).TextEncoder) {
return new TextEncoder().encode(buffer).buffer;
Sha1.prototype.finalize = function () {
if (this.finalized) {
return;
}
let utf8 = unescape(encodeURIComponent(buffer));
let result = new Uint8Array(utf8.length);
for (let i = 0; i < utf8.length; i++) {
result[i] = utf8.charCodeAt(i);
this.finalized = true;
var blocks = this.blocks, i = this.lastByteIndex;
blocks[16] = this.block;
blocks[i >> 2] |= EXTRA[i & 3];
this.block = blocks[16];
if (i >= 56) {
if (!this.hashed) {
this.hash();
}
blocks[0] = this.block;
blocks[16] = blocks[1] = blocks[2] = blocks[3] =
blocks[4] = blocks[5] = blocks[6] = blocks[7] =
blocks[8] = blocks[9] = blocks[10] = blocks[11] =
blocks[12] = blocks[13] = blocks[14] = blocks[15] = 0;
}
return result.buffer;
blocks[14] = this.hBytes << 3 | this.bytes >>> 29;
blocks[15] = this.bytes << 3;
this.hash();
};
Sha1.prototype.hash = function () {
var a = this.h0, b = this.h1, c = this.h2, d = this.h3, e = this.h4;
var f, j, t, blocks = this.blocks;
for(j = 16; j < 80; ++j) {
t = blocks[j - 3] ^ blocks[j - 8] ^ blocks[j - 14] ^ blocks[j - 16];
blocks[j] = (t << 1) | (t >>> 31);
}
for(j = 0; j < 20; j += 5) {
f = (b & c) | ((~b) & d);
t = (a << 5) | (a >>> 27);
e = t + f + e + 1518500249 + blocks[j] << 0;
b = (b << 30) | (b >>> 2);
f = (a & b) | ((~a) & c);
t = (e << 5) | (e >>> 27);
d = t + f + d + 1518500249 + blocks[j + 1] << 0;
a = (a << 30) | (a >>> 2);
f = (e & a) | ((~e) & b);
t = (d << 5) | (d >>> 27);
c = t + f + c + 1518500249 + blocks[j + 2] << 0;
e = (e << 30) | (e >>> 2);
f = (d & e) | ((~d) & a);
t = (c << 5) | (c >>> 27);
b = t + f + b + 1518500249 + blocks[j + 3] << 0;
d = (d << 30) | (d >>> 2);
f = (c & d) | ((~c) & e);
t = (b << 5) | (b >>> 27);
a = t + f + a + 1518500249 + blocks[j + 4] << 0;
c = (c << 30) | (c >>> 2);
}
for(; j < 40; j += 5) {
f = b ^ c ^ d;
t = (a << 5) | (a >>> 27);
e = t + f + e + 1859775393 + blocks[j] << 0;
b = (b << 30) | (b >>> 2);
f = a ^ b ^ c;
t = (e << 5) | (e >>> 27);
d = t + f + d + 1859775393 + blocks[j + 1] << 0;
a = (a << 30) | (a >>> 2);
f = e ^ a ^ b;
t = (d << 5) | (d >>> 27);
c = t + f + c + 1859775393 + blocks[j + 2] << 0;
e = (e << 30) | (e >>> 2);
f = d ^ e ^ a;
t = (c << 5) | (c >>> 27);
b = t + f + b + 1859775393 + blocks[j + 3] << 0;
d = (d << 30) | (d >>> 2);
f = c ^ d ^ e;
t = (b << 5) | (b >>> 27);
a = t + f + a + 1859775393 + blocks[j + 4] << 0;
c = (c << 30) | (c >>> 2);
}
for(; j < 60; j += 5) {
f = (b & c) | (b & d) | (c & d);
t = (a << 5) | (a >>> 27);
e = t + f + e - 1894007588 + blocks[j] << 0;
b = (b << 30) | (b >>> 2);
f = (a & b) | (a & c) | (b & c);
t = (e << 5) | (e >>> 27);
d = t + f + d - 1894007588 + blocks[j + 1] << 0;
a = (a << 30) | (a >>> 2);
f = (e & a) | (e & b) | (a & b);
t = (d << 5) | (d >>> 27);
c = t + f + c - 1894007588 + blocks[j + 2] << 0;
e = (e << 30) | (e >>> 2);
f = (d & e) | (d & a) | (e & a);
t = (c << 5) | (c >>> 27);
b = t + f + b - 1894007588 + blocks[j + 3] << 0;
d = (d << 30) | (d >>> 2);
f = (c & d) | (c & e) | (d & e);
t = (b << 5) | (b >>> 27);
a = t + f + a - 1894007588 + blocks[j + 4] << 0;
c = (c << 30) | (c >>> 2);
}
for(; j < 80; j += 5) {
f = b ^ c ^ d;
t = (a << 5) | (a >>> 27);
e = t + f + e - 899497514 + blocks[j] << 0;
b = (b << 30) | (b >>> 2);
f = a ^ b ^ c;
t = (e << 5) | (e >>> 27);
d = t + f + d - 899497514 + blocks[j + 1] << 0;
a = (a << 30) | (a >>> 2);
f = e ^ a ^ b;
t = (d << 5) | (d >>> 27);
c = t + f + c - 899497514 + blocks[j + 2] << 0;
e = (e << 30) | (e >>> 2);
f = d ^ e ^ a;
t = (c << 5) | (c >>> 27);
b = t + f + b - 899497514 + blocks[j + 3] << 0;
d = (d << 30) | (d >>> 2);
f = c ^ d ^ e;
t = (b << 5) | (b >>> 27);
a = t + f + a - 899497514 + blocks[j + 4] << 0;
c = (c << 30) | (c >>> 2);
}
this.h0 = this.h0 + a << 0;
this.h1 = this.h1 + b << 0;
this.h2 = this.h2 + c << 0;
this.h3 = this.h3 + d << 0;
this.h4 = this.h4 + e << 0;
};
Sha1.prototype.hex = function () {
this.finalize();
var h0 = this.h0, h1 = this.h1, h2 = this.h2, h3 = this.h3, h4 = this.h4;
return HEX_CHARS[(h0 >> 28) & 0x0F] + HEX_CHARS[(h0 >> 24) & 0x0F] +
HEX_CHARS[(h0 >> 20) & 0x0F] + HEX_CHARS[(h0 >> 16) & 0x0F] +
HEX_CHARS[(h0 >> 12) & 0x0F] + HEX_CHARS[(h0 >> 8) & 0x0F] +
HEX_CHARS[(h0 >> 4) & 0x0F] + HEX_CHARS[h0 & 0x0F] +
HEX_CHARS[(h1 >> 28) & 0x0F] + HEX_CHARS[(h1 >> 24) & 0x0F] +
HEX_CHARS[(h1 >> 20) & 0x0F] + HEX_CHARS[(h1 >> 16) & 0x0F] +
HEX_CHARS[(h1 >> 12) & 0x0F] + HEX_CHARS[(h1 >> 8) & 0x0F] +
HEX_CHARS[(h1 >> 4) & 0x0F] + HEX_CHARS[h1 & 0x0F] +
HEX_CHARS[(h2 >> 28) & 0x0F] + HEX_CHARS[(h2 >> 24) & 0x0F] +
HEX_CHARS[(h2 >> 20) & 0x0F] + HEX_CHARS[(h2 >> 16) & 0x0F] +
HEX_CHARS[(h2 >> 12) & 0x0F] + HEX_CHARS[(h2 >> 8) & 0x0F] +
HEX_CHARS[(h2 >> 4) & 0x0F] + HEX_CHARS[h2 & 0x0F] +
HEX_CHARS[(h3 >> 28) & 0x0F] + HEX_CHARS[(h3 >> 24) & 0x0F] +
HEX_CHARS[(h3 >> 20) & 0x0F] + HEX_CHARS[(h3 >> 16) & 0x0F] +
HEX_CHARS[(h3 >> 12) & 0x0F] + HEX_CHARS[(h3 >> 8) & 0x0F] +
HEX_CHARS[(h3 >> 4) & 0x0F] + HEX_CHARS[h3 & 0x0F] +
HEX_CHARS[(h4 >> 28) & 0x0F] + HEX_CHARS[(h4 >> 24) & 0x0F] +
HEX_CHARS[(h4 >> 20) & 0x0F] + HEX_CHARS[(h4 >> 16) & 0x0F] +
HEX_CHARS[(h4 >> 12) & 0x0F] + HEX_CHARS[(h4 >> 8) & 0x0F] +
HEX_CHARS[(h4 >> 4) & 0x0F] + HEX_CHARS[h4 & 0x0F];
};
Sha1.prototype.toString = Sha1.prototype.hex;
Sha1.prototype.digest = function () {
this.finalize();
var h0 = this.h0, h1 = this.h1, h2 = this.h2, h3 = this.h3, h4 = this.h4;
return [
(h0 >> 24) & 0xFF, (h0 >> 16) & 0xFF, (h0 >> 8) & 0xFF, h0 & 0xFF,
(h1 >> 24) & 0xFF, (h1 >> 16) & 0xFF, (h1 >> 8) & 0xFF, h1 & 0xFF,
(h2 >> 24) & 0xFF, (h2 >> 16) & 0xFF, (h2 >> 8) & 0xFF, h2 & 0xFF,
(h3 >> 24) & 0xFF, (h3 >> 16) & 0xFF, (h3 >> 8) & 0xFF, h3 & 0xFF,
(h4 >> 24) & 0xFF, (h4 >> 16) & 0xFF, (h4 >> 8) & 0xFF, h4 & 0xFF
];
};
Sha1.prototype.array = Sha1.prototype.digest;
Sha1.prototype.arrayBuffer = function () {
this.finalize();
const buffer = new ArrayBuffer(20);
const dataView = new DataView(buffer);
dataView.setUint32(0, this.h0);
dataView.setUint32(4, this.h1);
dataView.setUint32(8, this.h2);
dataView.setUint32(12, this.h3);
dataView.setUint32(16, this.h4);
return buffer;
};
createMethod();
})();
export function encode_text(buffer: string) : ArrayBuffer {
if ((window as any).TextEncoder) {
return new TextEncoder().encode(buffer).buffer;
}
export function sha1(message: string | ArrayBuffer) : PromiseLike<ArrayBuffer> {
if(!(typeof(message) === "string" || message instanceof ArrayBuffer)) throw "Invalid type!";
let buffer = message instanceof ArrayBuffer ? message : encode_text(message as string);
if(!crypto || !crypto.subtle || !crypto.subtle.digest || /Edge/.test(navigator.userAgent))
return new Promise<ArrayBuffer>(resolve => {
resolve(_sha1.arrayBuffer(buffer as ArrayBuffer));
});
else
return crypto.subtle.digest("SHA-1", buffer);
let utf8 = unescape(encodeURIComponent(buffer));
let result = new Uint8Array(utf8.length);
for (let i = 0; i < utf8.length; i++) {
result[i] = utf8.charCodeAt(i);
}
return result.buffer;
}
}
export function sha1(message: string | ArrayBuffer) : PromiseLike<ArrayBuffer> {
if(!(typeof(message) === "string" || message instanceof ArrayBuffer)) throw "Invalid type!";
let buffer = message instanceof ArrayBuffer ? message : encode_text(message as string);
if(!crypto || !crypto.subtle || !crypto.subtle.digest || /Edge/.test(navigator.userAgent))
return new Promise<ArrayBuffer>(resolve => {
resolve(_sha1.arrayBuffer(buffer as ArrayBuffer));
});
else
return crypto.subtle.digest("SHA-1", buffer);
}

10
shared/js/crypto/uid.ts Normal file
View File

@ -0,0 +1,10 @@
function s4() {
return Math
.floor((1 + Math.random()) * 0x10000)
.toString(16)
.substring(1);
}
export function guid() {
return s4() + s4() + '-' + s4() + '-' + s4() + '-' + s4() + '-' + s4() + s4() + s4();
}

View File

@ -1,24 +1,22 @@
namespace dns {
export interface AddressTarget {
target_ip: string;
target_port?: number;
}
export interface AddressTarget {
target_ip: string;
target_port?: number;
}
export interface ResolveOptions {
timeout?: number;
allow_cache?: boolean;
max_depth?: number;
export interface ResolveOptions {
timeout?: number;
allow_cache?: boolean;
max_depth?: number;
allow_srv?: boolean;
allow_cname?: boolean;
allow_any?: boolean;
allow_a?: boolean;
allow_aaaa?: boolean;
}
allow_srv?: boolean;
allow_cname?: boolean;
allow_any?: boolean;
allow_a?: boolean;
allow_aaaa?: boolean;
}
export const default_options: ResolveOptions = {
timeout: 5000,
allow_cache: true,
max_depth: 5
};
}
export const default_options: ResolveOptions = {
timeout: 5000,
allow_cache: true,
max_depth: 5
};

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -1,324 +1,317 @@
function guid() {
function s4() {
return Math.floor((1 + Math.random()) * 0x10000)
.toString(16)
.substring(1);
}
return s4() + s4() + '-' + s4() + '-' + s4() + '-' + s4() + '-' + s4() + s4() + s4();
import * as log from "tc-shared/log";
import {LogCategory} from "tc-shared/log";
import {guid} from "tc-shared/crypto/uid";
import {StaticSettings} from "tc-shared/settings";
import {createErrorModal} from "tc-shared/ui/elements/Modal";
import * as loader from "tc-loader";
import {formatMessage} from "tc-shared/ui/frames/chat";
export interface TranslationKey {
message: string;
line?: number;
character?: number;
filename?: string;
}
namespace i18n {
export interface TranslationKey {
message: string;
line?: number;
character?: number;
filename?: string;
export interface Translation {
key: TranslationKey;
translated: string;
flags?: string[];
}
export interface Contributor {
name: string;
email: string;
}
export interface TranslationFile {
path: string;
full_url: string;
translations: Translation[];
}
export interface RepositoryTranslation {
key: string;
path: string;
country_code: string;
name: string;
contributors: Contributor[];
}
export interface TranslationRepository {
unique_id: string;
url: string;
name?: string;
contact?: string;
translations?: RepositoryTranslation[];
load_timestamp?: number;
}
let translations: Translation[] = [];
let fast_translate: { [key:string]:string; } = {};
export function tr(message: string, key?: string) {
const sloppy = fast_translate[message];
if(sloppy) return sloppy;
log.info(LogCategory.I18N, "Translating \"%s\". Default: \"%s\"", key, message);
let translated = message;
for(const translation of translations) {
if(translation.key.message == message) {
translated = translation.translated;
break;
}
}
export interface Translation {
key: TranslationKey;
translated: string;
flags?: string[];
}
fast_translate[message] = translated;
return translated;
}
export interface Contributor {
name: string;
email: string;
}
export function tra(message: string, ...args: any[]) {
message = tr(message);
return formatMessage(message, ...args);
}
export interface TranslationFile {
path: string;
full_url: string;
async function load_translation_file(url: string, path: string) : Promise<TranslationFile> {
return new Promise<TranslationFile>((resolve, reject) => {
$.ajax({
url: url,
async: true,
success: result => {
try {
const file = (typeof(result) === "string" ? JSON.parse(result) : result) as TranslationFile;
if(!file) {
reject("Invalid json");
return;
}
translations: Translation[];
}
file.full_url = url;
file.path = path;
export interface RepositoryTranslation {
key: string;
path: string;
country_code: string;
name: string;
contributors: Contributor[];
}
export interface TranslationRepository {
unique_id: string;
url: string;
name?: string;
contact?: string;
translations?: RepositoryTranslation[];
load_timestamp?: number;
}
let translations: Translation[] = [];
let fast_translate: { [key:string]:string; } = {};
export function tr(message: string, key?: string) {
const sloppy = fast_translate[message];
if(sloppy) return sloppy;
log.info(LogCategory.I18N, "Translating \"%s\". Default: \"%s\"", key, message);
let translated = message;
for(const translation of translations) {
if(translation.key.message == message) {
translated = translation.translated;
break;
//TODO: Validate file
resolve(file);
} catch(error) {
log.warn(LogCategory.I18N, tr("Failed to load translation file %s. Failed to parse or process json: %o"), url, error);
reject(tr("Failed to process or parse json!"));
}
},
error: (xhr, error) => {
reject(tr("Failed to load file: ") + error);
}
})
});
}
export function load_file(url: string, path: string) : Promise<void> {
return load_translation_file(url, path).then(async result => {
/* TODO: Improve this test?!*/
try {
tr("Dummy translation test");
} catch(error) {
throw "dummy test failed";
}
fast_translate[message] = translated;
return translated;
}
log.info(LogCategory.I18N, tr("Successfully initialized up translation file from %s"), url);
translations = result.translations;
}).catch(error => {
log.warn(LogCategory.I18N, tr("Failed to load translation file from \"%s\". Error: %o"), url, error);
return Promise.reject(error);
});
}
export function tra(message: string, ...args: any[]) {
message = tr(message);
return MessageHelper.formatMessage(message, ...args);
}
async function load_translation_file(url: string, path: string) : Promise<TranslationFile> {
return new Promise<TranslationFile>((resolve, reject) => {
async function load_repository0(repo: TranslationRepository, reload: boolean) {
if(!repo.load_timestamp || repo.load_timestamp < 1000 || reload) {
const info_json = await new Promise((resolve, reject) => {
$.ajax({
url: url,
url: repo.url + "/info.json",
async: true,
cache: !reload,
success: result => {
try {
const file = (typeof(result) === "string" ? JSON.parse(result) : result) as TranslationFile;
if(!file) {
reject("Invalid json");
return;
}
file.full_url = url;
file.path = path;
//TODO: Validate file
resolve(file);
} catch(error) {
log.warn(LogCategory.I18N, tr("Failed to load translation file %s. Failed to parse or process json: %o"), url, error);
reject(tr("Failed to process or parse json!"));
const file = (typeof(result) === "string" ? JSON.parse(result) : result) as TranslationFile;
if(!file) {
reject("Invalid json");
return;
}
resolve(file);
},
error: (xhr, error) => {
reject(tr("Failed to load file: ") + error);
}
})
});
Object.assign(repo, info_json);
}
export function load_file(url: string, path: string) : Promise<void> {
return load_translation_file(url, path).then(async result => {
/* TODO: Improve this test?!*/
try {
tr("Dummy translation test");
} catch(error) {
throw "dummy test failed";
}
if(!repo.unique_id)
repo.unique_id = guid();
log.info(LogCategory.I18N, tr("Successfully initialized up translation file from %s"), url);
translations = result.translations;
}).catch(error => {
log.warn(LogCategory.I18N, tr("Failed to load translation file from \"%s\". Error: %o"), url, error);
return Promise.reject(error);
});
repo.translations = repo.translations || [];
repo.load_timestamp = Date.now();
}
export async function load_repository(url: string) : Promise<TranslationRepository> {
const result = {} as TranslationRepository;
result.url = url;
await load_repository0(result, false);
return result;
}
export namespace config {
export interface TranslationConfig {
current_repository_url?: string;
current_language?: string;
current_translation_url?: string;
current_translation_path?: string;
}
async function load_repository0(repo: TranslationRepository, reload: boolean) {
if(!repo.load_timestamp || repo.load_timestamp < 1000 || reload) {
const info_json = await new Promise((resolve, reject) => {
$.ajax({
url: repo.url + "/info.json",
async: true,
cache: !reload,
success: result => {
const file = (typeof(result) === "string" ? JSON.parse(result) : result) as TranslationFile;
if(!file) {
reject("Invalid json");
return;
}
export interface RepositoryConfig {
repositories?: {
url?: string;
repository?: TranslationRepository;
}[];
}
resolve(file);
},
error: (xhr, error) => {
reject(tr("Failed to load file: ") + error);
}
})
const repository_config_key = "i18n.repository";
let _cached_repository_config: RepositoryConfig;
export function repository_config() {
if(_cached_repository_config)
return _cached_repository_config;
const config_string = localStorage.getItem(repository_config_key);
let config: RepositoryConfig;
try {
config = config_string ? JSON.parse(config_string) : {};
} catch(error) {
log.error(LogCategory.I18N, tr("Failed to parse repository config: %o"), error);
}
config.repositories = config.repositories || [];
for(const repo of config.repositories)
(repo.repository || {load_timestamp: 0}).load_timestamp = 0;
if(config.repositories.length == 0) {
//Add the default TeaSpeak repository
load_repository(StaticSettings.instance.static("i18n.default_repository", "https://web.teaspeak.de/i18n/")).then(repo => {
log.info(LogCategory.I18N, tr("Successfully added default repository from \"%s\"."), repo.url);
register_repository(repo);
}).catch(error => {
log.warn(LogCategory.I18N, tr("Failed to add default repository. Error: %o"), error);
});
Object.assign(repo, info_json);
}
if(!repo.unique_id)
repo.unique_id = guid();
repo.translations = repo.translations || [];
repo.load_timestamp = Date.now();
return _cached_repository_config = config;
}
export async function load_repository(url: string) : Promise<TranslationRepository> {
const result = {} as TranslationRepository;
result.url = url;
await load_repository0(result, false);
return result;
export function save_repository_config() {
localStorage.setItem(repository_config_key, JSON.stringify(_cached_repository_config));
}
export namespace config {
export interface TranslationConfig {
current_repository_url?: string;
current_language?: string;
const translation_config_key = "i18n.translation";
let _cached_translation_config: TranslationConfig;
current_translation_url?: string;
current_translation_path?: string;
}
export interface RepositoryConfig {
repositories?: {
url?: string;
repository?: TranslationRepository;
}[];
}
const repository_config_key = "i18n.repository";
let _cached_repository_config: RepositoryConfig;
export function repository_config() {
if(_cached_repository_config)
return _cached_repository_config;
const config_string = localStorage.getItem(repository_config_key);
let config: RepositoryConfig;
try {
config = config_string ? JSON.parse(config_string) : {};
} catch(error) {
log.error(LogCategory.I18N, tr("Failed to parse repository config: %o"), error);
}
config.repositories = config.repositories || [];
for(const repo of config.repositories)
(repo.repository || {load_timestamp: 0}).load_timestamp = 0;
if(config.repositories.length == 0) {
//Add the default TeaSpeak repository
load_repository(StaticSettings.instance.static("i18n.default_repository", "https://web.teaspeak.de/i18n/")).then(repo => {
log.info(LogCategory.I18N, tr("Successfully added default repository from \"%s\"."), repo.url);
register_repository(repo);
}).catch(error => {
log.warn(LogCategory.I18N, tr("Failed to add default repository. Error: %o"), error);
});
}
return _cached_repository_config = config;
}
export function save_repository_config() {
localStorage.setItem(repository_config_key, JSON.stringify(_cached_repository_config));
}
const translation_config_key = "i18n.translation";
let _cached_translation_config: TranslationConfig;
export function translation_config() : TranslationConfig {
if(_cached_translation_config)
return _cached_translation_config;
const config_string = localStorage.getItem(translation_config_key);
try {
_cached_translation_config = config_string ? JSON.parse(config_string) : {};
} catch(error) {
log.error(LogCategory.I18N, tr("Failed to initialize translation config. Using default one. Error: %o"), error);
_cached_translation_config = {} as any;
}
export function translation_config() : TranslationConfig {
if(_cached_translation_config)
return _cached_translation_config;
const config_string = localStorage.getItem(translation_config_key);
try {
_cached_translation_config = config_string ? JSON.parse(config_string) : {};
} catch(error) {
log.error(LogCategory.I18N, tr("Failed to initialize translation config. Using default one. Error: %o"), error);
_cached_translation_config = {} as any;
}
export function save_translation_config() {
localStorage.setItem(translation_config_key, JSON.stringify(_cached_translation_config));
}
return _cached_translation_config;
}
export function register_repository(repository: TranslationRepository) {
if(!repository) return;
for(const repo of config.repository_config().repositories)
if(repo.url == repository.url) return;
config.repository_config().repositories.push(repository);
config.save_repository_config();
}
export function registered_repositories() : TranslationRepository[] {
return config.repository_config().repositories.map(e => e.repository || {url: e.url, load_timestamp: 0} as TranslationRepository);
}
export function delete_repository(repository: TranslationRepository) {
if(!repository) return;
for(const repo of [...config.repository_config().repositories])
if(repo.url == repository.url) {
config.repository_config().repositories.remove(repo);
}
config.save_repository_config();
}
export async function iterate_repositories(callback_entry: (repository: TranslationRepository) => any) {
const promises = [];
for(const repository of registered_repositories()) {
promises.push(load_repository0(repository, false).then(() => callback_entry(repository)).catch(error => {
log.warn(LogCategory.I18N, "Failed to fetch repository %s. error: %o", repository.url, error);
}));
}
await Promise.all(promises);
}
export function select_translation(repository: TranslationRepository, entry: RepositoryTranslation) {
const cfg = config.translation_config();
if(entry && repository) {
cfg.current_language = entry.name;
cfg.current_repository_url = repository.url;
cfg.current_translation_url = repository.url + entry.path;
cfg.current_translation_path = entry.path;
} else {
cfg.current_language = undefined;
cfg.current_repository_url = undefined;
cfg.current_translation_url = undefined;
cfg.current_translation_path = undefined;
}
config.save_translation_config();
}
/* ATTENTION: This method is called before most other library inizialisations! */
export async function initialize() {
const rcfg = config.repository_config(); /* initialize */
const cfg = config.translation_config();
if(cfg.current_translation_url) {
try {
await load_file(cfg.current_translation_url, cfg.current_translation_path);
} catch (error) {
console.error(tr("Failed to initialize selected translation: %o"), error);
const show_error = () => {
createErrorModal(tr("Translation System"), tra("Failed to load current selected translation file.{:br:}File: {0}{:br:}Error: {1}{:br:}{:br:}Using default fallback translations.", cfg.current_translation_url, error)).open()
};
if(loader.running())
loader.register_task(loader.Stage.JAVASCRIPT_INITIALIZING, {
priority: 10,
function: async () => show_error(),
name: "I18N error display"
});
else
show_error();
}
}
// await load_file("http://localhost/home/TeaSpeak/TeaSpeak/Web-Client/web/environment/development/i18n/de_DE.translation");
// await load_file("http://localhost/home/TeaSpeak/TeaSpeak/Web-Client/web/environment/development/i18n/test.json");
export function save_translation_config() {
localStorage.setItem(translation_config_key, JSON.stringify(_cached_translation_config));
}
}
// @ts-ignore
const tr: typeof i18n.tr = i18n.tr;
const tra: typeof i18n.tra = i18n.tra;
export function register_repository(repository: TranslationRepository) {
if(!repository) return;
(window as any).tr = i18n.tr;
(window as any).tra = i18n.tra;
for(const repo of config.repository_config().repositories)
if(repo.url == repository.url) return;
config.repository_config().repositories.push(repository);
config.save_repository_config();
}
export function registered_repositories() : TranslationRepository[] {
return config.repository_config().repositories.map(e => e.repository || {url: e.url, load_timestamp: 0} as TranslationRepository);
}
export function delete_repository(repository: TranslationRepository) {
if(!repository) return;
for(const repo of [...config.repository_config().repositories])
if(repo.url == repository.url) {
config.repository_config().repositories.remove(repo);
}
config.save_repository_config();
}
export async function iterate_repositories(callback_entry: (repository: TranslationRepository) => any) {
const promises = [];
for(const repository of registered_repositories()) {
promises.push(load_repository0(repository, false).then(() => callback_entry(repository)).catch(error => {
log.warn(LogCategory.I18N, "Failed to fetch repository %s. error: %o", repository.url, error);
}));
}
await Promise.all(promises);
}
export function select_translation(repository: TranslationRepository, entry: RepositoryTranslation) {
const cfg = config.translation_config();
if(entry && repository) {
cfg.current_language = entry.name;
cfg.current_repository_url = repository.url;
cfg.current_translation_url = repository.url + entry.path;
cfg.current_translation_path = entry.path;
} else {
cfg.current_language = undefined;
cfg.current_repository_url = undefined;
cfg.current_translation_url = undefined;
cfg.current_translation_path = undefined;
}
config.save_translation_config();
}
/* ATTENTION: This method is called before most other library initialisations! */
export async function initialize() {
const rcfg = config.repository_config(); /* initialize */
const cfg = config.translation_config();
if(cfg.current_translation_url) {
try {
await load_file(cfg.current_translation_url, cfg.current_translation_path);
} catch (error) {
console.error(tr("Failed to initialize selected translation: %o"), error);
const show_error = () => {
createErrorModal(tr("Translation System"), tra("Failed to load current selected translation file.{:br:}File: {0}{:br:}Error: {1}{:br:}{:br:}Using default fallback translations.", cfg.current_translation_url, error)).open()
};
if(loader.running())
loader.register_task(loader.Stage.JAVASCRIPT_INITIALIZING, {
priority: 10,
function: async () => show_error(),
name: "I18N error display"
});
else
show_error();
}
}
// await load_file("http://localhost/home/TeaSpeak/TeaSpeak/Web-Client/web/environment/development/i18n/de_DE.translation");
// await load_file("http://localhost/home/TeaSpeak/TeaSpeak/Web-Client/web/environment/development/i18n/test.json");
}
window.tr = tr;
window.tra = tra;

View File

@ -1,6 +1,8 @@
//Used by CertAccept popup
import {settings} from "tc-shared/settings";
import * as loader from "tc-loader";
enum LogCategory {
export enum LogCategory {
CHANNEL,
CHANNEL_PROPERTIES, /* separating channel and channel properties because on channel init logging is a big bottleneck */
CLIENT,
@ -19,233 +21,238 @@ enum LogCategory {
DNS
}
namespace log {
export enum LogType {
TRACE,
DEBUG,
INFO,
WARNING,
ERROR
export enum LogType {
TRACE,
DEBUG,
INFO,
WARNING,
ERROR
}
let category_mapping = new Map<number, string>([
[LogCategory.CHANNEL, "Channel "],
[LogCategory.CHANNEL_PROPERTIES, "Channel "],
[LogCategory.CLIENT, "Client "],
[LogCategory.SERVER, "Server "],
[LogCategory.BOOKMARKS, "Bookmark "],
[LogCategory.PERMISSIONS, "Permission "],
[LogCategory.GENERAL, "General "],
[LogCategory.NETWORKING, "Network "],
[LogCategory.VOICE, "Voice "],
[LogCategory.AUDIO, "Audio "],
[LogCategory.CHANNEL, "Chat "],
[LogCategory.I18N, "I18N "],
[LogCategory.IDENTITIES, "Identities "],
[LogCategory.IPC, "IPC "],
[LogCategory.STATISTICS, "Statistics "],
[LogCategory.DNS, "DNS "]
]);
export let enabled_mapping = new Map<number, boolean>([
[LogCategory.CHANNEL, true],
[LogCategory.CHANNEL_PROPERTIES, false],
[LogCategory.CLIENT, true],
[LogCategory.SERVER, true],
[LogCategory.BOOKMARKS, true],
[LogCategory.PERMISSIONS, true],
[LogCategory.GENERAL, true],
[LogCategory.NETWORKING, true],
[LogCategory.VOICE, true],
[LogCategory.AUDIO, true],
[LogCategory.CHAT, true],
[LogCategory.I18N, false],
[LogCategory.IDENTITIES, true],
[LogCategory.IPC, true],
[LogCategory.STATISTICS, true],
[LogCategory.DNS, true]
]);
//Values will be overridden by initialize()
export let level_mapping = new Map<LogType, boolean>([
[LogType.TRACE, true],
[LogType.DEBUG, true],
[LogType.INFO, true],
[LogType.WARNING, true],
[LogType.ERROR, true]
]);
enum GroupMode {
NATIVE,
PREFIX
}
const group_mode: GroupMode = GroupMode.PREFIX;
//Category Example: <url>?log.i18n.enabled=0
//Level Example A: <url>?log.level.trace.enabled=0
//Level Example B: <url>?log.level=0
export function initialize(default_level: LogType) {
for(const category of Object.keys(LogCategory).map(e => parseInt(e))) {
if(isNaN(category)) continue;
const category_name = LogCategory[category].toLowerCase();
enabled_mapping.set(category, settings.static_global<boolean>("log." + category_name.toLowerCase() + ".enabled", enabled_mapping.get(category)));
}
let category_mapping = new Map<number, string>([
[LogCategory.CHANNEL, "Channel "],
[LogCategory.CHANNEL_PROPERTIES, "Channel "],
[LogCategory.CLIENT, "Client "],
[LogCategory.SERVER, "Server "],
[LogCategory.BOOKMARKS, "Bookmark "],
[LogCategory.PERMISSIONS, "Permission "],
[LogCategory.GENERAL, "General "],
[LogCategory.NETWORKING, "Network "],
[LogCategory.VOICE, "Voice "],
[LogCategory.AUDIO, "Audio "],
[LogCategory.CHANNEL, "Chat "],
[LogCategory.I18N, "I18N "],
[LogCategory.IDENTITIES, "Identities "],
[LogCategory.IPC, "IPC "],
[LogCategory.STATISTICS, "Statistics "],
[LogCategory.DNS, "DNS "]
]);
const base_level = settings.static_global<number>("log.level", default_level);
export let enabled_mapping = new Map<number, boolean>([
[LogCategory.CHANNEL, true],
[LogCategory.CHANNEL_PROPERTIES, false],
[LogCategory.CLIENT, true],
[LogCategory.SERVER, true],
[LogCategory.BOOKMARKS, true],
[LogCategory.PERMISSIONS, true],
[LogCategory.GENERAL, true],
[LogCategory.NETWORKING, true],
[LogCategory.VOICE, true],
[LogCategory.AUDIO, true],
[LogCategory.CHAT, true],
[LogCategory.I18N, false],
[LogCategory.IDENTITIES, true],
[LogCategory.IPC, true],
[LogCategory.STATISTICS, true],
[LogCategory.DNS, true]
]);
for(const level of Object.keys(LogType).map(e => parseInt(e))) {
if(isNaN(level)) continue;
//Values will be overridden by initialize()
export let level_mapping = new Map<LogType, boolean>([
[LogType.TRACE, true],
[LogType.DEBUG, true],
[LogType.INFO, true],
[LogType.WARNING, true],
[LogType.ERROR, true]
]);
enum GroupMode {
NATIVE,
PREFIX
}
const group_mode: GroupMode = GroupMode.PREFIX;
//Category Example: <url>?log.i18n.enabled=0
//Level Example A: <url>?log.level.trace.enabled=0
//Level Example B: <url>?log.level=0
export function initialize(default_level: LogType) {
for(const category of Object.keys(LogCategory).map(e => parseInt(e))) {
if(isNaN(category)) continue;
const category_name = LogCategory[category].toLowerCase();
enabled_mapping.set(category, settings.static_global<boolean>("log." + category_name.toLowerCase() + ".enabled", enabled_mapping.get(category)));
}
const base_level = settings.static_global<number>("log.level", default_level);
for(const level of Object.keys(LogType).map(e => parseInt(e))) {
if(isNaN(level)) continue;
const level_name = LogType[level].toLowerCase();
level_mapping.set(level, settings.static_global<boolean>("log." + level_name + ".enabled", level >= base_level));
}
}
function logDirect(type: LogType, message: string, ...optionalParams: any[]) {
if(!level_mapping.get(type))
return;
switch (type) {
case LogType.TRACE:
case LogType.DEBUG:
console.debug(message, ...optionalParams);
break;
case LogType.INFO:
console.log(message, ...optionalParams);
break;
case LogType.WARNING:
console.warn(message, ...optionalParams);
break;
case LogType.ERROR:
console.error(message, ...optionalParams);
break;
}
}
export function log(type: LogType, category: LogCategory, message: string, ...optionalParams: any[]) {
if(!enabled_mapping.get(category)) return;
optionalParams.unshift(category_mapping.get(category));
message = "[%s] " + message;
logDirect(type, message, ...optionalParams);
}
export function trace(category: LogCategory, message: string, ...optionalParams: any[]) {
log(LogType.TRACE, category, message, ...optionalParams);
}
export function debug(category: LogCategory, message: string, ...optionalParams: any[]) {
log(LogType.DEBUG, category, message, ...optionalParams);
}
export function info(category: LogCategory, message: string, ...optionalParams: any[]) {
log(LogType.INFO, category, message, ...optionalParams);
}
export function warn(category: LogCategory, message: string, ...optionalParams: any[]) {
log(LogType.WARNING, category, message, ...optionalParams);
}
export function error(category: LogCategory, message: string, ...optionalParams: any[]) {
log(LogType.ERROR, category, message, ...optionalParams);
}
export function group(level: LogType, category: LogCategory, name: string, ...optionalParams: any[]) : Group {
name = "[%s] " + name;
optionalParams.unshift(category_mapping.get(category));
return new Group(group_mode, level, category, name, optionalParams);
}
export function table(level: LogType, category: LogCategory, title: string, arguments: any) {
if(group_mode == GroupMode.NATIVE) {
console.groupCollapsed(title);
console.table(arguments);
console.groupEnd();
} else {
if(!enabled_mapping.get(category) || !level_mapping.get(level))
return;
logDirect(level, tr("Snipped table \"%s\""), title);
}
}
export class Group {
readonly mode: GroupMode;
readonly level: LogType;
readonly category: LogCategory;
readonly enabled: boolean;
owner: Group = undefined;
private readonly name: string;
private readonly optionalParams: any[][];
private _collapsed: boolean = false;
private initialized = false;
private _log_prefix: string;
constructor(mode: GroupMode, level: LogType, category: LogCategory, name: string, optionalParams: any[][], owner: Group = undefined) {
this.level = level;
this.mode = mode;
this.category = category;
this.name = name;
this.optionalParams = optionalParams;
this.enabled = enabled_mapping.get(category);
}
group(level: LogType, name: string, ...optionalParams: any[]) : Group {
return new Group(this.mode, level, this.category, name, optionalParams, this);
}
collapsed(flag: boolean = true) : this {
this._collapsed = flag;
return this;
}
log(message: string, ...optionalParams: any[]) : this {
if(!this.enabled)
return this;
if(!this.initialized) {
if(this.mode == GroupMode.NATIVE) {
if(this._collapsed && console.groupCollapsed)
console.groupCollapsed(this.name, ...this.optionalParams);
else
console.group(this.name, ...this.optionalParams);
} else {
this._log_prefix = " ";
let parent = this.owner;
while(parent) {
if(parent.mode == GroupMode.PREFIX)
this._log_prefix = this._log_prefix + parent._log_prefix;
else
break;
}
}
this.initialized = true;
}
if(this.mode == GroupMode.NATIVE)
logDirect(this.level, message, ...optionalParams);
else {
logDirect(this.level, "[%s] " + this._log_prefix + message, category_mapping.get(this.category), ...optionalParams);
}
return this;
}
end() {
if(this.initialized) {
if(this.mode == GroupMode.NATIVE)
console.groupEnd();
}
}
get prefix() : string {
return this._log_prefix;
}
set prefix(prefix: string) {
this._log_prefix = prefix;
}
const level_name = LogType[level].toLowerCase();
level_mapping.set(level, settings.static_global<boolean>("log." + level_name + ".enabled", level >= base_level));
}
}
import LogType = log.LogType;
function logDirect(type: LogType, message: string, ...optionalParams: any[]) {
if(!level_mapping.get(type))
return;
switch (type) {
case LogType.TRACE:
case LogType.DEBUG:
console.debug(message, ...optionalParams);
break;
case LogType.INFO:
console.log(message, ...optionalParams);
break;
case LogType.WARNING:
console.warn(message, ...optionalParams);
break;
case LogType.ERROR:
console.error(message, ...optionalParams);
break;
}
}
export function log(type: LogType, category: LogCategory, message: string, ...optionalParams: any[]) {
if(!enabled_mapping.get(category)) return;
optionalParams.unshift(category_mapping.get(category));
message = "[%s] " + message;
logDirect(type, message, ...optionalParams);
}
export function trace(category: LogCategory, message: string, ...optionalParams: any[]) {
log(LogType.TRACE, category, message, ...optionalParams);
}
export function debug(category: LogCategory, message: string, ...optionalParams: any[]) {
log(LogType.DEBUG, category, message, ...optionalParams);
}
export function info(category: LogCategory, message: string, ...optionalParams: any[]) {
log(LogType.INFO, category, message, ...optionalParams);
}
export function warn(category: LogCategory, message: string, ...optionalParams: any[]) {
log(LogType.WARNING, category, message, ...optionalParams);
}
export function error(category: LogCategory, message: string, ...optionalParams: any[]) {
log(LogType.ERROR, category, message, ...optionalParams);
}
export function group(level: LogType, category: LogCategory, name: string, ...optionalParams: any[]) : Group {
name = "[%s] " + name;
optionalParams.unshift(category_mapping.get(category));
return new Group(group_mode, level, category, name, optionalParams);
}
export function table(level: LogType, category: LogCategory, title: string, args: any) {
if(group_mode == GroupMode.NATIVE) {
console.groupCollapsed(title);
console.table(args);
console.groupEnd();
} else {
if(!enabled_mapping.get(category) || !level_mapping.get(level))
return;
logDirect(level, tr("Snipped table \"%s\""), title);
}
}
export class Group {
readonly mode: GroupMode;
readonly level: LogType;
readonly category: LogCategory;
readonly enabled: boolean;
owner: Group = undefined;
private readonly name: string;
private readonly optionalParams: any[][];
private _collapsed: boolean = false;
private initialized = false;
private _log_prefix: string;
constructor(mode: GroupMode, level: LogType, category: LogCategory, name: string, optionalParams: any[][], owner: Group = undefined) {
this.level = level;
this.mode = mode;
this.category = category;
this.name = name;
this.optionalParams = optionalParams;
this.enabled = enabled_mapping.get(category);
}
group(level: LogType, name: string, ...optionalParams: any[]) : Group {
return new Group(this.mode, level, this.category, name, optionalParams, this);
}
collapsed(flag: boolean = true) : this {
this._collapsed = flag;
return this;
}
log(message: string, ...optionalParams: any[]) : this {
if(!this.enabled)
return this;
if(!this.initialized) {
if(this.mode == GroupMode.NATIVE) {
if(this._collapsed && console.groupCollapsed)
console.groupCollapsed(this.name, ...this.optionalParams);
else
console.group(this.name, ...this.optionalParams);
} else {
this._log_prefix = " ";
let parent = this.owner;
while(parent) {
if(parent.mode == GroupMode.PREFIX)
this._log_prefix = this._log_prefix + parent._log_prefix;
else
break;
}
}
this.initialized = true;
}
if(this.mode == GroupMode.NATIVE)
logDirect(this.level, message, ...optionalParams);
else {
logDirect(this.level, "[%s] " + this._log_prefix + message, category_mapping.get(this.category), ...optionalParams);
}
return this;
}
end() {
if(this.initialized) {
if(this.mode == GroupMode.NATIVE)
console.groupEnd();
}
}
get prefix() : string {
return this._log_prefix;
}
set prefix(prefix: string) {
this._log_prefix = prefix;
}
}
loader.register_task(loader.Stage.JAVASCRIPT_INITIALIZING, {
name: "log enabled initialisation",
function: async () => initialize(loader.version().debug_mode ? LogType.TRACE : LogType.INFO),
priority: 150
});
/* initialize global logging system, use by the loader for example */
window.log = module.exports;

View File

@ -1,31 +1,41 @@
/// <reference path="ui/frames/chat.ts" />
/// <reference path="ui/modal/ModalConnect.ts" />
/// <reference path="ui/modal/ModalCreateChannel.ts" />
/// <reference path="ui/modal/ModalBanClient.ts" />
/// <reference path="ui/modal/ModalYesNo.ts" />
/// <reference path="ui/modal/ModalBanList.ts" />
/// <reference path="settings.ts" />
/// <reference path="log.ts" />
/// <reference path="PPTListener.ts" />
import * as loader from "tc-loader";
import {settings, Settings} from "tc-shared/settings";
import * as profiles from "tc-shared/profiles/ConnectionProfile";
import {LogCategory} from "tc-shared/log";
import * as log from "tc-shared/log";
import * as bipc from "./BrowserIPC";
import * as sound from "./sound/Sounds";
import * as i18n from "./i18n/localize";
import {ConnectionHandler} from "tc-shared/ConnectionHandler";
import {createInfoModal} from "tc-shared/ui/elements/Modal";
import {tra} from "./i18n/localize";
import {RequestFileUpload} from "tc-shared/FileManager";
import * as stats from "./stats";
import * as fidentity from "./profiles/identities/TeaForumIdentity";
import {default_recorder, RecorderProfile, set_default_recorder} from "tc-shared/voice/RecorderProfile";
import * as cmanager from "tc-shared/ui/frames/connection_handlers";
import {server_connections, ServerConnectionManager} from "tc-shared/ui/frames/connection_handlers";
import * as control_bar from "tc-shared/ui/frames/ControlBar";
import {spawnConnectModal} from "tc-shared/ui/modal/ModalConnect";
import * as top_menu from "./ui/frames/MenuBar";
import {spawnYesNo} from "tc-shared/ui/modal/ModalYesNo";
import {formatMessage} from "tc-shared/ui/frames/chat";
import {openModalNewcomer} from "tc-shared/ui/modal/ModalNewcomer";
import * as aplayer from "tc-backend/audio/player";
import * as arecorder from "tc-backend/audio/recorder";
import * as ppt from "tc-backend/ppt";
import spawnYesNo = Modals.spawnYesNo;
/* required import for init */
require("./proto").initialize();
require("./ui/elements/ContextDivider").initialize();
const js_render = window.jsrender || $;
const native_client = window.require !== undefined;
function getUserMediaFunctionPromise() : (constraints: MediaStreamConstraints) => Promise<MediaStream> {
if('mediaDevices' in navigator && 'getUserMedia' in navigator.mediaDevices)
return constraints => navigator.mediaDevices.getUserMedia(constraints);
const _callbacked_function = navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia;
if(!_callbacked_function)
return undefined;
return constraints => new Promise<MediaStream>((resolve, reject) => _callbacked_function(constraints, resolve, reject));
}
interface Window {
open_connected_question: () => Promise<boolean>;
declare global {
interface Window {
open_connected_question: () => Promise<boolean>;
}
}
function setup_close() {
@ -50,7 +60,7 @@ function setup_close() {
}));
const exit = () => {
const {remote} = require('electron');
const {remote} = window.require('electron');
remote.getCurrentWindow().close();
};
@ -133,7 +143,7 @@ async function initialize_app() {
try { //Initialize main template
const main = $("#tmpl_main").renderTag({
multi_session: !settings.static_global(Settings.KEY_DISABLE_MULTI_SESSION),
app_version: app.ui_version()
app_version: loader.version().ui
}).dividerfy();
$("body").append(main);
@ -143,21 +153,21 @@ async function initialize_app() {
return;
}
control_bar = new ControlBar($("#control_bar")); /* setup the control bar */
control_bar.set_control_bar(new control_bar.ControlBar($("#control_bar"))); /* setup the control bar */
if(!audio.player.initialize())
if(!aplayer.initialize())
console.warn(tr("Failed to initialize audio controller!"));
audio.player.on_ready(() => {
if(audio.player.set_master_volume)
audio.player.on_ready(() => audio.player.set_master_volume(settings.global(Settings.KEY_SOUND_MASTER) / 100));
aplayer.on_ready(() => {
if(aplayer.set_master_volume)
aplayer.on_ready(() => aplayer.set_master_volume(settings.global(Settings.KEY_SOUND_MASTER) / 100));
else
log.warn(LogCategory.GENERAL, tr("Client does not support audio.player.set_master_volume()... May client is too old?"));
if(audio.recorder.device_refresh_available())
audio.recorder.refresh_devices();
log.warn(LogCategory.GENERAL, tr("Client does not support aplayer.set_master_volume()... May client is too old?"));
if(arecorder.device_refresh_available())
arecorder.refresh_devices();
});
default_recorder = new RecorderProfile("default");
set_default_recorder(new RecorderProfile("default"));
default_recorder.initialize().catch(error => {
log.error(LogCategory.AUDIO, tr("Failed to initialize default recorder: %o"), error);
});
@ -180,78 +190,6 @@ async function initialize_app() {
setup_close();
}
function str2ab8(str) {
const buf = new ArrayBuffer(str.length);
const bufView = new Uint8Array(buf);
for (let i = 0, strLen = str.length; i < strLen; i++) {
bufView[i] = str.charCodeAt(i);
}
return buf;
}
/* FIXME Dont use atob, because it sucks for non UTF-8 tings */
function arrayBufferBase64(base64: string) {
base64 = atob(base64);
const buf = new ArrayBuffer(base64.length);
const bufView = new Uint8Array(buf);
for (let i = 0, strLen = base64.length; i < strLen; i++) {
bufView[i] = base64.charCodeAt(i);
}
return buf;
}
function base64_encode_ab(source: ArrayBufferLike) {
const encodings = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
let base64 = "";
const bytes = new Uint8Array(source);
const byte_length = bytes.byteLength;
const byte_reminder = byte_length % 3;
const main_length = byte_length - byte_reminder;
let a, b, c, d;
let chunk;
// Main loop deals with bytes in chunks of 3
for (let i = 0; i < main_length; i = i + 3) {
// Combine the three bytes into a single integer
chunk = (bytes[i] << 16) | (bytes[i + 1] << 8) | bytes[i + 2];
// Use bitmasks to extract 6-bit segments from the triplet
a = (chunk & 16515072) >> 18; // 16515072 = (2^6 - 1) << 18
b = (chunk & 258048) >> 12; // 258048 = (2^6 - 1) << 12
c = (chunk & 4032) >> 6; // 4032 = (2^6 - 1) << 6
d = (chunk & 63) >> 0; // 63 = (2^6 - 1) << 0
// Convert the raw binary segments to the appropriate ASCII encoding
base64 += encodings[a] + encodings[b] + encodings[c] + encodings[d];
}
// Deal with the remaining bytes and padding
if (byte_reminder == 1) {
chunk = bytes[main_length];
a = (chunk & 252) >> 2; // 252 = (2^6 - 1) << 2
// Set the 4 least significant bits to zero
b = (chunk & 3) << 4; // 3 = 2^2 - 1
base64 += encodings[a] + encodings[b] + '==';
} else if (byte_reminder == 2) {
chunk = (bytes[main_length] << 8) | bytes[main_length + 1];
a = (chunk & 64512) >> 10; // 64512 = (2^6 - 1) << 10
b = (chunk & 1008) >> 4; // 1008 = (2^6 - 1) << 4
// Set the 2 least significant bits to zero
c = (chunk & 15) << 2; // 15 = 2^4 - 1
base64 += encodings[a] + encodings[b] + encodings[c] + '=';
}
return base64
}
/*
class TestProxy extends bipc.MethodProxy {
constructor(params: bipc.MethodProxyConnectParameters) {
@ -313,7 +251,7 @@ function handle_connect_request(properties: bipc.connect.ConnectRequestData, con
});
server_connections.set_active_connection_handler(connection);
} else {
Modals.spawnConnectModal({},{
spawnConnectModal({},{
url: properties.address,
enforce: true
}, {
@ -356,14 +294,14 @@ function main() {
top_menu.initialize();
server_connections = new ServerConnectionManager($("#connection-handlers"));
control_bar.initialise(); /* before connection handler to allow property apply */
cmanager.initialize(new ServerConnectionManager($("#connection-handlers")));
control_bar.control_bar.initialise(); /* before connection handler to allow property apply */
const initial_handler = server_connections.spawn_server_connection_handler();
initial_handler.acquire_recorder(default_recorder, false);
control_bar.set_connection_handler(initial_handler);
control_bar.control_bar.set_connection_handler(initial_handler);
/** Setup the XF forum identity **/
profiles.identities.update_forum();
fidentity.update_forum();
let _resize_timeout: NodeJS.Timer;
$(window).on('resize', event => {
@ -481,7 +419,7 @@ function main() {
/* for testing */
if(settings.static_global(Settings.KEY_USER_IS_NEW)) {
const modal = Modals.openModalNewcomer();
const modal = openModalNewcomer();
modal.close_listener.push(() => settings.changeGlobal(Settings.KEY_USER_IS_NEW, false));
}
}
@ -492,12 +430,12 @@ const task_teaweb_starter: loader.Task = {
try {
await initialize_app();
main();
if(!audio.player.initialized()) {
if(!aplayer.initialized()) {
log.info(LogCategory.VOICE, tr("Initialize audio controller later!"));
if(!audio.player.initializeFromGesture) {
console.error(tr("Missing audio.player.initializeFromGesture"));
if(!aplayer.initializeFromGesture) {
console.error(tr("Missing aplayer.initializeFromGesture"));
} else
$(document).one('click', event => audio.player.initializeFromGesture());
$(document).one('click', event => aplayer.initializeFromGesture());
}
} catch (ex) {
console.error(ex.stack);
@ -543,7 +481,7 @@ const task_connect_handler: loader.Task = {
"You could now close this page.";
createInfoModal(
tr("Connecting successfully within other instance"),
MessageHelper.formatMessage(tr(message), connect_data.address),
formatMessage(tr(message), connect_data.address),
{
closeable: false,
footer: undefined
@ -610,7 +548,7 @@ const task_certificate_callback: loader.Task = {
"This page will close in {0} seconds.";
createInfoModal(
tr("Certificate acccepted successfully"),
MessageHelper.formatMessage(tr(message), seconds_tag),
formatMessage(tr(message), seconds_tag),
{
closeable: false,
footer: undefined
@ -650,7 +588,7 @@ loader.register_task(loader.Stage.JAVASCRIPT_INITIALIZING, {
try {
await initialize();
if(app.is_web()) {
if(loader.version().type == "web") {
loader.register_task(loader.Stage.LOADED, task_certificate_callback);
} else {
loader.register_task(loader.Stage.LOADED, task_teaweb_starter);
@ -667,3 +605,15 @@ loader.register_task(loader.Stage.JAVASCRIPT_INITIALIZING, {
priority: 1000
});
loader.register_task(loader.Stage.LOADED, {
name: "error task",
function: async () => {
if(Settings.instance.static(Settings.KEY_LOAD_DUMMY_ERROR, false)) {
loader.critical_error("The tea is cold!", "Argh, this is evil! Cold tea dosn't taste good.");
throw "The tea is cold!";
}
},
priority: 20
});
export = {};

View File

@ -1,17 +1,24 @@
/// <reference path="../connection/ConnectionBase.ts" />
import {LaterPromise} from "tc-shared/utils/LaterPromise";
import * as log from "tc-shared/log";
import {LogCategory} from "tc-shared/log";
import {PermissionManager, PermissionValue} from "tc-shared/permission/PermissionManager";
import {ServerCommand} from "tc-shared/connection/ConnectionBase";
import {CommandResult} from "tc-shared/connection/ServerConnectionDeclaration";
import {ConnectionHandler} from "tc-shared/ConnectionHandler";
import {AbstractCommandHandler} from "tc-shared/connection/AbstractCommandHandler";
enum GroupType {
export enum GroupType {
QUERY,
TEMPLATE,
NORMAL
}
enum GroupTarget {
export enum GroupTarget {
SERVER,
CHANNEL
}
class GroupProperties {
export class GroupProperties {
iconid: number = 0;
sortid: number = 0;
@ -19,12 +26,12 @@ class GroupProperties {
namemode: number = 0;
}
class GroupPermissionRequest {
export class GroupPermissionRequest {
group_id: number;
promise: LaterPromise<PermissionValue[]>;
}
class Group {
export class Group {
properties: GroupProperties = new GroupProperties();
readonly handle: GroupManager;
@ -63,7 +70,7 @@ class Group {
}
}
class GroupManager extends connection.AbstractCommandHandler {
export class GroupManager extends AbstractCommandHandler {
readonly handle: ConnectionHandler;
serverGroups: Group[] = [];
@ -83,7 +90,7 @@ class GroupManager extends connection.AbstractCommandHandler {
this.channelGroups = undefined;
}
handle_command(command: connection.ServerCommand): boolean {
handle_command(command: ServerCommand): boolean {
switch (command.command) {
case "notifyservergrouplist":
case "notifychannelgrouplist":
@ -160,7 +167,7 @@ class GroupManager extends connection.AbstractCommandHandler {
}
let group = new Group(this,parseInt(target == GroupTarget.SERVER ? groupData["sgid"] : groupData["cgid"]), target, type, groupData["name"]);
for(let key in groupData as any) {
for(let key in Object.keys(groupData)) {
if(key == "sgid") continue;
if(key == "cgid") continue;
if(key == "type") continue;

View File

@ -1,358 +1,13 @@
/// <reference path="../ConnectionHandler.ts" />
/// <reference path="../connection/ConnectionBase.ts" />
/// <reference path="../i18n/localize.ts" />
import * as log from "tc-shared/log";
import {LogCategory, LogType} from "tc-shared/log";
import PermissionType from "tc-shared/permission/PermissionType";
import {LaterPromise} from "tc-shared/utils/LaterPromise";
import {ServerCommand} from "tc-shared/connection/ConnectionBase";
import {CommandResult, ErrorID} from "tc-shared/connection/ServerConnectionDeclaration";
import {ConnectionHandler} from "tc-shared/ConnectionHandler";
import {AbstractCommandHandler} from "tc-shared/connection/AbstractCommandHandler";
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",
B_SERVERINSTANCE_VIRTUALSERVER_LIST = "b_serverinstance_virtualserver_list",
B_SERVERINSTANCE_BINDING_LIST = "b_serverinstance_binding_list",
B_SERVERINSTANCE_PERMISSION_LIST = "b_serverinstance_permission_list",
B_SERVERINSTANCE_PERMISSION_FIND = "b_serverinstance_permission_find",
B_VIRTUALSERVER_CREATE = "b_virtualserver_create",
B_VIRTUALSERVER_DELETE = "b_virtualserver_delete",
B_VIRTUALSERVER_START_ANY = "b_virtualserver_start_any",
B_VIRTUALSERVER_STOP_ANY = "b_virtualserver_stop_any",
B_VIRTUALSERVER_CHANGE_MACHINE_ID = "b_virtualserver_change_machine_id",
B_VIRTUALSERVER_CHANGE_TEMPLATE = "b_virtualserver_change_template",
B_SERVERQUERY_LOGIN = "b_serverquery_login",
B_SERVERINSTANCE_TEXTMESSAGE_SEND = "b_serverinstance_textmessage_send",
B_SERVERINSTANCE_LOG_VIEW = "b_serverinstance_log_view",
B_SERVERINSTANCE_LOG_ADD = "b_serverinstance_log_add",
B_SERVERINSTANCE_STOP = "b_serverinstance_stop",
B_SERVERINSTANCE_MODIFY_SETTINGS = "b_serverinstance_modify_settings",
B_SERVERINSTANCE_MODIFY_QUERYGROUP = "b_serverinstance_modify_querygroup",
B_SERVERINSTANCE_MODIFY_TEMPLATES = "b_serverinstance_modify_templates",
B_VIRTUALSERVER_SELECT = "b_virtualserver_select",
B_VIRTUALSERVER_SELECT_GODMODE = "b_virtualserver_select_godmode",
B_VIRTUALSERVER_INFO_VIEW = "b_virtualserver_info_view",
B_VIRTUALSERVER_CONNECTIONINFO_VIEW = "b_virtualserver_connectioninfo_view",
B_VIRTUALSERVER_CHANNEL_LIST = "b_virtualserver_channel_list",
B_VIRTUALSERVER_CHANNEL_SEARCH = "b_virtualserver_channel_search",
B_VIRTUALSERVER_CLIENT_LIST = "b_virtualserver_client_list",
B_VIRTUALSERVER_CLIENT_SEARCH = "b_virtualserver_client_search",
B_VIRTUALSERVER_CLIENT_DBLIST = "b_virtualserver_client_dblist",
B_VIRTUALSERVER_CLIENT_DBSEARCH = "b_virtualserver_client_dbsearch",
B_VIRTUALSERVER_CLIENT_DBINFO = "b_virtualserver_client_dbinfo",
B_VIRTUALSERVER_PERMISSION_FIND = "b_virtualserver_permission_find",
B_VIRTUALSERVER_CUSTOM_SEARCH = "b_virtualserver_custom_search",
B_VIRTUALSERVER_START = "b_virtualserver_start",
B_VIRTUALSERVER_STOP = "b_virtualserver_stop",
B_VIRTUALSERVER_TOKEN_LIST = "b_virtualserver_token_list",
B_VIRTUALSERVER_TOKEN_ADD = "b_virtualserver_token_add",
B_VIRTUALSERVER_TOKEN_USE = "b_virtualserver_token_use",
B_VIRTUALSERVER_TOKEN_DELETE = "b_virtualserver_token_delete",
B_VIRTUALSERVER_LOG_VIEW = "b_virtualserver_log_view",
B_VIRTUALSERVER_LOG_ADD = "b_virtualserver_log_add",
B_VIRTUALSERVER_JOIN_IGNORE_PASSWORD = "b_virtualserver_join_ignore_password",
B_VIRTUALSERVER_NOTIFY_REGISTER = "b_virtualserver_notify_register",
B_VIRTUALSERVER_NOTIFY_UNREGISTER = "b_virtualserver_notify_unregister",
B_VIRTUALSERVER_SNAPSHOT_CREATE = "b_virtualserver_snapshot_create",
B_VIRTUALSERVER_SNAPSHOT_DEPLOY = "b_virtualserver_snapshot_deploy",
B_VIRTUALSERVER_PERMISSION_RESET = "b_virtualserver_permission_reset",
B_VIRTUALSERVER_MODIFY_NAME = "b_virtualserver_modify_name",
B_VIRTUALSERVER_MODIFY_WELCOMEMESSAGE = "b_virtualserver_modify_welcomemessage",
B_VIRTUALSERVER_MODIFY_MAXCLIENTS = "b_virtualserver_modify_maxclients",
B_VIRTUALSERVER_MODIFY_RESERVED_SLOTS = "b_virtualserver_modify_reserved_slots",
B_VIRTUALSERVER_MODIFY_PASSWORD = "b_virtualserver_modify_password",
B_VIRTUALSERVER_MODIFY_DEFAULT_SERVERGROUP = "b_virtualserver_modify_default_servergroup",
B_VIRTUALSERVER_MODIFY_DEFAULT_MUSICGROUP = "b_virtualserver_modify_default_musicgroup",
B_VIRTUALSERVER_MODIFY_DEFAULT_CHANNELGROUP = "b_virtualserver_modify_default_channelgroup",
B_VIRTUALSERVER_MODIFY_DEFAULT_CHANNELADMINGROUP = "b_virtualserver_modify_default_channeladmingroup",
B_VIRTUALSERVER_MODIFY_CHANNEL_FORCED_SILENCE = "b_virtualserver_modify_channel_forced_silence",
B_VIRTUALSERVER_MODIFY_COMPLAIN = "b_virtualserver_modify_complain",
B_VIRTUALSERVER_MODIFY_ANTIFLOOD = "b_virtualserver_modify_antiflood",
B_VIRTUALSERVER_MODIFY_FT_SETTINGS = "b_virtualserver_modify_ft_settings",
B_VIRTUALSERVER_MODIFY_FT_QUOTAS = "b_virtualserver_modify_ft_quotas",
B_VIRTUALSERVER_MODIFY_HOSTMESSAGE = "b_virtualserver_modify_hostmessage",
B_VIRTUALSERVER_MODIFY_HOSTBANNER = "b_virtualserver_modify_hostbanner",
B_VIRTUALSERVER_MODIFY_HOSTBUTTON = "b_virtualserver_modify_hostbutton",
B_VIRTUALSERVER_MODIFY_PORT = "b_virtualserver_modify_port",
B_VIRTUALSERVER_MODIFY_HOST = "b_virtualserver_modify_host",
B_VIRTUALSERVER_MODIFY_DEFAULT_MESSAGES = "b_virtualserver_modify_default_messages",
B_VIRTUALSERVER_MODIFY_AUTOSTART = "b_virtualserver_modify_autostart",
B_VIRTUALSERVER_MODIFY_NEEDED_IDENTITY_SECURITY_LEVEL = "b_virtualserver_modify_needed_identity_security_level",
B_VIRTUALSERVER_MODIFY_PRIORITY_SPEAKER_DIMM_MODIFICATOR = "b_virtualserver_modify_priority_speaker_dimm_modificator",
B_VIRTUALSERVER_MODIFY_LOG_SETTINGS = "b_virtualserver_modify_log_settings",
B_VIRTUALSERVER_MODIFY_MIN_CLIENT_VERSION = "b_virtualserver_modify_min_client_version",
B_VIRTUALSERVER_MODIFY_ICON_ID = "b_virtualserver_modify_icon_id",
B_VIRTUALSERVER_MODIFY_WEBLIST = "b_virtualserver_modify_weblist",
B_VIRTUALSERVER_MODIFY_CODEC_ENCRYPTION_MODE = "b_virtualserver_modify_codec_encryption_mode",
B_VIRTUALSERVER_MODIFY_TEMPORARY_PASSWORDS = "b_virtualserver_modify_temporary_passwords",
B_VIRTUALSERVER_MODIFY_TEMPORARY_PASSWORDS_OWN = "b_virtualserver_modify_temporary_passwords_own",
B_VIRTUALSERVER_MODIFY_CHANNEL_TEMP_DELETE_DELAY_DEFAULT = "b_virtualserver_modify_channel_temp_delete_delay_default",
B_VIRTUALSERVER_MODIFY_MUSIC_BOT_LIMIT = "b_virtualserver_modify_music_bot_limit",
B_VIRTUALSERVER_MODIFY_COUNTRY_CODE = "b_virtualserver_modify_country_code",
I_CHANNEL_MIN_DEPTH = "i_channel_min_depth",
I_CHANNEL_MAX_DEPTH = "i_channel_max_depth",
B_CHANNEL_GROUP_INHERITANCE_END = "b_channel_group_inheritance_end",
I_CHANNEL_PERMISSION_MODIFY_POWER = "i_channel_permission_modify_power",
I_CHANNEL_NEEDED_PERMISSION_MODIFY_POWER = "i_channel_needed_permission_modify_power",
B_CHANNEL_INFO_VIEW = "b_channel_info_view",
B_CHANNEL_CREATE_CHILD = "b_channel_create_child",
B_CHANNEL_CREATE_PERMANENT = "b_channel_create_permanent",
B_CHANNEL_CREATE_SEMI_PERMANENT = "b_channel_create_semi_permanent",
B_CHANNEL_CREATE_TEMPORARY = "b_channel_create_temporary",
B_CHANNEL_CREATE_PRIVATE = "b_channel_create_private",
B_CHANNEL_CREATE_WITH_TOPIC = "b_channel_create_with_topic",
B_CHANNEL_CREATE_WITH_DESCRIPTION = "b_channel_create_with_description",
B_CHANNEL_CREATE_WITH_PASSWORD = "b_channel_create_with_password",
B_CHANNEL_CREATE_MODIFY_WITH_CODEC_SPEEX8 = "b_channel_create_modify_with_codec_speex8",
B_CHANNEL_CREATE_MODIFY_WITH_CODEC_SPEEX16 = "b_channel_create_modify_with_codec_speex16",
B_CHANNEL_CREATE_MODIFY_WITH_CODEC_SPEEX32 = "b_channel_create_modify_with_codec_speex32",
B_CHANNEL_CREATE_MODIFY_WITH_CODEC_CELTMONO48 = "b_channel_create_modify_with_codec_celtmono48",
B_CHANNEL_CREATE_MODIFY_WITH_CODEC_OPUSVOICE = "b_channel_create_modify_with_codec_opusvoice",
B_CHANNEL_CREATE_MODIFY_WITH_CODEC_OPUSMUSIC = "b_channel_create_modify_with_codec_opusmusic",
I_CHANNEL_CREATE_MODIFY_WITH_CODEC_MAXQUALITY = "i_channel_create_modify_with_codec_maxquality",
I_CHANNEL_CREATE_MODIFY_WITH_CODEC_LATENCY_FACTOR_MIN = "i_channel_create_modify_with_codec_latency_factor_min",
I_CHANNEL_CREATE_MODIFY_CONVERSATION_HISTORY_LENGTH = "i_channel_create_modify_conversation_history_length",
B_CHANNEL_CREATE_MODIFY_CONVERSATION_HISTORY_UNLIMITED = "b_channel_create_modify_conversation_history_unlimited",
B_CHANNEL_CREATE_MODIFY_CONVERSATION_PRIVATE = "b_channel_create_modify_conversation_private",
B_CHANNEL_CREATE_WITH_MAXCLIENTS = "b_channel_create_with_maxclients",
B_CHANNEL_CREATE_WITH_MAXFAMILYCLIENTS = "b_channel_create_with_maxfamilyclients",
B_CHANNEL_CREATE_WITH_SORTORDER = "b_channel_create_with_sortorder",
B_CHANNEL_CREATE_WITH_DEFAULT = "b_channel_create_with_default",
B_CHANNEL_CREATE_WITH_NEEDED_TALK_POWER = "b_channel_create_with_needed_talk_power",
B_CHANNEL_CREATE_MODIFY_WITH_FORCE_PASSWORD = "b_channel_create_modify_with_force_password",
I_CHANNEL_CREATE_MODIFY_WITH_TEMP_DELETE_DELAY = "i_channel_create_modify_with_temp_delete_delay",
B_CHANNEL_MODIFY_PARENT = "b_channel_modify_parent",
B_CHANNEL_MODIFY_MAKE_DEFAULT = "b_channel_modify_make_default",
B_CHANNEL_MODIFY_MAKE_PERMANENT = "b_channel_modify_make_permanent",
B_CHANNEL_MODIFY_MAKE_SEMI_PERMANENT = "b_channel_modify_make_semi_permanent",
B_CHANNEL_MODIFY_MAKE_TEMPORARY = "b_channel_modify_make_temporary",
B_CHANNEL_MODIFY_NAME = "b_channel_modify_name",
B_CHANNEL_MODIFY_TOPIC = "b_channel_modify_topic",
B_CHANNEL_MODIFY_DESCRIPTION = "b_channel_modify_description",
B_CHANNEL_MODIFY_PASSWORD = "b_channel_modify_password",
B_CHANNEL_MODIFY_CODEC = "b_channel_modify_codec",
B_CHANNEL_MODIFY_CODEC_QUALITY = "b_channel_modify_codec_quality",
B_CHANNEL_MODIFY_CODEC_LATENCY_FACTOR = "b_channel_modify_codec_latency_factor",
B_CHANNEL_MODIFY_MAXCLIENTS = "b_channel_modify_maxclients",
B_CHANNEL_MODIFY_MAXFAMILYCLIENTS = "b_channel_modify_maxfamilyclients",
B_CHANNEL_MODIFY_SORTORDER = "b_channel_modify_sortorder",
B_CHANNEL_MODIFY_NEEDED_TALK_POWER = "b_channel_modify_needed_talk_power",
I_CHANNEL_MODIFY_POWER = "i_channel_modify_power",
I_CHANNEL_NEEDED_MODIFY_POWER = "i_channel_needed_modify_power",
B_CHANNEL_MODIFY_MAKE_CODEC_ENCRYPTED = "b_channel_modify_make_codec_encrypted",
B_CHANNEL_MODIFY_TEMP_DELETE_DELAY = "b_channel_modify_temp_delete_delay",
B_CHANNEL_DELETE_PERMANENT = "b_channel_delete_permanent",
B_CHANNEL_DELETE_SEMI_PERMANENT = "b_channel_delete_semi_permanent",
B_CHANNEL_DELETE_TEMPORARY = "b_channel_delete_temporary",
B_CHANNEL_DELETE_FLAG_FORCE = "b_channel_delete_flag_force",
I_CHANNEL_DELETE_POWER = "i_channel_delete_power",
B_CHANNEL_CONVERSATION_MESSAGE_DELETE = "b_channel_conversation_message_delete",
I_CHANNEL_NEEDED_DELETE_POWER = "i_channel_needed_delete_power",
B_CHANNEL_JOIN_PERMANENT = "b_channel_join_permanent",
B_CHANNEL_JOIN_SEMI_PERMANENT = "b_channel_join_semi_permanent",
B_CHANNEL_JOIN_TEMPORARY = "b_channel_join_temporary",
B_CHANNEL_JOIN_IGNORE_PASSWORD = "b_channel_join_ignore_password",
B_CHANNEL_JOIN_IGNORE_MAXCLIENTS = "b_channel_join_ignore_maxclients",
B_CHANNEL_IGNORE_VIEW_POWER = "b_channel_ignore_view_power",
I_CHANNEL_JOIN_POWER = "i_channel_join_power",
I_CHANNEL_NEEDED_JOIN_POWER = "i_channel_needed_join_power",
B_CHANNEL_IGNORE_JOIN_POWER = "b_channel_ignore_join_power",
B_CHANNEL_IGNORE_DESCRIPTION_VIEW_POWER = "b_channel_ignore_description_view_power",
I_CHANNEL_VIEW_POWER = "i_channel_view_power",
I_CHANNEL_NEEDED_VIEW_POWER = "i_channel_needed_view_power",
I_CHANNEL_SUBSCRIBE_POWER = "i_channel_subscribe_power",
I_CHANNEL_NEEDED_SUBSCRIBE_POWER = "i_channel_needed_subscribe_power",
I_CHANNEL_DESCRIPTION_VIEW_POWER = "i_channel_description_view_power",
I_CHANNEL_NEEDED_DESCRIPTION_VIEW_POWER = "i_channel_needed_description_view_power",
I_ICON_ID = "i_icon_id",
I_MAX_ICON_FILESIZE = "i_max_icon_filesize",
I_MAX_PLAYLIST_SIZE = "i_max_playlist_size",
I_MAX_PLAYLISTS = "i_max_playlists",
B_ICON_MANAGE = "b_icon_manage",
B_GROUP_IS_PERMANENT = "b_group_is_permanent",
I_GROUP_AUTO_UPDATE_TYPE = "i_group_auto_update_type",
I_GROUP_AUTO_UPDATE_MAX_VALUE = "i_group_auto_update_max_value",
I_GROUP_SORT_ID = "i_group_sort_id",
I_GROUP_SHOW_NAME_IN_TREE = "i_group_show_name_in_tree",
B_VIRTUALSERVER_SERVERGROUP_CREATE = "b_virtualserver_servergroup_create",
B_VIRTUALSERVER_SERVERGROUP_LIST = "b_virtualserver_servergroup_list",
B_VIRTUALSERVER_SERVERGROUP_PERMISSION_LIST = "b_virtualserver_servergroup_permission_list",
B_VIRTUALSERVER_SERVERGROUP_CLIENT_LIST = "b_virtualserver_servergroup_client_list",
B_VIRTUALSERVER_CHANNELGROUP_CREATE = "b_virtualserver_channelgroup_create",
B_VIRTUALSERVER_CHANNELGROUP_LIST = "b_virtualserver_channelgroup_list",
B_VIRTUALSERVER_CHANNELGROUP_PERMISSION_LIST = "b_virtualserver_channelgroup_permission_list",
B_VIRTUALSERVER_CHANNELGROUP_CLIENT_LIST = "b_virtualserver_channelgroup_client_list",
B_VIRTUALSERVER_CLIENT_PERMISSION_LIST = "b_virtualserver_client_permission_list",
B_VIRTUALSERVER_CHANNEL_PERMISSION_LIST = "b_virtualserver_channel_permission_list",
B_VIRTUALSERVER_CHANNELCLIENT_PERMISSION_LIST = "b_virtualserver_channelclient_permission_list",
B_VIRTUALSERVER_PLAYLIST_PERMISSION_LIST = "b_virtualserver_playlist_permission_list",
I_SERVER_GROUP_MODIFY_POWER = "i_server_group_modify_power",
I_SERVER_GROUP_NEEDED_MODIFY_POWER = "i_server_group_needed_modify_power",
I_SERVER_GROUP_MEMBER_ADD_POWER = "i_server_group_member_add_power",
I_SERVER_GROUP_SELF_ADD_POWER = "i_server_group_self_add_power",
I_SERVER_GROUP_NEEDED_MEMBER_ADD_POWER = "i_server_group_needed_member_add_power",
I_SERVER_GROUP_MEMBER_REMOVE_POWER = "i_server_group_member_remove_power",
I_SERVER_GROUP_SELF_REMOVE_POWER = "i_server_group_self_remove_power",
I_SERVER_GROUP_NEEDED_MEMBER_REMOVE_POWER = "i_server_group_needed_member_remove_power",
I_CHANNEL_GROUP_MODIFY_POWER = "i_channel_group_modify_power",
I_CHANNEL_GROUP_NEEDED_MODIFY_POWER = "i_channel_group_needed_modify_power",
I_CHANNEL_GROUP_MEMBER_ADD_POWER = "i_channel_group_member_add_power",
I_CHANNEL_GROUP_SELF_ADD_POWER = "i_channel_group_self_add_power",
I_CHANNEL_GROUP_NEEDED_MEMBER_ADD_POWER = "i_channel_group_needed_member_add_power",
I_CHANNEL_GROUP_MEMBER_REMOVE_POWER = "i_channel_group_member_remove_power",
I_CHANNEL_GROUP_SELF_REMOVE_POWER = "i_channel_group_self_remove_power",
I_CHANNEL_GROUP_NEEDED_MEMBER_REMOVE_POWER = "i_channel_group_needed_member_remove_power",
I_GROUP_MEMBER_ADD_POWER = "i_group_member_add_power",
I_GROUP_NEEDED_MEMBER_ADD_POWER = "i_group_needed_member_add_power",
I_GROUP_MEMBER_REMOVE_POWER = "i_group_member_remove_power",
I_GROUP_NEEDED_MEMBER_REMOVE_POWER = "i_group_needed_member_remove_power",
I_GROUP_MODIFY_POWER = "i_group_modify_power",
I_GROUP_NEEDED_MODIFY_POWER = "i_group_needed_modify_power",
I_PERMISSION_MODIFY_POWER = "i_permission_modify_power",
B_PERMISSION_MODIFY_POWER_IGNORE = "b_permission_modify_power_ignore",
B_VIRTUALSERVER_SERVERGROUP_DELETE = "b_virtualserver_servergroup_delete",
B_VIRTUALSERVER_CHANNELGROUP_DELETE = "b_virtualserver_channelgroup_delete",
I_CLIENT_PERMISSION_MODIFY_POWER = "i_client_permission_modify_power",
I_CLIENT_NEEDED_PERMISSION_MODIFY_POWER = "i_client_needed_permission_modify_power",
I_CLIENT_MAX_CLONES_UID = "i_client_max_clones_uid",
I_CLIENT_MAX_CLONES_IP = "i_client_max_clones_ip",
I_CLIENT_MAX_CLONES_HWID = "i_client_max_clones_hwid",
I_CLIENT_MAX_IDLETIME = "i_client_max_idletime",
I_CLIENT_MAX_AVATAR_FILESIZE = "i_client_max_avatar_filesize",
I_CLIENT_MAX_CHANNEL_SUBSCRIPTIONS = "i_client_max_channel_subscriptions",
I_CLIENT_MAX_CHANNELS = "i_client_max_channels",
I_CLIENT_MAX_TEMPORARY_CHANNELS = "i_client_max_temporary_channels",
I_CLIENT_MAX_SEMI_CHANNELS = "i_client_max_semi_channels",
I_CLIENT_MAX_PERMANENT_CHANNELS = "i_client_max_permanent_channels",
B_CLIENT_USE_PRIORITY_SPEAKER = "b_client_use_priority_speaker",
B_CLIENT_SKIP_CHANNELGROUP_PERMISSIONS = "b_client_skip_channelgroup_permissions",
B_CLIENT_FORCE_PUSH_TO_TALK = "b_client_force_push_to_talk",
B_CLIENT_IGNORE_BANS = "b_client_ignore_bans",
B_CLIENT_IGNORE_VPN = "b_client_ignore_vpn",
B_CLIENT_IGNORE_ANTIFLOOD = "b_client_ignore_antiflood",
B_CLIENT_ENFORCE_VALID_HWID = "b_client_enforce_valid_hwid",
B_CLIENT_ALLOW_INVALID_PACKET = "b_client_allow_invalid_packet",
B_CLIENT_ALLOW_INVALID_BADGES = "b_client_allow_invalid_badges",
B_CLIENT_ISSUE_CLIENT_QUERY_COMMAND = "b_client_issue_client_query_command",
B_CLIENT_USE_RESERVED_SLOT = "b_client_use_reserved_slot",
B_CLIENT_USE_CHANNEL_COMMANDER = "b_client_use_channel_commander",
B_CLIENT_REQUEST_TALKER = "b_client_request_talker",
B_CLIENT_AVATAR_DELETE_OTHER = "b_client_avatar_delete_other",
B_CLIENT_IS_STICKY = "b_client_is_sticky",
B_CLIENT_IGNORE_STICKY = "b_client_ignore_sticky",
B_CLIENT_MUSIC_CREATE_PERMANENT = "b_client_music_create_permanent",
B_CLIENT_MUSIC_CREATE_SEMI_PERMANENT = "b_client_music_create_semi_permanent",
B_CLIENT_MUSIC_CREATE_TEMPORARY = "b_client_music_create_temporary",
B_CLIENT_MUSIC_MODIFY_PERMANENT = "b_client_music_modify_permanent",
B_CLIENT_MUSIC_MODIFY_SEMI_PERMANENT = "b_client_music_modify_semi_permanent",
B_CLIENT_MUSIC_MODIFY_TEMPORARY = "b_client_music_modify_temporary",
I_CLIENT_MUSIC_CREATE_MODIFY_MAX_VOLUME = "i_client_music_create_modify_max_volume",
I_CLIENT_MUSIC_LIMIT = "i_client_music_limit",
I_CLIENT_MUSIC_NEEDED_DELETE_POWER = "i_client_music_needed_delete_power",
I_CLIENT_MUSIC_DELETE_POWER = "i_client_music_delete_power",
I_CLIENT_MUSIC_PLAY_POWER = "i_client_music_play_power",
I_CLIENT_MUSIC_NEEDED_PLAY_POWER = "i_client_music_needed_play_power",
I_CLIENT_MUSIC_MODIFY_POWER = "i_client_music_modify_power",
I_CLIENT_MUSIC_NEEDED_MODIFY_POWER = "i_client_music_needed_modify_power",
I_CLIENT_MUSIC_RENAME_POWER = "i_client_music_rename_power",
I_CLIENT_MUSIC_NEEDED_RENAME_POWER = "i_client_music_needed_rename_power",
B_PLAYLIST_CREATE = "b_playlist_create",
I_PLAYLIST_VIEW_POWER = "i_playlist_view_power",
I_PLAYLIST_NEEDED_VIEW_POWER = "i_playlist_needed_view_power",
I_PLAYLIST_MODIFY_POWER = "i_playlist_modify_power",
I_PLAYLIST_NEEDED_MODIFY_POWER = "i_playlist_needed_modify_power",
I_PLAYLIST_PERMISSION_MODIFY_POWER = "i_playlist_permission_modify_power",
I_PLAYLIST_NEEDED_PERMISSION_MODIFY_POWER = "i_playlist_needed_permission_modify_power",
I_PLAYLIST_DELETE_POWER = "i_playlist_delete_power",
I_PLAYLIST_NEEDED_DELETE_POWER = "i_playlist_needed_delete_power",
I_PLAYLIST_SONG_ADD_POWER = "i_playlist_song_add_power",
I_PLAYLIST_SONG_NEEDED_ADD_POWER = "i_playlist_song_needed_add_power",
I_PLAYLIST_SONG_REMOVE_POWER = "i_playlist_song_remove_power",
I_PLAYLIST_SONG_NEEDED_REMOVE_POWER = "i_playlist_song_needed_remove_power",
B_CLIENT_INFO_VIEW = "b_client_info_view",
B_CLIENT_PERMISSIONOVERVIEW_VIEW = "b_client_permissionoverview_view",
B_CLIENT_PERMISSIONOVERVIEW_OWN = "b_client_permissionoverview_own",
B_CLIENT_REMOTEADDRESS_VIEW = "b_client_remoteaddress_view",
I_CLIENT_SERVERQUERY_VIEW_POWER = "i_client_serverquery_view_power",
I_CLIENT_NEEDED_SERVERQUERY_VIEW_POWER = "i_client_needed_serverquery_view_power",
B_CLIENT_CUSTOM_INFO_VIEW = "b_client_custom_info_view",
B_CLIENT_MUSIC_CHANNEL_LIST = "b_client_music_channel_list",
B_CLIENT_MUSIC_SERVER_LIST = "b_client_music_server_list",
I_CLIENT_MUSIC_INFO = "i_client_music_info",
I_CLIENT_MUSIC_NEEDED_INFO = "i_client_music_needed_info",
I_CLIENT_KICK_FROM_SERVER_POWER = "i_client_kick_from_server_power",
I_CLIENT_NEEDED_KICK_FROM_SERVER_POWER = "i_client_needed_kick_from_server_power",
I_CLIENT_KICK_FROM_CHANNEL_POWER = "i_client_kick_from_channel_power",
I_CLIENT_NEEDED_KICK_FROM_CHANNEL_POWER = "i_client_needed_kick_from_channel_power",
I_CLIENT_BAN_POWER = "i_client_ban_power",
I_CLIENT_NEEDED_BAN_POWER = "i_client_needed_ban_power",
I_CLIENT_MOVE_POWER = "i_client_move_power",
I_CLIENT_NEEDED_MOVE_POWER = "i_client_needed_move_power",
I_CLIENT_COMPLAIN_POWER = "i_client_complain_power",
I_CLIENT_NEEDED_COMPLAIN_POWER = "i_client_needed_complain_power",
B_CLIENT_COMPLAIN_LIST = "b_client_complain_list",
B_CLIENT_COMPLAIN_DELETE_OWN = "b_client_complain_delete_own",
B_CLIENT_COMPLAIN_DELETE = "b_client_complain_delete",
B_CLIENT_BAN_LIST = "b_client_ban_list",
B_CLIENT_BAN_LIST_GLOBAL = "b_client_ban_list_global",
B_CLIENT_BAN_TRIGGER_LIST = "b_client_ban_trigger_list",
B_CLIENT_BAN_CREATE = "b_client_ban_create",
B_CLIENT_BAN_CREATE_GLOBAL = "b_client_ban_create_global",
B_CLIENT_BAN_NAME = "b_client_ban_name",
B_CLIENT_BAN_IP = "b_client_ban_ip",
B_CLIENT_BAN_HWID = "b_client_ban_hwid",
B_CLIENT_BAN_EDIT = "b_client_ban_edit",
B_CLIENT_BAN_EDIT_GLOBAL = "b_client_ban_edit_global",
B_CLIENT_BAN_DELETE_OWN = "b_client_ban_delete_own",
B_CLIENT_BAN_DELETE = "b_client_ban_delete",
B_CLIENT_BAN_DELETE_OWN_GLOBAL = "b_client_ban_delete_own_global",
B_CLIENT_BAN_DELETE_GLOBAL = "b_client_ban_delete_global",
I_CLIENT_BAN_MAX_BANTIME = "i_client_ban_max_bantime",
I_CLIENT_PRIVATE_TEXTMESSAGE_POWER = "i_client_private_textmessage_power",
I_CLIENT_NEEDED_PRIVATE_TEXTMESSAGE_POWER = "i_client_needed_private_textmessage_power",
B_CLIENT_EVEN_TEXTMESSAGE_SEND = "b_client_even_textmessage_send",
B_CLIENT_SERVER_TEXTMESSAGE_SEND = "b_client_server_textmessage_send",
B_CLIENT_CHANNEL_TEXTMESSAGE_SEND = "b_client_channel_textmessage_send",
B_CLIENT_OFFLINE_TEXTMESSAGE_SEND = "b_client_offline_textmessage_send",
I_CLIENT_TALK_POWER = "i_client_talk_power",
I_CLIENT_NEEDED_TALK_POWER = "i_client_needed_talk_power",
I_CLIENT_POKE_POWER = "i_client_poke_power",
I_CLIENT_NEEDED_POKE_POWER = "i_client_needed_poke_power",
B_CLIENT_SET_FLAG_TALKER = "b_client_set_flag_talker",
I_CLIENT_WHISPER_POWER = "i_client_whisper_power",
I_CLIENT_NEEDED_WHISPER_POWER = "i_client_needed_whisper_power",
B_CLIENT_MODIFY_DESCRIPTION = "b_client_modify_description",
B_CLIENT_MODIFY_OWN_DESCRIPTION = "b_client_modify_own_description",
B_CLIENT_USE_BBCODE_ANY = "b_client_use_bbcode_any",
B_CLIENT_USE_BBCODE_URL = "b_client_use_bbcode_url",
B_CLIENT_USE_BBCODE_IMAGE = "b_client_use_bbcode_image",
B_CLIENT_MODIFY_DBPROPERTIES = "b_client_modify_dbproperties",
B_CLIENT_DELETE_DBPROPERTIES = "b_client_delete_dbproperties",
B_CLIENT_CREATE_MODIFY_SERVERQUERY_LOGIN = "b_client_create_modify_serverquery_login",
B_CLIENT_QUERY_CREATE = "b_client_query_create",
B_CLIENT_QUERY_LIST = "b_client_query_list",
B_CLIENT_QUERY_LIST_OWN = "b_client_query_list_own",
B_CLIENT_QUERY_RENAME = "b_client_query_rename",
B_CLIENT_QUERY_RENAME_OWN = "b_client_query_rename_own",
B_CLIENT_QUERY_CHANGE_PASSWORD = "b_client_query_change_password",
B_CLIENT_QUERY_CHANGE_OWN_PASSWORD = "b_client_query_change_own_password",
B_CLIENT_QUERY_CHANGE_PASSWORD_GLOBAL = "b_client_query_change_password_global",
B_CLIENT_QUERY_DELETE = "b_client_query_delete",
B_CLIENT_QUERY_DELETE_OWN = "b_client_query_delete_own",
B_FT_IGNORE_PASSWORD = "b_ft_ignore_password",
B_FT_TRANSFER_LIST = "b_ft_transfer_list",
I_FT_FILE_UPLOAD_POWER = "i_ft_file_upload_power",
I_FT_NEEDED_FILE_UPLOAD_POWER = "i_ft_needed_file_upload_power",
I_FT_FILE_DOWNLOAD_POWER = "i_ft_file_download_power",
I_FT_NEEDED_FILE_DOWNLOAD_POWER = "i_ft_needed_file_download_power",
I_FT_FILE_DELETE_POWER = "i_ft_file_delete_power",
I_FT_NEEDED_FILE_DELETE_POWER = "i_ft_needed_file_delete_power",
I_FT_FILE_RENAME_POWER = "i_ft_file_rename_power",
I_FT_NEEDED_FILE_RENAME_POWER = "i_ft_needed_file_rename_power",
I_FT_FILE_BROWSE_POWER = "i_ft_file_browse_power",
I_FT_NEEDED_FILE_BROWSE_POWER = "i_ft_needed_file_browse_power",
I_FT_DIRECTORY_CREATE_POWER = "i_ft_directory_create_power",
I_FT_NEEDED_DIRECTORY_CREATE_POWER = "i_ft_needed_directory_create_power",
I_FT_QUOTA_MB_DOWNLOAD_PER_CLIENT = "i_ft_quota_mb_download_per_client",
I_FT_QUOTA_MB_UPLOAD_PER_CLIENT = "i_ft_quota_mb_upload_per_client"
}
class PermissionInfo {
export class PermissionInfo {
name: string;
id: number;
description: string;
@ -363,21 +18,21 @@ class PermissionInfo {
}
}
class PermissionGroup {
export class PermissionGroup {
begin: number;
end: number;
deep: number;
name: string;
}
class GroupedPermissions {
export class GroupedPermissions {
group: PermissionGroup;
permissions: PermissionInfo[];
children: GroupedPermissions[];
parent: GroupedPermissions;
}
class PermissionValue {
export class PermissionValue {
readonly type: PermissionInfo;
value: number;
flag_skip: boolean;
@ -411,75 +66,74 @@ class PermissionValue {
}
}
class NeededPermissionValue extends PermissionValue {
export class NeededPermissionValue extends PermissionValue {
constructor(type, value) {
super(type, value);
}
}
namespace permissions {
export type PermissionRequestKeys = {
client_id?: number;
channel_id?: number;
playlist_id?: number;
export type PermissionRequestKeys = {
client_id?: number;
channel_id?: number;
playlist_id?: number;
}
export type PermissionRequest = PermissionRequestKeys & {
timeout_id: any;
promise: LaterPromise<PermissionValue[]>;
};
export namespace find {
export type Entry = {
type: "server" | "channel" | "client" | "client_channel" | "channel_group" | "server_group";
value: number;
id: number;
}
export type PermissionRequest = PermissionRequestKeys & {
timeout_id: any;
promise: LaterPromise<PermissionValue[]>;
};
export type Client = Entry & {
type: "client",
export namespace find {
export type Entry = {
type: "server" | "channel" | "client" | "client_channel" | "channel_group" | "server_group";
value: number;
id: number;
}
client_id: number;
}
export type Client = Entry & {
type: "client",
export type Channel = Entry & {
type: "channel",
client_id: number;
}
channel_id: number;
}
export type Channel = Entry & {
type: "channel",
export type Server = Entry & {
type: "server"
}
channel_id: number;
}
export type ClientChannel = Entry & {
type: "client_channel",
export type Server = Entry & {
type: "server"
}
client_id: number;
channel_id: number;
}
export type ClientChannel = Entry & {
type: "client_channel",
export type ChannelGroup = Entry & {
type: "channel_group",
client_id: number;
channel_id: number;
}
group_id: number;
}
export type ChannelGroup = Entry & {
type: "channel_group",
export type ServerGroup = Entry & {
type: "server_group",
group_id: number;
}
export type ServerGroup = Entry & {
type: "server_group",
group_id: number;
}
group_id: number;
}
}
type RequestLists =
export type RequestLists =
"requests_channel_permissions" |
"requests_client_permissions" |
"requests_client_channel_permissions" |
"requests_playlist_permissions" |
"requests_playlist_client_permissions";
class PermissionManager extends connection.AbstractCommandHandler {
export class PermissionManager extends AbstractCommandHandler {
readonly handle: ConnectionHandler;
permissionList: PermissionInfo[] = [];
@ -488,11 +142,11 @@ class PermissionManager extends connection.AbstractCommandHandler {
needed_permission_change_listener: {[permission: string]:(() => any)[]} = {};
requests_channel_permissions: permissions.PermissionRequest[] = [];
requests_client_permissions: permissions.PermissionRequest[] = [];
requests_client_channel_permissions: permissions.PermissionRequest[] = [];
requests_playlist_permissions: permissions.PermissionRequest[] = [];
requests_playlist_client_permissions: permissions.PermissionRequest[] = [];
requests_channel_permissions: PermissionRequest[] = [];
requests_client_permissions: PermissionRequest[] = [];
requests_client_channel_permissions: PermissionRequest[] = [];
requests_playlist_permissions: PermissionRequest[] = [];
requests_playlist_client_permissions: PermissionRequest[] = [];
requests_permfind: {
timeout_id: number,
@ -603,7 +257,7 @@ class PermissionManager extends connection.AbstractCommandHandler {
this._cacheNeededPermissions = undefined;
}
handle_command(command: connection.ServerCommand): boolean {
handle_command(command: ServerCommand): boolean {
switch (command.command) {
case "notifyclientneededpermissions":
this.onNeededPermissions(command.arguments);
@ -775,7 +429,7 @@ class PermissionManager extends connection.AbstractCommandHandler {
}, "success", PermissionManager.parse_permission_bulk(json, this.handle.permissions));
}
private execute_channel_permission_request(request: permissions.PermissionRequestKeys) {
private execute_channel_permission_request(request: PermissionRequestKeys) {
this.handle.serverConnection.send_command("channelpermlist", {"cid": request.channel_id}).catch(error => {
if(error instanceof CommandResult && error.id == ErrorID.EMPTY_RESULT)
this.fullfill_permission_request("requests_channel_permissions", request, "success", []);
@ -785,7 +439,7 @@ class PermissionManager extends connection.AbstractCommandHandler {
}
requestChannelPermissions(channelId: number) : Promise<PermissionValue[]> {
const keys: permissions.PermissionRequestKeys = {
const keys: PermissionRequestKeys = {
channel_id: channelId
};
return this.execute_permission_request("requests_channel_permissions", keys, this.execute_channel_permission_request.bind(this));
@ -799,7 +453,7 @@ class PermissionManager extends connection.AbstractCommandHandler {
}, "success", PermissionManager.parse_permission_bulk(json, this.handle.permissions));
}
private execute_client_permission_request(request: permissions.PermissionRequestKeys) {
private execute_client_permission_request(request: PermissionRequestKeys) {
this.handle.serverConnection.send_command("clientpermlist", {cldbid: request.client_id}).catch(error => {
if(error instanceof CommandResult && error.id == ErrorID.EMPTY_RESULT)
this.fullfill_permission_request("requests_client_permissions", request, "success", []);
@ -809,7 +463,7 @@ class PermissionManager extends connection.AbstractCommandHandler {
}
requestClientPermissions(client_id: number) : Promise<PermissionValue[]> {
const keys: permissions.PermissionRequestKeys = {
const keys: PermissionRequestKeys = {
client_id: client_id
};
return this.execute_permission_request("requests_client_permissions", keys, this.execute_client_permission_request.bind(this));
@ -826,7 +480,7 @@ class PermissionManager extends connection.AbstractCommandHandler {
}, "success", PermissionManager.parse_permission_bulk(json, this.handle.permissions));
}
private execute_client_channel_permission_request(request: permissions.PermissionRequestKeys) {
private execute_client_channel_permission_request(request: PermissionRequestKeys) {
this.handle.serverConnection.send_command("channelclientpermlist", {cldbid: request.client_id, cid: request.channel_id})
.catch(error => {
if(error instanceof CommandResult && error.id == ErrorID.EMPTY_RESULT)
@ -837,7 +491,7 @@ class PermissionManager extends connection.AbstractCommandHandler {
}
requestClientChannelPermissions(client_id: number, channel_id: number) : Promise<PermissionValue[]> {
const keys: permissions.PermissionRequestKeys = {
const keys: PermissionRequestKeys = {
client_id: client_id
};
return this.execute_permission_request("requests_client_channel_permissions", keys, this.execute_client_channel_permission_request.bind(this));
@ -852,7 +506,7 @@ class PermissionManager extends connection.AbstractCommandHandler {
}, "success", PermissionManager.parse_permission_bulk(json, this.handle.permissions));
}
private execute_playlist_permission_request(request: permissions.PermissionRequestKeys) {
private execute_playlist_permission_request(request: PermissionRequestKeys) {
this.handle.serverConnection.send_command("playlistpermlist", {playlist_id: request.playlist_id})
.catch(error => {
if(error instanceof CommandResult && error.id == ErrorID.EMPTY_RESULT)
@ -863,7 +517,7 @@ class PermissionManager extends connection.AbstractCommandHandler {
}
requestPlaylistPermissions(playlist_id: number) : Promise<PermissionValue[]> {
const keys: permissions.PermissionRequestKeys = {
const keys: PermissionRequestKeys = {
playlist_id: playlist_id
};
return this.execute_permission_request("requests_playlist_permissions", keys, this.execute_playlist_permission_request.bind(this));
@ -880,7 +534,7 @@ class PermissionManager extends connection.AbstractCommandHandler {
}, "success", PermissionManager.parse_permission_bulk(json, this.handle.permissions));
}
private execute_playlist_client_permission_request(request: permissions.PermissionRequestKeys) {
private execute_playlist_client_permission_request(request: PermissionRequestKeys) {
this.handle.serverConnection.send_command("playlistclientpermlist", {playlist_id: request.playlist_id, cldbid: request.client_id})
.catch(error => {
if(error instanceof CommandResult && error.id == ErrorID.EMPTY_RESULT)
@ -891,7 +545,7 @@ class PermissionManager extends connection.AbstractCommandHandler {
}
requestPlaylistClientPermissions(playlist_id: number, client_database_id: number) : Promise<PermissionValue[]> {
const keys: permissions.PermissionRequestKeys = {
const keys: PermissionRequestKeys = {
playlist_id: playlist_id,
client_id: client_database_id
};
@ -907,8 +561,8 @@ class PermissionManager extends connection.AbstractCommandHandler {
};
private execute_permission_request(list: RequestLists,
criteria: permissions.PermissionRequestKeys,
execute: (criteria: permissions.PermissionRequestKeys) => any) : Promise<PermissionValue[]> {
criteria: PermissionRequestKeys,
execute: (criteria: PermissionRequestKeys) => any) : Promise<PermissionValue[]> {
for(const request of this[list])
if(this.criteria_equal(request, criteria) && request.promise.time() + 1000 < Date.now())
return request.promise;
@ -922,7 +576,7 @@ class PermissionManager extends connection.AbstractCommandHandler {
return result.promise;
};
private fullfill_permission_request(list: RequestLists, criteria: permissions.PermissionRequestKeys, status: "success" | "error", result: any) {
private fullfill_permission_request(list: RequestLists, criteria: PermissionRequestKeys, status: "success" | "error", result: any) {
for(const request of this[list]) {
if(this.criteria_equal(request, criteria)) {
this[list].remove(request);
@ -932,7 +586,7 @@ class PermissionManager extends connection.AbstractCommandHandler {
}
}
find_permission(...permissions: string[]) : Promise<permissions.find.Entry[]> {
find_permission(...permissions: string[]) : Promise<find.Entry[]> {
const permission_ids = [];
for(const permission of permissions) {
const info = this.resolveInfo(permission);
@ -942,11 +596,11 @@ class PermissionManager extends connection.AbstractCommandHandler {
}
if(!permission_ids.length) return Promise.resolve([]);
return new Promise<permissions.find.Entry[]>((resolve, reject) => {
return new Promise<find.Entry[]>((resolve, reject) => {
const single_handler = {
command: "notifypermfind",
function: command => {
const result: permissions.find.Entry[] = [];
const result: find.Entry[] = [];
for(const entry of command.arguments) {
const perm_id = parseInt(entry["p"]);
if(permission_ids.indexOf(perm_id) === -1) return; /* not our permfind result */
@ -960,32 +614,32 @@ class PermissionManager extends connection.AbstractCommandHandler {
data = {
type: "server_group",
group_id: parseInt(entry["id1"]),
} as permissions.find.ServerGroup;
} as find.ServerGroup;
break;
case 1:
data = {
type: "client",
client_id: parseInt(entry["id2"]),
} as permissions.find.Client;
} as find.Client;
break;
case 2:
data = {
type: "channel",
channel_id: parseInt(entry["id2"]),
} as permissions.find.Channel;
} as find.Channel;
break;
case 3:
data = {
type: "channel_group",
group_id: parseInt(entry["id1"]),
} as permissions.find.ChannelGroup;
} as find.ChannelGroup;
break;
case 4:
data = {
type: "client_channel",
client_id: parseInt(entry["id1"]),
channel_id: parseInt(entry["id1"]),
} as permissions.find.ClientChannel;
} as find.ClientChannel;
break;
default:
continue;

View File

@ -0,0 +1,350 @@
export 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",
B_SERVERINSTANCE_VIRTUALSERVER_LIST = "b_serverinstance_virtualserver_list",
B_SERVERINSTANCE_BINDING_LIST = "b_serverinstance_binding_list",
B_SERVERINSTANCE_PERMISSION_LIST = "b_serverinstance_permission_list",
B_SERVERINSTANCE_PERMISSION_FIND = "b_serverinstance_permission_find",
B_VIRTUALSERVER_CREATE = "b_virtualserver_create",
B_VIRTUALSERVER_DELETE = "b_virtualserver_delete",
B_VIRTUALSERVER_START_ANY = "b_virtualserver_start_any",
B_VIRTUALSERVER_STOP_ANY = "b_virtualserver_stop_any",
B_VIRTUALSERVER_CHANGE_MACHINE_ID = "b_virtualserver_change_machine_id",
B_VIRTUALSERVER_CHANGE_TEMPLATE = "b_virtualserver_change_template",
B_SERVERQUERY_LOGIN = "b_serverquery_login",
B_SERVERINSTANCE_TEXTMESSAGE_SEND = "b_serverinstance_textmessage_send",
B_SERVERINSTANCE_LOG_VIEW = "b_serverinstance_log_view",
B_SERVERINSTANCE_LOG_ADD = "b_serverinstance_log_add",
B_SERVERINSTANCE_STOP = "b_serverinstance_stop",
B_SERVERINSTANCE_MODIFY_SETTINGS = "b_serverinstance_modify_settings",
B_SERVERINSTANCE_MODIFY_QUERYGROUP = "b_serverinstance_modify_querygroup",
B_SERVERINSTANCE_MODIFY_TEMPLATES = "b_serverinstance_modify_templates",
B_VIRTUALSERVER_SELECT = "b_virtualserver_select",
B_VIRTUALSERVER_SELECT_GODMODE = "b_virtualserver_select_godmode",
B_VIRTUALSERVER_INFO_VIEW = "b_virtualserver_info_view",
B_VIRTUALSERVER_CONNECTIONINFO_VIEW = "b_virtualserver_connectioninfo_view",
B_VIRTUALSERVER_CHANNEL_LIST = "b_virtualserver_channel_list",
B_VIRTUALSERVER_CHANNEL_SEARCH = "b_virtualserver_channel_search",
B_VIRTUALSERVER_CLIENT_LIST = "b_virtualserver_client_list",
B_VIRTUALSERVER_CLIENT_SEARCH = "b_virtualserver_client_search",
B_VIRTUALSERVER_CLIENT_DBLIST = "b_virtualserver_client_dblist",
B_VIRTUALSERVER_CLIENT_DBSEARCH = "b_virtualserver_client_dbsearch",
B_VIRTUALSERVER_CLIENT_DBINFO = "b_virtualserver_client_dbinfo",
B_VIRTUALSERVER_PERMISSION_FIND = "b_virtualserver_permission_find",
B_VIRTUALSERVER_CUSTOM_SEARCH = "b_virtualserver_custom_search",
B_VIRTUALSERVER_START = "b_virtualserver_start",
B_VIRTUALSERVER_STOP = "b_virtualserver_stop",
B_VIRTUALSERVER_TOKEN_LIST = "b_virtualserver_token_list",
B_VIRTUALSERVER_TOKEN_ADD = "b_virtualserver_token_add",
B_VIRTUALSERVER_TOKEN_USE = "b_virtualserver_token_use",
B_VIRTUALSERVER_TOKEN_DELETE = "b_virtualserver_token_delete",
B_VIRTUALSERVER_LOG_VIEW = "b_virtualserver_log_view",
B_VIRTUALSERVER_LOG_ADD = "b_virtualserver_log_add",
B_VIRTUALSERVER_JOIN_IGNORE_PASSWORD = "b_virtualserver_join_ignore_password",
B_VIRTUALSERVER_NOTIFY_REGISTER = "b_virtualserver_notify_register",
B_VIRTUALSERVER_NOTIFY_UNREGISTER = "b_virtualserver_notify_unregister",
B_VIRTUALSERVER_SNAPSHOT_CREATE = "b_virtualserver_snapshot_create",
B_VIRTUALSERVER_SNAPSHOT_DEPLOY = "b_virtualserver_snapshot_deploy",
B_VIRTUALSERVER_PERMISSION_RESET = "b_virtualserver_permission_reset",
B_VIRTUALSERVER_MODIFY_NAME = "b_virtualserver_modify_name",
B_VIRTUALSERVER_MODIFY_WELCOMEMESSAGE = "b_virtualserver_modify_welcomemessage",
B_VIRTUALSERVER_MODIFY_MAXCLIENTS = "b_virtualserver_modify_maxclients",
B_VIRTUALSERVER_MODIFY_RESERVED_SLOTS = "b_virtualserver_modify_reserved_slots",
B_VIRTUALSERVER_MODIFY_PASSWORD = "b_virtualserver_modify_password",
B_VIRTUALSERVER_MODIFY_DEFAULT_SERVERGROUP = "b_virtualserver_modify_default_servergroup",
B_VIRTUALSERVER_MODIFY_DEFAULT_MUSICGROUP = "b_virtualserver_modify_default_musicgroup",
B_VIRTUALSERVER_MODIFY_DEFAULT_CHANNELGROUP = "b_virtualserver_modify_default_channelgroup",
B_VIRTUALSERVER_MODIFY_DEFAULT_CHANNELADMINGROUP = "b_virtualserver_modify_default_channeladmingroup",
B_VIRTUALSERVER_MODIFY_CHANNEL_FORCED_SILENCE = "b_virtualserver_modify_channel_forced_silence",
B_VIRTUALSERVER_MODIFY_COMPLAIN = "b_virtualserver_modify_complain",
B_VIRTUALSERVER_MODIFY_ANTIFLOOD = "b_virtualserver_modify_antiflood",
B_VIRTUALSERVER_MODIFY_FT_SETTINGS = "b_virtualserver_modify_ft_settings",
B_VIRTUALSERVER_MODIFY_FT_QUOTAS = "b_virtualserver_modify_ft_quotas",
B_VIRTUALSERVER_MODIFY_HOSTMESSAGE = "b_virtualserver_modify_hostmessage",
B_VIRTUALSERVER_MODIFY_HOSTBANNER = "b_virtualserver_modify_hostbanner",
B_VIRTUALSERVER_MODIFY_HOSTBUTTON = "b_virtualserver_modify_hostbutton",
B_VIRTUALSERVER_MODIFY_PORT = "b_virtualserver_modify_port",
B_VIRTUALSERVER_MODIFY_HOST = "b_virtualserver_modify_host",
B_VIRTUALSERVER_MODIFY_DEFAULT_MESSAGES = "b_virtualserver_modify_default_messages",
B_VIRTUALSERVER_MODIFY_AUTOSTART = "b_virtualserver_modify_autostart",
B_VIRTUALSERVER_MODIFY_NEEDED_IDENTITY_SECURITY_LEVEL = "b_virtualserver_modify_needed_identity_security_level",
B_VIRTUALSERVER_MODIFY_PRIORITY_SPEAKER_DIMM_MODIFICATOR = "b_virtualserver_modify_priority_speaker_dimm_modificator",
B_VIRTUALSERVER_MODIFY_LOG_SETTINGS = "b_virtualserver_modify_log_settings",
B_VIRTUALSERVER_MODIFY_MIN_CLIENT_VERSION = "b_virtualserver_modify_min_client_version",
B_VIRTUALSERVER_MODIFY_ICON_ID = "b_virtualserver_modify_icon_id",
B_VIRTUALSERVER_MODIFY_WEBLIST = "b_virtualserver_modify_weblist",
B_VIRTUALSERVER_MODIFY_CODEC_ENCRYPTION_MODE = "b_virtualserver_modify_codec_encryption_mode",
B_VIRTUALSERVER_MODIFY_TEMPORARY_PASSWORDS = "b_virtualserver_modify_temporary_passwords",
B_VIRTUALSERVER_MODIFY_TEMPORARY_PASSWORDS_OWN = "b_virtualserver_modify_temporary_passwords_own",
B_VIRTUALSERVER_MODIFY_CHANNEL_TEMP_DELETE_DELAY_DEFAULT = "b_virtualserver_modify_channel_temp_delete_delay_default",
B_VIRTUALSERVER_MODIFY_MUSIC_BOT_LIMIT = "b_virtualserver_modify_music_bot_limit",
B_VIRTUALSERVER_MODIFY_COUNTRY_CODE = "b_virtualserver_modify_country_code",
I_CHANNEL_MIN_DEPTH = "i_channel_min_depth",
I_CHANNEL_MAX_DEPTH = "i_channel_max_depth",
B_CHANNEL_GROUP_INHERITANCE_END = "b_channel_group_inheritance_end",
I_CHANNEL_PERMISSION_MODIFY_POWER = "i_channel_permission_modify_power",
I_CHANNEL_NEEDED_PERMISSION_MODIFY_POWER = "i_channel_needed_permission_modify_power",
B_CHANNEL_INFO_VIEW = "b_channel_info_view",
B_CHANNEL_CREATE_CHILD = "b_channel_create_child",
B_CHANNEL_CREATE_PERMANENT = "b_channel_create_permanent",
B_CHANNEL_CREATE_SEMI_PERMANENT = "b_channel_create_semi_permanent",
B_CHANNEL_CREATE_TEMPORARY = "b_channel_create_temporary",
B_CHANNEL_CREATE_PRIVATE = "b_channel_create_private",
B_CHANNEL_CREATE_WITH_TOPIC = "b_channel_create_with_topic",
B_CHANNEL_CREATE_WITH_DESCRIPTION = "b_channel_create_with_description",
B_CHANNEL_CREATE_WITH_PASSWORD = "b_channel_create_with_password",
B_CHANNEL_CREATE_MODIFY_WITH_CODEC_SPEEX8 = "b_channel_create_modify_with_codec_speex8",
B_CHANNEL_CREATE_MODIFY_WITH_CODEC_SPEEX16 = "b_channel_create_modify_with_codec_speex16",
B_CHANNEL_CREATE_MODIFY_WITH_CODEC_SPEEX32 = "b_channel_create_modify_with_codec_speex32",
B_CHANNEL_CREATE_MODIFY_WITH_CODEC_CELTMONO48 = "b_channel_create_modify_with_codec_celtmono48",
B_CHANNEL_CREATE_MODIFY_WITH_CODEC_OPUSVOICE = "b_channel_create_modify_with_codec_opusvoice",
B_CHANNEL_CREATE_MODIFY_WITH_CODEC_OPUSMUSIC = "b_channel_create_modify_with_codec_opusmusic",
I_CHANNEL_CREATE_MODIFY_WITH_CODEC_MAXQUALITY = "i_channel_create_modify_with_codec_maxquality",
I_CHANNEL_CREATE_MODIFY_WITH_CODEC_LATENCY_FACTOR_MIN = "i_channel_create_modify_with_codec_latency_factor_min",
I_CHANNEL_CREATE_MODIFY_CONVERSATION_HISTORY_LENGTH = "i_channel_create_modify_conversation_history_length",
B_CHANNEL_CREATE_MODIFY_CONVERSATION_HISTORY_UNLIMITED = "b_channel_create_modify_conversation_history_unlimited",
B_CHANNEL_CREATE_MODIFY_CONVERSATION_PRIVATE = "b_channel_create_modify_conversation_private",
B_CHANNEL_CREATE_WITH_MAXCLIENTS = "b_channel_create_with_maxclients",
B_CHANNEL_CREATE_WITH_MAXFAMILYCLIENTS = "b_channel_create_with_maxfamilyclients",
B_CHANNEL_CREATE_WITH_SORTORDER = "b_channel_create_with_sortorder",
B_CHANNEL_CREATE_WITH_DEFAULT = "b_channel_create_with_default",
B_CHANNEL_CREATE_WITH_NEEDED_TALK_POWER = "b_channel_create_with_needed_talk_power",
B_CHANNEL_CREATE_MODIFY_WITH_FORCE_PASSWORD = "b_channel_create_modify_with_force_password",
I_CHANNEL_CREATE_MODIFY_WITH_TEMP_DELETE_DELAY = "i_channel_create_modify_with_temp_delete_delay",
B_CHANNEL_MODIFY_PARENT = "b_channel_modify_parent",
B_CHANNEL_MODIFY_MAKE_DEFAULT = "b_channel_modify_make_default",
B_CHANNEL_MODIFY_MAKE_PERMANENT = "b_channel_modify_make_permanent",
B_CHANNEL_MODIFY_MAKE_SEMI_PERMANENT = "b_channel_modify_make_semi_permanent",
B_CHANNEL_MODIFY_MAKE_TEMPORARY = "b_channel_modify_make_temporary",
B_CHANNEL_MODIFY_NAME = "b_channel_modify_name",
B_CHANNEL_MODIFY_TOPIC = "b_channel_modify_topic",
B_CHANNEL_MODIFY_DESCRIPTION = "b_channel_modify_description",
B_CHANNEL_MODIFY_PASSWORD = "b_channel_modify_password",
B_CHANNEL_MODIFY_CODEC = "b_channel_modify_codec",
B_CHANNEL_MODIFY_CODEC_QUALITY = "b_channel_modify_codec_quality",
B_CHANNEL_MODIFY_CODEC_LATENCY_FACTOR = "b_channel_modify_codec_latency_factor",
B_CHANNEL_MODIFY_MAXCLIENTS = "b_channel_modify_maxclients",
B_CHANNEL_MODIFY_MAXFAMILYCLIENTS = "b_channel_modify_maxfamilyclients",
B_CHANNEL_MODIFY_SORTORDER = "b_channel_modify_sortorder",
B_CHANNEL_MODIFY_NEEDED_TALK_POWER = "b_channel_modify_needed_talk_power",
I_CHANNEL_MODIFY_POWER = "i_channel_modify_power",
I_CHANNEL_NEEDED_MODIFY_POWER = "i_channel_needed_modify_power",
B_CHANNEL_MODIFY_MAKE_CODEC_ENCRYPTED = "b_channel_modify_make_codec_encrypted",
B_CHANNEL_MODIFY_TEMP_DELETE_DELAY = "b_channel_modify_temp_delete_delay",
B_CHANNEL_DELETE_PERMANENT = "b_channel_delete_permanent",
B_CHANNEL_DELETE_SEMI_PERMANENT = "b_channel_delete_semi_permanent",
B_CHANNEL_DELETE_TEMPORARY = "b_channel_delete_temporary",
B_CHANNEL_DELETE_FLAG_FORCE = "b_channel_delete_flag_force",
I_CHANNEL_DELETE_POWER = "i_channel_delete_power",
B_CHANNEL_CONVERSATION_MESSAGE_DELETE = "b_channel_conversation_message_delete",
I_CHANNEL_NEEDED_DELETE_POWER = "i_channel_needed_delete_power",
B_CHANNEL_JOIN_PERMANENT = "b_channel_join_permanent",
B_CHANNEL_JOIN_SEMI_PERMANENT = "b_channel_join_semi_permanent",
B_CHANNEL_JOIN_TEMPORARY = "b_channel_join_temporary",
B_CHANNEL_JOIN_IGNORE_PASSWORD = "b_channel_join_ignore_password",
B_CHANNEL_JOIN_IGNORE_MAXCLIENTS = "b_channel_join_ignore_maxclients",
B_CHANNEL_IGNORE_VIEW_POWER = "b_channel_ignore_view_power",
I_CHANNEL_JOIN_POWER = "i_channel_join_power",
I_CHANNEL_NEEDED_JOIN_POWER = "i_channel_needed_join_power",
B_CHANNEL_IGNORE_JOIN_POWER = "b_channel_ignore_join_power",
B_CHANNEL_IGNORE_DESCRIPTION_VIEW_POWER = "b_channel_ignore_description_view_power",
I_CHANNEL_VIEW_POWER = "i_channel_view_power",
I_CHANNEL_NEEDED_VIEW_POWER = "i_channel_needed_view_power",
I_CHANNEL_SUBSCRIBE_POWER = "i_channel_subscribe_power",
I_CHANNEL_NEEDED_SUBSCRIBE_POWER = "i_channel_needed_subscribe_power",
I_CHANNEL_DESCRIPTION_VIEW_POWER = "i_channel_description_view_power",
I_CHANNEL_NEEDED_DESCRIPTION_VIEW_POWER = "i_channel_needed_description_view_power",
I_ICON_ID = "i_icon_id",
I_MAX_ICON_FILESIZE = "i_max_icon_filesize",
I_MAX_PLAYLIST_SIZE = "i_max_playlist_size",
I_MAX_PLAYLISTS = "i_max_playlists",
B_ICON_MANAGE = "b_icon_manage",
B_GROUP_IS_PERMANENT = "b_group_is_permanent",
I_GROUP_AUTO_UPDATE_TYPE = "i_group_auto_update_type",
I_GROUP_AUTO_UPDATE_MAX_VALUE = "i_group_auto_update_max_value",
I_GROUP_SORT_ID = "i_group_sort_id",
I_GROUP_SHOW_NAME_IN_TREE = "i_group_show_name_in_tree",
B_VIRTUALSERVER_SERVERGROUP_CREATE = "b_virtualserver_servergroup_create",
B_VIRTUALSERVER_SERVERGROUP_LIST = "b_virtualserver_servergroup_list",
B_VIRTUALSERVER_SERVERGROUP_PERMISSION_LIST = "b_virtualserver_servergroup_permission_list",
B_VIRTUALSERVER_SERVERGROUP_CLIENT_LIST = "b_virtualserver_servergroup_client_list",
B_VIRTUALSERVER_CHANNELGROUP_CREATE = "b_virtualserver_channelgroup_create",
B_VIRTUALSERVER_CHANNELGROUP_LIST = "b_virtualserver_channelgroup_list",
B_VIRTUALSERVER_CHANNELGROUP_PERMISSION_LIST = "b_virtualserver_channelgroup_permission_list",
B_VIRTUALSERVER_CHANNELGROUP_CLIENT_LIST = "b_virtualserver_channelgroup_client_list",
B_VIRTUALSERVER_CLIENT_PERMISSION_LIST = "b_virtualserver_client_permission_list",
B_VIRTUALSERVER_CHANNEL_PERMISSION_LIST = "b_virtualserver_channel_permission_list",
B_VIRTUALSERVER_CHANNELCLIENT_PERMISSION_LIST = "b_virtualserver_channelclient_permission_list",
B_VIRTUALSERVER_PLAYLIST_PERMISSION_LIST = "b_virtualserver_playlist_permission_list",
I_SERVER_GROUP_MODIFY_POWER = "i_server_group_modify_power",
I_SERVER_GROUP_NEEDED_MODIFY_POWER = "i_server_group_needed_modify_power",
I_SERVER_GROUP_MEMBER_ADD_POWER = "i_server_group_member_add_power",
I_SERVER_GROUP_SELF_ADD_POWER = "i_server_group_self_add_power",
I_SERVER_GROUP_NEEDED_MEMBER_ADD_POWER = "i_server_group_needed_member_add_power",
I_SERVER_GROUP_MEMBER_REMOVE_POWER = "i_server_group_member_remove_power",
I_SERVER_GROUP_SELF_REMOVE_POWER = "i_server_group_self_remove_power",
I_SERVER_GROUP_NEEDED_MEMBER_REMOVE_POWER = "i_server_group_needed_member_remove_power",
I_CHANNEL_GROUP_MODIFY_POWER = "i_channel_group_modify_power",
I_CHANNEL_GROUP_NEEDED_MODIFY_POWER = "i_channel_group_needed_modify_power",
I_CHANNEL_GROUP_MEMBER_ADD_POWER = "i_channel_group_member_add_power",
I_CHANNEL_GROUP_SELF_ADD_POWER = "i_channel_group_self_add_power",
I_CHANNEL_GROUP_NEEDED_MEMBER_ADD_POWER = "i_channel_group_needed_member_add_power",
I_CHANNEL_GROUP_MEMBER_REMOVE_POWER = "i_channel_group_member_remove_power",
I_CHANNEL_GROUP_SELF_REMOVE_POWER = "i_channel_group_self_remove_power",
I_CHANNEL_GROUP_NEEDED_MEMBER_REMOVE_POWER = "i_channel_group_needed_member_remove_power",
I_GROUP_MEMBER_ADD_POWER = "i_group_member_add_power",
I_GROUP_NEEDED_MEMBER_ADD_POWER = "i_group_needed_member_add_power",
I_GROUP_MEMBER_REMOVE_POWER = "i_group_member_remove_power",
I_GROUP_NEEDED_MEMBER_REMOVE_POWER = "i_group_needed_member_remove_power",
I_GROUP_MODIFY_POWER = "i_group_modify_power",
I_GROUP_NEEDED_MODIFY_POWER = "i_group_needed_modify_power",
I_PERMISSION_MODIFY_POWER = "i_permission_modify_power",
B_PERMISSION_MODIFY_POWER_IGNORE = "b_permission_modify_power_ignore",
B_VIRTUALSERVER_SERVERGROUP_DELETE = "b_virtualserver_servergroup_delete",
B_VIRTUALSERVER_CHANNELGROUP_DELETE = "b_virtualserver_channelgroup_delete",
I_CLIENT_PERMISSION_MODIFY_POWER = "i_client_permission_modify_power",
I_CLIENT_NEEDED_PERMISSION_MODIFY_POWER = "i_client_needed_permission_modify_power",
I_CLIENT_MAX_CLONES_UID = "i_client_max_clones_uid",
I_CLIENT_MAX_CLONES_IP = "i_client_max_clones_ip",
I_CLIENT_MAX_CLONES_HWID = "i_client_max_clones_hwid",
I_CLIENT_MAX_IDLETIME = "i_client_max_idletime",
I_CLIENT_MAX_AVATAR_FILESIZE = "i_client_max_avatar_filesize",
I_CLIENT_MAX_CHANNEL_SUBSCRIPTIONS = "i_client_max_channel_subscriptions",
I_CLIENT_MAX_CHANNELS = "i_client_max_channels",
I_CLIENT_MAX_TEMPORARY_CHANNELS = "i_client_max_temporary_channels",
I_CLIENT_MAX_SEMI_CHANNELS = "i_client_max_semi_channels",
I_CLIENT_MAX_PERMANENT_CHANNELS = "i_client_max_permanent_channels",
B_CLIENT_USE_PRIORITY_SPEAKER = "b_client_use_priority_speaker",
B_CLIENT_SKIP_CHANNELGROUP_PERMISSIONS = "b_client_skip_channelgroup_permissions",
B_CLIENT_FORCE_PUSH_TO_TALK = "b_client_force_push_to_talk",
B_CLIENT_IGNORE_BANS = "b_client_ignore_bans",
B_CLIENT_IGNORE_VPN = "b_client_ignore_vpn",
B_CLIENT_IGNORE_ANTIFLOOD = "b_client_ignore_antiflood",
B_CLIENT_ENFORCE_VALID_HWID = "b_client_enforce_valid_hwid",
B_CLIENT_ALLOW_INVALID_PACKET = "b_client_allow_invalid_packet",
B_CLIENT_ALLOW_INVALID_BADGES = "b_client_allow_invalid_badges",
B_CLIENT_ISSUE_CLIENT_QUERY_COMMAND = "b_client_issue_client_query_command",
B_CLIENT_USE_RESERVED_SLOT = "b_client_use_reserved_slot",
B_CLIENT_USE_CHANNEL_COMMANDER = "b_client_use_channel_commander",
B_CLIENT_REQUEST_TALKER = "b_client_request_talker",
B_CLIENT_AVATAR_DELETE_OTHER = "b_client_avatar_delete_other",
B_CLIENT_IS_STICKY = "b_client_is_sticky",
B_CLIENT_IGNORE_STICKY = "b_client_ignore_sticky",
B_CLIENT_MUSIC_CREATE_PERMANENT = "b_client_music_create_permanent",
B_CLIENT_MUSIC_CREATE_SEMI_PERMANENT = "b_client_music_create_semi_permanent",
B_CLIENT_MUSIC_CREATE_TEMPORARY = "b_client_music_create_temporary",
B_CLIENT_MUSIC_MODIFY_PERMANENT = "b_client_music_modify_permanent",
B_CLIENT_MUSIC_MODIFY_SEMI_PERMANENT = "b_client_music_modify_semi_permanent",
B_CLIENT_MUSIC_MODIFY_TEMPORARY = "b_client_music_modify_temporary",
I_CLIENT_MUSIC_CREATE_MODIFY_MAX_VOLUME = "i_client_music_create_modify_max_volume",
I_CLIENT_MUSIC_LIMIT = "i_client_music_limit",
I_CLIENT_MUSIC_NEEDED_DELETE_POWER = "i_client_music_needed_delete_power",
I_CLIENT_MUSIC_DELETE_POWER = "i_client_music_delete_power",
I_CLIENT_MUSIC_PLAY_POWER = "i_client_music_play_power",
I_CLIENT_MUSIC_NEEDED_PLAY_POWER = "i_client_music_needed_play_power",
I_CLIENT_MUSIC_MODIFY_POWER = "i_client_music_modify_power",
I_CLIENT_MUSIC_NEEDED_MODIFY_POWER = "i_client_music_needed_modify_power",
I_CLIENT_MUSIC_RENAME_POWER = "i_client_music_rename_power",
I_CLIENT_MUSIC_NEEDED_RENAME_POWER = "i_client_music_needed_rename_power",
B_PLAYLIST_CREATE = "b_playlist_create",
I_PLAYLIST_VIEW_POWER = "i_playlist_view_power",
I_PLAYLIST_NEEDED_VIEW_POWER = "i_playlist_needed_view_power",
I_PLAYLIST_MODIFY_POWER = "i_playlist_modify_power",
I_PLAYLIST_NEEDED_MODIFY_POWER = "i_playlist_needed_modify_power",
I_PLAYLIST_PERMISSION_MODIFY_POWER = "i_playlist_permission_modify_power",
I_PLAYLIST_NEEDED_PERMISSION_MODIFY_POWER = "i_playlist_needed_permission_modify_power",
I_PLAYLIST_DELETE_POWER = "i_playlist_delete_power",
I_PLAYLIST_NEEDED_DELETE_POWER = "i_playlist_needed_delete_power",
I_PLAYLIST_SONG_ADD_POWER = "i_playlist_song_add_power",
I_PLAYLIST_SONG_NEEDED_ADD_POWER = "i_playlist_song_needed_add_power",
I_PLAYLIST_SONG_REMOVE_POWER = "i_playlist_song_remove_power",
I_PLAYLIST_SONG_NEEDED_REMOVE_POWER = "i_playlist_song_needed_remove_power",
B_CLIENT_INFO_VIEW = "b_client_info_view",
B_CLIENT_PERMISSIONOVERVIEW_VIEW = "b_client_permissionoverview_view",
B_CLIENT_PERMISSIONOVERVIEW_OWN = "b_client_permissionoverview_own",
B_CLIENT_REMOTEADDRESS_VIEW = "b_client_remoteaddress_view",
I_CLIENT_SERVERQUERY_VIEW_POWER = "i_client_serverquery_view_power",
I_CLIENT_NEEDED_SERVERQUERY_VIEW_POWER = "i_client_needed_serverquery_view_power",
B_CLIENT_CUSTOM_INFO_VIEW = "b_client_custom_info_view",
B_CLIENT_MUSIC_CHANNEL_LIST = "b_client_music_channel_list",
B_CLIENT_MUSIC_SERVER_LIST = "b_client_music_server_list",
I_CLIENT_MUSIC_INFO = "i_client_music_info",
I_CLIENT_MUSIC_NEEDED_INFO = "i_client_music_needed_info",
I_CLIENT_KICK_FROM_SERVER_POWER = "i_client_kick_from_server_power",
I_CLIENT_NEEDED_KICK_FROM_SERVER_POWER = "i_client_needed_kick_from_server_power",
I_CLIENT_KICK_FROM_CHANNEL_POWER = "i_client_kick_from_channel_power",
I_CLIENT_NEEDED_KICK_FROM_CHANNEL_POWER = "i_client_needed_kick_from_channel_power",
I_CLIENT_BAN_POWER = "i_client_ban_power",
I_CLIENT_NEEDED_BAN_POWER = "i_client_needed_ban_power",
I_CLIENT_MOVE_POWER = "i_client_move_power",
I_CLIENT_NEEDED_MOVE_POWER = "i_client_needed_move_power",
I_CLIENT_COMPLAIN_POWER = "i_client_complain_power",
I_CLIENT_NEEDED_COMPLAIN_POWER = "i_client_needed_complain_power",
B_CLIENT_COMPLAIN_LIST = "b_client_complain_list",
B_CLIENT_COMPLAIN_DELETE_OWN = "b_client_complain_delete_own",
B_CLIENT_COMPLAIN_DELETE = "b_client_complain_delete",
B_CLIENT_BAN_LIST = "b_client_ban_list",
B_CLIENT_BAN_LIST_GLOBAL = "b_client_ban_list_global",
B_CLIENT_BAN_TRIGGER_LIST = "b_client_ban_trigger_list",
B_CLIENT_BAN_CREATE = "b_client_ban_create",
B_CLIENT_BAN_CREATE_GLOBAL = "b_client_ban_create_global",
B_CLIENT_BAN_NAME = "b_client_ban_name",
B_CLIENT_BAN_IP = "b_client_ban_ip",
B_CLIENT_BAN_HWID = "b_client_ban_hwid",
B_CLIENT_BAN_EDIT = "b_client_ban_edit",
B_CLIENT_BAN_EDIT_GLOBAL = "b_client_ban_edit_global",
B_CLIENT_BAN_DELETE_OWN = "b_client_ban_delete_own",
B_CLIENT_BAN_DELETE = "b_client_ban_delete",
B_CLIENT_BAN_DELETE_OWN_GLOBAL = "b_client_ban_delete_own_global",
B_CLIENT_BAN_DELETE_GLOBAL = "b_client_ban_delete_global",
I_CLIENT_BAN_MAX_BANTIME = "i_client_ban_max_bantime",
I_CLIENT_PRIVATE_TEXTMESSAGE_POWER = "i_client_private_textmessage_power",
I_CLIENT_NEEDED_PRIVATE_TEXTMESSAGE_POWER = "i_client_needed_private_textmessage_power",
B_CLIENT_EVEN_TEXTMESSAGE_SEND = "b_client_even_textmessage_send",
B_CLIENT_SERVER_TEXTMESSAGE_SEND = "b_client_server_textmessage_send",
B_CLIENT_CHANNEL_TEXTMESSAGE_SEND = "b_client_channel_textmessage_send",
B_CLIENT_OFFLINE_TEXTMESSAGE_SEND = "b_client_offline_textmessage_send",
I_CLIENT_TALK_POWER = "i_client_talk_power",
I_CLIENT_NEEDED_TALK_POWER = "i_client_needed_talk_power",
I_CLIENT_POKE_POWER = "i_client_poke_power",
I_CLIENT_NEEDED_POKE_POWER = "i_client_needed_poke_power",
B_CLIENT_SET_FLAG_TALKER = "b_client_set_flag_talker",
I_CLIENT_WHISPER_POWER = "i_client_whisper_power",
I_CLIENT_NEEDED_WHISPER_POWER = "i_client_needed_whisper_power",
B_CLIENT_MODIFY_DESCRIPTION = "b_client_modify_description",
B_CLIENT_MODIFY_OWN_DESCRIPTION = "b_client_modify_own_description",
B_CLIENT_USE_BBCODE_ANY = "b_client_use_bbcode_any",
B_CLIENT_USE_BBCODE_URL = "b_client_use_bbcode_url",
B_CLIENT_USE_BBCODE_IMAGE = "b_client_use_bbcode_image",
B_CLIENT_MODIFY_DBPROPERTIES = "b_client_modify_dbproperties",
B_CLIENT_DELETE_DBPROPERTIES = "b_client_delete_dbproperties",
B_CLIENT_CREATE_MODIFY_SERVERQUERY_LOGIN = "b_client_create_modify_serverquery_login",
B_CLIENT_QUERY_CREATE = "b_client_query_create",
B_CLIENT_QUERY_LIST = "b_client_query_list",
B_CLIENT_QUERY_LIST_OWN = "b_client_query_list_own",
B_CLIENT_QUERY_RENAME = "b_client_query_rename",
B_CLIENT_QUERY_RENAME_OWN = "b_client_query_rename_own",
B_CLIENT_QUERY_CHANGE_PASSWORD = "b_client_query_change_password",
B_CLIENT_QUERY_CHANGE_OWN_PASSWORD = "b_client_query_change_own_password",
B_CLIENT_QUERY_CHANGE_PASSWORD_GLOBAL = "b_client_query_change_password_global",
B_CLIENT_QUERY_DELETE = "b_client_query_delete",
B_CLIENT_QUERY_DELETE_OWN = "b_client_query_delete_own",
B_FT_IGNORE_PASSWORD = "b_ft_ignore_password",
B_FT_TRANSFER_LIST = "b_ft_transfer_list",
I_FT_FILE_UPLOAD_POWER = "i_ft_file_upload_power",
I_FT_NEEDED_FILE_UPLOAD_POWER = "i_ft_needed_file_upload_power",
I_FT_FILE_DOWNLOAD_POWER = "i_ft_file_download_power",
I_FT_NEEDED_FILE_DOWNLOAD_POWER = "i_ft_needed_file_download_power",
I_FT_FILE_DELETE_POWER = "i_ft_file_delete_power",
I_FT_NEEDED_FILE_DELETE_POWER = "i_ft_needed_file_delete_power",
I_FT_FILE_RENAME_POWER = "i_ft_file_rename_power",
I_FT_NEEDED_FILE_RENAME_POWER = "i_ft_needed_file_rename_power",
I_FT_FILE_BROWSE_POWER = "i_ft_file_browse_power",
I_FT_NEEDED_FILE_BROWSE_POWER = "i_ft_needed_file_browse_power",
I_FT_DIRECTORY_CREATE_POWER = "i_ft_directory_create_power",
I_FT_NEEDED_DIRECTORY_CREATE_POWER = "i_ft_needed_directory_create_power",
I_FT_QUOTA_MB_DOWNLOAD_PER_CLIENT = "i_ft_quota_mb_download_per_client",
I_FT_QUOTA_MB_UPLOAD_PER_CLIENT = "i_ft_quota_mb_upload_per_client"
}
export default PermissionType;

View File

@ -1,251 +1,257 @@
namespace profiles {
export class ConnectionProfile {
id: string;
import {decode_identity, IdentitifyType, Identity} from "tc-shared/profiles/Identity";
import {guid} from "tc-shared/crypto/uid";
import {TeaForumIdentity} from "tc-shared/profiles/identities/TeaForumIdentity";
import {TeaSpeakIdentity} from "tc-shared/profiles/identities/TeamSpeakIdentity";
import {AbstractServerConnection} from "tc-shared/connection/ConnectionBase";
import {HandshakeIdentityHandler} from "tc-shared/connection/HandshakeHandler";
import {createErrorModal} from "tc-shared/ui/elements/Modal";
import {formatMessage} from "tc-shared/ui/frames/chat";
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;
constructor(id: string) {
this.id = id;
}
selected_identity_type: string = "unset";
identities: { [key: string]: Identity } = {};
connect_username(): string {
if (this.default_username && this.default_username !== "Another TeaSpeak user")
return this.default_username;
constructor(id: string) {
this.id = id;
}
let selected = this.selected_identity();
let name = selected ? selected.fallback_name() : undefined;
return name || "Another TeaSpeak user";
}
connect_username(): string {
if (this.default_username && this.default_username !== "Another TeaSpeak user")
return this.default_username;
selected_identity(current_type?: identities.IdentitifyType): identities.Identity {
if (!current_type)
current_type = this.selected_type();
let selected = this.selected_identity();
let name = selected ? selected.fallback_name() : undefined;
return name || "Another TeaSpeak user";
}
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()];
}
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 TeaForumIdentity.identity();
} else if (current_type == IdentitifyType.TEAMSPEAK || current_type == IdentitifyType.NICKNAME) {
return this.identities[IdentitifyType[current_type].toLowerCase()];
}
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;
}
return undefined;
}
async function decode_profile(data): Promise<ConnectionProfile | string> {
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;
selected_type?(): IdentitifyType {
return this.selected_identity_type ? IdentitifyType[this.selected_identity_type.toUpperCase()] : undefined;
}
interface ProfilesData {
version: number;
profiles: string[];
set_identity(type: IdentitifyType, identity: Identity) {
this.identities[IdentitifyType[type].toLowerCase()] = identity;
}
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!<br>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();
}
spawn_identity_handshake_handler?(connection: AbstractServerConnection): HandshakeIdentityHandler {
const identity = this.selected_identity();
if (!identity)
return undefined;
return identity.spawn_identity_handshake_handler(connection);
}
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;
}
encode?(): string {
const identity_data = {};
for (const key in this.identities)
if (this.identities[key])
identity_data[key] = this.identities[key].encode();
let _requires_save = false;
export function save() {
const profiles: string[] = [];
for (const profile of available_profiles)
profiles.push(profile.encode());
const data = JSON.stringify({
return JSON.stringify({
version: 1,
profiles: profiles
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
});
localStorage.setItem("profiles", data);
}
export function mark_need_save() {
_requires_save = true;
valid(): boolean {
const identity = this.selected_identity();
return !!identity && identity.valid();
}
}
export function requires_save(): boolean {
return _requires_save;
}
async function decode_profile(data): Promise<ConnectionProfile | string> {
data = JSON.parse(data);
if (data.version !== 1)
return "invalid version";
export function profiles(): ConnectionProfile[] {
return available_profiles;
}
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();
export function find_profile(id: string): ConnectionProfile | undefined {
for (const profile of profiles())
if (profile.id == id)
return profile;
if (data.identity_data) {
for (const key of Object.keys(data.identity_data)) {
const type = IdentitifyType[key.toUpperCase() as string];
const _data = data.identity_data[key];
if (type == undefined) continue;
return undefined;
}
const identity = await decode_identity(type, _data);
if (identity == undefined) continue;
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();
result.identities[key.toLowerCase()] = identity;
}
profile.id = "default";
return old_default;
}
export function delete_profile(profile: ConnectionProfile) {
available_profiles.remove(profile);
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"), 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 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!<br>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, TeaForumIdentity.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);
}

View File

@ -1,110 +1,119 @@
namespace profiles.identities {
export enum IdentitifyType {
TEAFORO,
TEAMSPEAK,
NICKNAME
import {AbstractServerConnection, ServerCommand} from "tc-shared/connection/ConnectionBase";
import {HandshakeIdentityHandler} from "tc-shared/connection/HandshakeHandler";
import {AbstractCommandHandler} from "tc-shared/connection/AbstractCommandHandler";
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<void>;
spawn_identity_handshake_handler(connection: AbstractServerConnection) : HandshakeIdentityHandler;
}
/* avoid circular dependencies here */
export async function decode_identity(type: IdentitifyType, data: string) : Promise<Identity> {
let identity: Identity;
switch (type) {
case IdentitifyType.NICKNAME:
const nidentity = require("tc-shared/profiles/identities/NameIdentity");
identity = new nidentity.NameIdentity();
break;
case IdentitifyType.TEAFORO:
const fidentity = require("tc-shared/profiles/identities/TeaForumIdentity");
identity = new fidentity.TeaForumIdentity(undefined);
break;
case IdentitifyType.TEAMSPEAK:
const tidentity = require("tc-shared/profiles/identities/TeamSpeakIdentity");
identity = new tidentity.TeaSpeakIdentity(undefined, undefined);
break;
}
if(!identity)
return undefined;
try {
await identity.decode(data)
} catch(error) {
/* todo better error handling! */
console.error(error);
return undefined;
}
export interface Identity {
fallback_name(): string | undefined ;
uid() : string;
type() : IdentitifyType;
return identity;
}
valid() : boolean;
export function create_identity(type: IdentitifyType) {
let identity: Identity;
switch (type) {
case IdentitifyType.NICKNAME:
const nidentity = require("tc-shared/profiles/identities/NameIdentity");
identity = new nidentity.NameIdentity();
break;
case IdentitifyType.TEAFORO:
const fidentity = require("tc-shared/profiles/identities/TeaForumIdentity");
identity = new fidentity.TeaForumIdentity(undefined);
break;
case IdentitifyType.TEAMSPEAK:
const tidentity = require("tc-shared/profiles/identities/TeamSpeakIdentity");
identity = new tidentity.TeaSpeakIdentity(undefined, undefined);
break;
}
return identity;
}
encode?() : string;
decode(data: string) : Promise<void>;
export class HandshakeCommandHandler<T extends AbstractHandshakeIdentityHandler> extends AbstractCommandHandler {
readonly handle: T;
spawn_identity_handshake_handler(connection: connection.AbstractServerConnection) : connection.HandshakeIdentityHandler;
constructor(connection: AbstractServerConnection, handle: T) {
super(connection);
this.handle = handle;
}
export async function decode_identity(type: IdentitifyType, data: string) : Promise<Identity> {
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;
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);
}
return true;
}
}
return identity;
export abstract class AbstractHandshakeIdentityHandler implements HandshakeIdentityHandler {
connection: AbstractServerConnection;
protected callbacks: ((success: boolean, message?: string) => any)[] = [];
protected constructor(connection: AbstractServerConnection) {
this.connection = connection;
}
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;
register_callback(callback: (success: boolean, message?: string) => any) {
this.callbacks.push(callback);
}
export class HandshakeCommandHandler<T extends AbstractHandshakeIdentityHandler> extends connection.AbstractCommandHandler {
readonly handle: T;
abstract start_handshake();
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_success() {
for(const callback of this.callbacks)
callback(true);
}
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);
}
protected trigger_fail(message: string) {
for(const callback of this.callbacks)
callback(false, message);
}
}

View File

@ -1,88 +1,97 @@
/// <reference path="../Identity.ts" />
import {
AbstractHandshakeIdentityHandler,
HandshakeCommandHandler,
IdentitifyType,
Identity
} from "tc-shared/profiles/Identity";
import * as log from "tc-shared/log";
import {LogCategory} from "tc-shared/log";
import {CommandResult} from "tc-shared/connection/ServerConnectionDeclaration";
import {AbstractServerConnection} from "tc-shared/connection/ConnectionBase";
import {HandshakeIdentityHandler} from "tc-shared/connection/HandshakeHandler";
namespace profiles.identities {
class NameHandshakeHandler extends AbstractHandshakeIdentityHandler {
readonly identity: NameIdentity;
handler: HandshakeCommandHandler<NameHandshakeHandler>;
console.error(AbstractHandshakeIdentityHandler);
class NameHandshakeHandler extends AbstractHandshakeIdentityHandler {
readonly identity: NameIdentity;
handler: HandshakeCommandHandler<NameHandshakeHandler>;
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");
}
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();
}
this.handler = new HandshakeCommandHandler(connection, this);
this.handler["handshakeidentityproof"] = () => this.trigger_fail("server requested unexpected proof");
}
export class NameIdentity implements Identity {
private _name: string;
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());
}
constructor(name?: string) {
this._name = name;
}
protected trigger_fail(message: string) {
this.connection.command_handler_boss().unregister_handler(this.handler);
super.trigger_fail(message);
}
set_name(name: string) { this._name = name; }
protected trigger_success() {
this.connection.command_handler_boss().unregister_handler(this.handler);
super.trigger_success();
}
}
name() : string { return this._name; }
export class NameIdentity implements Identity {
private _name: string;
fallback_name(): string | undefined {
return this._name;
}
constructor(name?: string) {
this._name = name;
}
uid(): string {
return btoa(this._name); //FIXME hash!
}
set_name(name: string) { this._name = name; }
type(): IdentitifyType {
return IdentitifyType.NICKNAME;
}
name() : string { return this._name; }
valid(): boolean {
return this._name != undefined && this._name.length >= 5;
}
fallback_name(): string | undefined {
return this._name;
}
decode(data) : Promise<void> {
data = JSON.parse(data);
if(data.version !== 1)
throw "invalid version";
uid(): string {
return btoa(this._name); //FIXME hash!
}
this._name = data["name"];
return;
}
type(): IdentitifyType {
return IdentitifyType.NICKNAME;
}
encode?() : string {
return JSON.stringify({
version: 1,
name: this._name
});
}
valid(): boolean {
return this._name != undefined && this._name.length >= 5;
}
spawn_identity_handshake_handler(connection: connection.AbstractServerConnection) : connection.HandshakeIdentityHandler {
return new NameHandshakeHandler(connection, this);
}
decode(data) : Promise<void> {
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);
}
}

View File

@ -1,122 +1,135 @@
/// <reference path="../Identity.ts" />
import {
AbstractHandshakeIdentityHandler,
HandshakeCommandHandler,
IdentitifyType,
Identity
} from "tc-shared/profiles/Identity";
import * as log from "tc-shared/log";
import {LogCategory} from "tc-shared/log";
import {CommandResult} from "tc-shared/connection/ServerConnectionDeclaration";
import {AbstractServerConnection} from "tc-shared/connection/ConnectionBase";
import {HandshakeIdentityHandler} from "tc-shared/connection/HandshakeHandler";
import * as forum from "./teaspeak-forum";
namespace profiles.identities {
class TeaForumHandshakeHandler extends AbstractHandshakeIdentityHandler {
readonly identity: TeaForumIdentity;
handler: HandshakeCommandHandler<TeaForumHandshakeHandler>;
class TeaForumHandshakeHandler extends AbstractHandshakeIdentityHandler {
readonly identity: TeaForumIdentity;
handler: HandshakeCommandHandler<TeaForumHandshakeHandler>;
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();
}
constructor(connection: AbstractServerConnection, identity: TeaForumIdentity) {
super(connection);
this.identity = identity;
this.handler = new HandshakeCommandHandler(connection, this);
this.handler["handshakeidentityproof"] = this.handle_proof.bind(this);
}
export class TeaForumIdentity implements Identity {
private readonly identity_data: forum.Data;
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);
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<void> {
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"));
}
if(error instanceof CommandResult)
error = error.extra_message || error.message;
this.trigger_fail("failed to execute begin (" + error + ")");
});
}
let static_identity: TeaForumIdentity;
export function set_static_identity(identity: TeaForumIdentity) {
static_identity = identity;
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 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_fail(message: string) {
this.connection.command_handler_boss().unregister_handler(this.handler);
super.trigger_fail(message);
}
export function valid_static_forum_identity() : boolean {
return static_identity && static_identity.valid();
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 static_forum_identity() : TeaForumIdentity | undefined {
constructor(data: forum.Data) {
this.identity_data = data;
}
data() {
return this.identity_data;
}
decode(data) : Promise<void> {
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"));
}
public static identity() {
return static_identity;
}
}
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;
}

File diff suppressed because it is too large Load Diff

View File

@ -1,5 +1,11 @@
interface Window {
grecaptcha: GReCaptcha;
import {settings, Settings} from "tc-shared/settings";
import * as loader from "tc-loader";
import * as fidentity from "./TeaForumIdentity";
declare global {
interface Window {
grecaptcha: GReCaptcha;
}
}
interface GReCaptcha {
@ -18,349 +24,347 @@ interface GReCaptcha {
reset(widget_id?: string);
}
namespace forum {
export namespace gcaptcha {
export async function initialize() {
if(typeof(window.grecaptcha) === "undefined") {
let script = document.createElement("script");
script.async = true;
export namespace gcaptcha {
export async function initialize() {
if(typeof(window.grecaptcha) === "undefined") {
let script = document.createElement("script");
script.async = true;
let timeout;
const callback_name = "captcha_callback_" + Math.random().toString().replace(".", "");
try {
await new Promise((resolve, reject) => {
script.onerror = reject;
window[callback_name] = resolve;
script.src = "https://www.google.com/recaptcha/api.js?onload=" + encodeURIComponent(callback_name) + "&render=explicit";
document.body.append(script);
timeout = setTimeout(() => reject("timeout"), 15000);
});
} catch(error) {
script.remove();
script = undefined;
console.error(tr("Failed to fetch recaptcha javascript source: %o"), error);
throw tr("failed to download source");
} finally {
if(script)
script.onerror = undefined;
delete window[callback_name];
clearTimeout(timeout);
}
}
if(typeof(window.grecaptcha) === "undefined")
throw tr("failed to load recaptcha");
}
export async function spawn(container: JQuery, key: string, callback_data: (token: string) => any) {
let timeout;
const callback_name = "captcha_callback_" + Math.random().toString().replace(".", "");
try {
await initialize();
await new Promise((resolve, reject) => {
script.onerror = reject;
window[callback_name] = resolve;
script.src = "https://www.google.com/recaptcha/api.js?onload=" + encodeURIComponent(callback_name) + "&render=explicit";
document.body.append(script);
timeout = setTimeout(() => reject("timeout"), 15000);
});
} catch(error) {
console.error(tr("Failed to initialize G-Recaptcha. Error: %o"), error);
throw tr("initialisation failed");
}
if(container.attr("captcha-uuid"))
window.grecaptcha.reset(container.attr("captcha-uuid"));
else {
container.attr("captcha-uuid", window.grecaptcha.render(container[0], {
"sitekey": key,
callback: callback_data
}));
script.remove();
script = undefined;
console.error(tr("Failed to fetch recaptcha javascript source: %o"), error);
throw tr("failed to download source");
} finally {
if(script)
script.onerror = undefined;
delete window[callback_name];
timeout && clearTimeout(timeout);
}
}
if(typeof(window.grecaptcha) === "undefined")
throw tr("failed to load recaptcha");
}
function api_url() {
return settings.static_global(Settings.KEY_TEAFORO_URL);
}
export class Data {
readonly auth_key: string;
readonly raw: string;
readonly sign: string;
parsed: {
user_id: number;
user_name: string;
data_age: number;
user_group_id: number;
is_staff: boolean;
user_groups: number[];
};
constructor(auth: string, raw: string, sign: string) {
this.auth_key = auth;
this.raw = raw;
this.sign = sign;
this.parsed = JSON.parse(raw);
export async function spawn(container: JQuery, key: string, callback_data: (token: string) => any) {
try {
await initialize();
} catch(error) {
console.error(tr("Failed to initialize G-Recaptcha. Error: %o"), error);
throw tr("initialisation failed");
}
if(container.attr("captcha-uuid"))
window.grecaptcha.reset(container.attr("captcha-uuid"));
else {
container.attr("captcha-uuid", window.grecaptcha.render(container[0], {
"sitekey": key,
callback: callback_data
}));
}
data_json() : string { return this.raw; }
data_sign() : string { return this.sign; }
name() : string { return this.parsed.user_name; }
user_id() { return this.parsed.user_id; }
user_group() { return this.parsed.user_group_id; }
is_stuff() : boolean { return this.parsed.is_staff; }
is_premium() : boolean { return this.parsed.user_groups.indexOf(5) != -1; }
data_age() : Date { return new Date(this.parsed.data_age); }
is_expired() : boolean { return this.parsed.data_age + 48 * 60 * 60 * 1000 < Date.now(); }
should_renew() : boolean { return this.parsed.data_age + 24 * 60 * 60 * 1000 < Date.now(); } /* renew data all 24hrs */
}
let _data: Data | undefined;
}
export function logged_in() : boolean {
return !!_data && !_data.is_expired();
function api_url() {
return settings.static_global(Settings.KEY_TEAFORO_URL);
}
export class Data {
readonly auth_key: string;
readonly raw: string;
readonly sign: string;
parsed: {
user_id: number;
user_name: string;
data_age: number;
user_group_id: number;
is_staff: boolean;
user_groups: number[];
};
constructor(auth: string, raw: string, sign: string) {
this.auth_key = auth;
this.raw = raw;
this.sign = sign;
this.parsed = JSON.parse(raw);
}
export function data() : Data { return _data; }
export interface LoginResult {
status: "success" | "captcha" | "error";
data_json() : string { return this.raw; }
data_sign() : string { return this.sign; }
error_message?: string;
captcha?: {
type: "gre-captcha" | "unknown";
data: any; /* in case of gre-captcha it would be the side key */
name() : string { return this.parsed.user_name; }
user_id() { return this.parsed.user_id; }
user_group() { return this.parsed.user_group_id; }
is_stuff() : boolean { return this.parsed.is_staff; }
is_premium() : boolean { return this.parsed.user_groups.indexOf(5) != -1; }
data_age() : Date { return new Date(this.parsed.data_age); }
is_expired() : boolean { return this.parsed.data_age + 48 * 60 * 60 * 1000 < Date.now(); }
should_renew() : boolean { return this.parsed.data_age + 24 * 60 * 60 * 1000 < Date.now(); } /* renew data all 24hrs */
}
let _data: Data | undefined;
export function logged_in() : boolean {
return !!_data && !_data.is_expired();
}
export function data() : Data { return _data; }
export interface LoginResult {
status: "success" | "captcha" | "error";
error_message?: string;
captcha?: {
type: "gre-captcha" | "unknown";
data: any; /* in case of gre-captcha it would be the side key */
};
}
export async function login(username: string, password: string, captcha?: any) : Promise<LoginResult> {
let response;
try {
response = await new Promise<any>((resolve, reject) => {
$.ajax({
url: api_url() + "?web-api/v1/login",
type: "POST",
cache: false,
data: {
username: username,
password: password,
remember: true,
"g-recaptcha-response": captcha
},
crossDomain: true,
success: resolve,
error: (xhr, status, error) => {
console.log(tr("Login request failed %o: %o"), status, error);
reject(tr("request failed"));
}
})
});
} catch(error) {
return {
status: "error",
error_message: tr("failed to send login request")
};
}
export async function login(username: string, password: string, captcha?: any) : Promise<LoginResult> {
let response;
try {
response = await new Promise<any>((resolve, reject) => {
$.ajax({
url: api_url() + "?web-api/v1/login",
type: "POST",
cache: false,
data: {
username: username,
password: password,
remember: true,
"g-recaptcha-response": captcha
},
if(response["status"] !== "ok") {
console.error(tr("Response status not okey. Error happend: %o"), response);
return {
status: "error",
error_message: (response["errors"] || [])[0] || tr("Unknown error")
};
}
crossDomain: true,
if(!response["success"]) {
console.error(tr("Login failed. Response %o"), response);
success: resolve,
error: (xhr, status, error) => {
console.log(tr("Login request failed %o: %o"), status, error);
reject(tr("request failed"));
}
})
});
} catch(error) {
return {
status: "error",
error_message: tr("failed to send login request")
let message = tr("failed to login");
let captcha;
/* user/password wrong | and maybe captcha required */
if(response["code"] == 1 || response["code"] == 3)
message = tr("Invalid username or password");
if(response["code"] == 2 || response["code"] == 3) {
captcha = {
type: response["captcha"]["type"],
data: response["captcha"]["siteKey"] //TODO: Why so static here?
};
}
if(response["status"] !== "ok") {
console.error(tr("Response status not okey. Error happend: %o"), response);
return {
status: "error",
error_message: (response["errors"] || [])[0] || tr("Unknown error")
};
}
if(!response["success"]) {
console.error(tr("Login failed. Response %o"), response);
let message = tr("failed to login");
let captcha;
/* user/password wrong | and maybe captcha required */
if(response["code"] == 1 || response["code"] == 3)
message = tr("Invalid username or password");
if(response["code"] == 2 || response["code"] == 3) {
captcha = {
type: response["captcha"]["type"],
data: response["captcha"]["siteKey"] //TODO: Why so static here?
};
if(response["code"] == 2)
message = tr("captcha required");
}
return {
status: typeof(captcha) !== "undefined" ? "captcha" : "error",
error_message: message,
captcha: captcha
};
}
//document.cookie = "user_data=" + response["data"] + ";path=/";
//document.cookie = "user_sign=" + response["sign"] + ";path=/";
try {
_data = new Data(response["auth-key"], response["data"], response["sign"]);
localStorage.setItem("teaspeak-forum-data", response["data"]);
localStorage.setItem("teaspeak-forum-sign", response["sign"]);
localStorage.setItem("teaspeak-forum-auth", response["auth-key"]);
profiles.identities.update_forum();
} catch(error) {
console.error(tr("Failed to parse forum given data: %o"), error);
return {
status: "error",
error_message: tr("Failed to parse response data")
}
if(response["code"] == 2)
message = tr("captcha required");
}
return {
status: "success"
status: typeof(captcha) !== "undefined" ? "captcha" : "error",
error_message: message,
captcha: captcha
};
}
//document.cookie = "user_data=" + response["data"] + ";path=/";
//document.cookie = "user_sign=" + response["sign"] + ";path=/";
export async function renew_data() : Promise<"success" | "login-required"> {
let response;
try {
response = await new Promise<any>((resolve, reject) => {
$.ajax({
url: api_url() + "?web-api/v1/renew-data",
type: "GET",
cache: false,
crossDomain: true,
data: {
"auth-key": _data.auth_key
},
success: resolve,
error: (xhr, status, error) => {
console.log(tr("Renew request failed %o: %o"), status, error);
reject(tr("request failed"));
}
})
});
} catch(error) {
throw tr("failed to send renew request");
try {
_data = new Data(response["auth-key"], response["data"], response["sign"]);
localStorage.setItem("teaspeak-forum-data", response["data"]);
localStorage.setItem("teaspeak-forum-sign", response["sign"]);
localStorage.setItem("teaspeak-forum-auth", response["auth-key"]);
fidentity.update_forum();
} catch(error) {
console.error(tr("Failed to parse forum given data: %o"), error);
return {
status: "error",
error_message: tr("Failed to parse response data")
}
}
if(response["status"] !== "ok") {
console.error(tr("Response status not okey. Error happend: %o"), response);
throw (response["errors"] || [])[0] || tr("Unknown error");
return {
status: "success"
};
}
export async function renew_data() : Promise<"success" | "login-required"> {
let response;
try {
response = await new Promise<any>((resolve, reject) => {
$.ajax({
url: api_url() + "?web-api/v1/renew-data",
type: "GET",
cache: false,
crossDomain: true,
data: {
"auth-key": _data.auth_key
},
success: resolve,
error: (xhr, status, error) => {
console.log(tr("Renew request failed %o: %o"), status, error);
reject(tr("request failed"));
}
})
});
} catch(error) {
throw tr("failed to send renew request");
}
if(response["status"] !== "ok") {
console.error(tr("Response status not okey. Error happend: %o"), response);
throw (response["errors"] || [])[0] || tr("Unknown error");
}
if(!response["success"]) {
if(response["code"] == 1) {
return "login-required";
}
throw "invalid error code (" + response["code"] + ")";
}
if(!response["data"] || !response["sign"])
throw tr("response missing data");
if(!response["success"]) {
if(response["code"] == 1) {
return "login-required";
}
console.debug(tr("Renew succeeded. Parsing data."));
try {
_data = new Data(_data.auth_key, response["data"], response["sign"]);
localStorage.setItem("teaspeak-forum-data", response["data"]);
localStorage.setItem("teaspeak-forum-sign", response["sign"]);
fidentity.update_forum();
} catch(error) {
console.error(tr("Failed to parse forum given data: %o"), error);
throw tr("failed to parse data");
}
return "success";
}
export async function logout() : Promise<void> {
if(!logged_in())
return;
let response;
try {
response = await new Promise<any>((resolve, reject) => {
$.ajax({
url: api_url() + "?web-api/v1/logout",
type: "GET",
cache: false,
crossDomain: true,
data: {
"auth-key": _data.auth_key
},
success: resolve,
error: (xhr, status, error) => {
console.log(tr("Logout request failed %o: %o"), status, error);
reject(tr("request failed"));
}
})
});
} catch(error) {
throw tr("failed to send logout request");
}
if(response["status"] !== "ok") {
console.error(tr("Response status not okey. Error happend: %o"), response);
throw (response["errors"] || [])[0] || tr("Unknown error");
}
if(!response["success"]) {
/* code 1 means not logged in, its an success */
if(response["code"] != 1) {
throw "invalid error code (" + response["code"] + ")";
}
if(!response["data"] || !response["sign"])
throw tr("response missing data");
console.debug(tr("Renew succeeded. Parsing data."));
try {
_data = new Data(_data.auth_key, response["data"], response["sign"]);
localStorage.setItem("teaspeak-forum-data", response["data"]);
localStorage.setItem("teaspeak-forum-sign", response["sign"]);
profiles.identities.update_forum();
} catch(error) {
console.error(tr("Failed to parse forum given data: %o"), error);
throw tr("failed to parse data");
}
return "success";
}
export async function logout() : Promise<void> {
if(!logged_in())
_data = undefined;
localStorage.removeItem("teaspeak-forum-data");
localStorage.removeItem("teaspeak-forum-sign");
localStorage.removeItem("teaspeak-forum-auth");
fidentity.update_forum();
}
loader.register_task(loader.Stage.JAVASCRIPT_INITIALIZING, {
name: "TeaForo initialize",
priority: 10,
function: async () => {
const raw_data = localStorage.getItem("teaspeak-forum-data");
const raw_sign = localStorage.getItem("teaspeak-forum-sign");
const forum_auth = localStorage.getItem("teaspeak-forum-auth");
if(!raw_data || !raw_sign || !forum_auth) {
console.log(tr("No TeaForo authentification found. TeaForo connection status: unconnected"));
return;
}
let response;
try {
response = await new Promise<any>((resolve, reject) => {
$.ajax({
url: api_url() + "?web-api/v1/logout",
type: "GET",
cache: false,
crossDomain: true,
data: {
"auth-key": _data.auth_key
},
success: resolve,
error: (xhr, status, error) => {
console.log(tr("Logout request failed %o: %o"), status, error);
reject(tr("request failed"));
}
})
});
_data = new Data(forum_auth, raw_data, raw_sign);
} catch(error) {
throw tr("failed to send logout request");
console.error(tr("Failed to initialize TeaForo connection from local data. Error: %o"), error);
return;
}
if(_data.should_renew()) {
console.info(tr("TeaForo data should be renewed. Executing renew."));
renew_data().then(status => {
if(status === "success") {
console.info(tr("TeaForo data has been successfully renewed."));
} else {
console.warn(tr("Failed to renew TeaForo data. New login required."));
localStorage.removeItem("teaspeak-forum-data");
localStorage.removeItem("teaspeak-forum-sign");
localStorage.removeItem("teaspeak-forum-auth");
}
}).catch(error => {
console.warn(tr("Failed to renew TeaForo data. An error occurred: %o"), error);
});
return;
}
if(response["status"] !== "ok") {
console.error(tr("Response status not okey. Error happend: %o"), response);
throw (response["errors"] || [])[0] || tr("Unknown error");
if(_data && _data.is_expired()) {
console.error(tr("TeaForo data is expired. TeaForo connection isn't available!"));
}
if(!response["success"]) {
/* code 1 means not logged in, its an success */
if(response["code"] != 1) {
throw "invalid error code (" + response["code"] + ")";
}
}
_data = undefined;
localStorage.removeItem("teaspeak-forum-data");
localStorage.removeItem("teaspeak-forum-sign");
localStorage.removeItem("teaspeak-forum-auth");
profiles.identities.update_forum();
}
loader.register_task(loader.Stage.JAVASCRIPT_INITIALIZING, {
name: "TeaForo initialize",
priority: 10,
function: async () => {
const raw_data = localStorage.getItem("teaspeak-forum-data");
const raw_sign = localStorage.getItem("teaspeak-forum-sign");
const forum_auth = localStorage.getItem("teaspeak-forum-auth");
if(!raw_data || !raw_sign || !forum_auth) {
console.log(tr("No TeaForo authentification found. TeaForo connection status: unconnected"));
return;
}
try {
_data = new Data(forum_auth, raw_data, raw_sign);
} catch(error) {
console.error(tr("Failed to initialize TeaForo connection from local data. Error: %o"), error);
return;
}
if(_data.should_renew()) {
console.info(tr("TeaForo data should be renewed. Executing renew."));
renew_data().then(status => {
if(status === "success") {
console.info(tr("TeaForo data has been successfully renewed."));
} else {
console.warn(tr("Failed to renew TeaForo data. New login required."));
localStorage.removeItem("teaspeak-forum-data");
localStorage.removeItem("teaspeak-forum-sign");
localStorage.removeItem("teaspeak-forum-auth");
}
}).catch(error => {
console.warn(tr("Failed to renew TeaForo data. An error occurred: %o"), error);
});
return;
}
if(_data && _data.is_expired()) {
console.error(tr("TeaForo data is expired. TeaForo connection isn't available!"));
}
}
})
}
})

View File

@ -1,45 +1,109 @@
//Used by CertAccept popup
interface Array<T> {
remove(elem?: T): boolean;
last?(): T;
declare global {
interface Array<T> {
remove(elem?: T): boolean;
last?(): T;
pop_front(): T | undefined;
pop_front(): T | undefined;
}
interface JSON {
map_to<T>(object: T, json: any, variables?: string | string[], validator?: (map_field: string, map_value: string) => boolean, variable_direction?: number) : number;
map_field_to<T>(object: T, value: any, field: string) : boolean;
}
type JQueryScrollType = "height" | "width";
interface JQuery<TElement = HTMLElement> {
render(values?: any) : string;
renderTag(values?: any) : JQuery<TElement>;
hasScrollBar(direction?: JQueryScrollType) : boolean;
visible_height() : number;
visible_width() : number;
/* bootstrap */
alert() : JQuery<TElement>;
modal(properties: any) : this;
bootstrapMaterialDesign() : this;
/* first element which matches the selector, could be the element itself or a parent */
firstParent(selector: string) : JQuery;
}
interface JQueryStatic<TElement extends Node = HTMLElement> {
spawn<K extends keyof HTMLElementTagNameMap>(tagName: K): JQuery<HTMLElementTagNameMap[K]>;
views: any;
}
interface String {
format(...fmt): string;
format(arguments: string[]): string;
}
interface Twemoji {
parse(message: string) : string;
}
let twemoji: Twemoji;
interface HighlightJS {
listLanguages() : string[];
getLanguage(name: string) : any | undefined;
highlight(language: string, text: string, ignore_illegals?: boolean) : HighlightJSResult;
highlightAuto(text: string) : HighlightJSResult;
}
interface HighlightJSResult {
language: string;
relevance: number;
value: string;
second_best?: any;
}
interface DOMPurify {
sanitize(html: string, config?: {
ADD_ATTR?: string[]
ADD_TAGS?: string[];
}) : string;
}
let DOMPurify: DOMPurify;
let remarkable: typeof window.remarkable;
class webkitAudioContext extends AudioContext {}
class webkitOfflineAudioContext extends OfflineAudioContext {}
interface Window {
readonly webkitAudioContext: typeof webkitAudioContext;
readonly AudioContext: typeof webkitAudioContext;
readonly OfflineAudioContext: typeof OfflineAudioContext;
readonly webkitOfflineAudioContext: typeof webkitOfflineAudioContext;
readonly RTCPeerConnection: typeof RTCPeerConnection;
readonly Pointer_stringify: any;
readonly jsrender: any;
twemoji: Twemoji;
hljs: HighlightJS;
remarkable: any;
require(id: string): any;
}
interface Navigator {
browserSpecs: {
name: string,
version: string
};
mozGetUserMedia(constraints: MediaStreamConstraints, successCallback: NavigatorUserMediaSuccessCallback, errorCallback: NavigatorUserMediaErrorCallback): void;
webkitGetUserMedia(constraints: MediaStreamConstraints, successCallback: NavigatorUserMediaSuccessCallback, errorCallback: NavigatorUserMediaErrorCallback): void;
}
}
interface JSON {
map_to<T>(object: T, json: any, variables?: string | string[], validator?: (map_field: string, map_value: string) => boolean, variable_direction?: number) : number;
map_field_to<T>(object: T, value: any, field: string) : boolean;
}
type JQueryScrollType = "height" | "width";
interface JQuery<TElement = HTMLElement> {
render(values?: any) : string;
renderTag(values?: any) : JQuery<TElement>;
hasScrollBar(direction?: JQueryScrollType) : boolean;
visible_height() : number;
visible_width() : number;
/* bootstrap */
alert() : JQuery<TElement>;
modal(properties: any) : this;
bootstrapMaterialDesign() : this;
/* first element which matches the selector, could be the element itself or a parent */
firstParent(selector: string) : JQuery;
}
interface JQueryStatic<TElement extends Node = HTMLElement> {
spawn<K extends keyof HTMLElementTagNameMap>(tagName: K): JQuery<HTMLElementTagNameMap[K]>;
views: any;
}
interface String {
format(...fmt): string;
format(arguments: string[]): string;
}
export function initialize() { }
if(!JSON.map_to) {
JSON.map_to = function <T>(object: T, json: any, variables?: string | string[], validator?: (map_field: string, map_value: string) => boolean, variable_direction?: number): number {
@ -131,6 +195,7 @@ if(typeof ($) !== "undefined") {
return $(document.createElement(tagName) as any);
}
}
if(!$.fn.renderTag) {
$.fn.renderTag = function (this: JQuery, values?: any) : JQuery {
let result;
@ -184,7 +249,8 @@ if(typeof ($) !== "undefined") {
const result = this.height();
this.attr("style", original_style || "");
return result;
}
};
if(!$.fn.visible_width)
$.fn.visible_width = function (this: JQuery<HTMLElement>) {
const original_style = this.attr("style");
@ -197,7 +263,8 @@ if(typeof ($) !== "undefined") {
const result = this.width();
this.attr("style", original_style || "");
return result;
}
};
if(!$.fn.firstParent)
$.fn.firstParent = function (this: JQuery<HTMLElement>, selector: string) {
if(this.is(selector))
@ -232,30 +299,6 @@ function concatenate(resultConstructor, ...arrays) {
return result;
}
function formatDate(secs: number) : string {
let years = Math.floor(secs / (60 * 60 * 24 * 365));
let days = Math.floor(secs / (60 * 60 * 24)) % 365;
let hours = Math.floor(secs / (60 * 60)) % 24;
let minutes = Math.floor(secs / 60) % 60;
let seconds = Math.floor(secs % 60);
let result = "";
if(years > 0)
result += years + " " + tr("years") + " ";
if(years > 0 || days > 0)
result += days + " " + tr("days") + " ";
if(years > 0 || days > 0 || hours > 0)
result += hours + " " + tr("hours") + " ";
if(years > 0 || days > 0 || hours > 0 || minutes > 0)
result += minutes + " " + tr("minutes") + " ";
if(years > 0 || days > 0 || hours > 0 || minutes > 0 || seconds > 0)
result += seconds + " " + tr("seconds") + " ";
else
result = tr("now") + " ";
return result.substr(0, result.length - 1);
}
function calculate_width(text: string) : number {
let element = $.spawn("div");
element.text(text)
@ -265,64 +308,4 @@ function calculate_width(text: string) : number {
let size = element.width();
element.detach();
return size;
}
interface Twemoji {
parse(message: string) : string;
}
declare let twemoji: Twemoji;
interface HighlightJS {
listLanguages() : string[];
getLanguage(name: string) : any | undefined;
highlight(language: string, text: string, ignore_illegals?: boolean) : HighlightJSResult;
highlightAuto(text: string) : HighlightJSResult;
}
interface HighlightJSResult {
language: string;
relevance: number;
value: string;
second_best?: any;
}
interface DOMPurify {
sanitize(html: string, config?: {
ADD_ATTR?: string[]
ADD_TAGS?: string[];
}) : string;
}
declare let DOMPurify: DOMPurify;
declare let remarkable: typeof window.remarkable;
declare class webkitAudioContext extends AudioContext {}
declare class webkitOfflineAudioContext extends OfflineAudioContext {}
interface Window {
readonly webkitAudioContext: typeof webkitAudioContext;
readonly AudioContext: typeof webkitAudioContext;
readonly OfflineAudioContext: typeof OfflineAudioContext;
readonly webkitOfflineAudioContext: typeof webkitOfflineAudioContext;
readonly RTCPeerConnection: typeof RTCPeerConnection;
readonly Pointer_stringify: any;
readonly jsrender: any;
twemoji: Twemoji;
hljs: HighlightJS;
remarkable: any;
require(id: string): any;
}
interface Navigator {
browserSpecs: {
name: string,
version: string
};
mozGetUserMedia(constraints: MediaStreamConstraints, successCallback: NavigatorUserMediaSuccessCallback, errorCallback: NavigatorUserMediaErrorCallback): void;
webkitGetUserMedia(constraints: MediaStreamConstraints, successCallback: NavigatorUserMediaSuccessCallback, errorCallback: NavigatorUserMediaErrorCallback): void;
}

View File

@ -1,6 +1,10 @@
/// <reference path="ui/elements/modal.ts" />
//Used by CertAccept popup
import {createErrorModal} from "tc-shared/ui/elements/Modal";
import {LogCategory} from "tc-shared/log";
import * as loader from "tc-loader";
import * as log from "tc-shared/log";
if(typeof(customElements) !== "undefined") {
try {
class X_Properties extends HTMLElement {}
@ -9,12 +13,12 @@ if(typeof(customElements) !== "undefined") {
customElements.define('x-properties', X_Properties, { extends: 'div' });
customElements.define('x-property', X_Property, { extends: 'div' });
} catch(error) {
console.warn("failed to define costum elements");
console.warn("failed to define costume elements");
}
}
/* T = value type */
interface SettingsKey<T> {
export interface SettingsKey<T> {
key: string;
fallback_keys?: string | string[];
@ -25,7 +29,7 @@ interface SettingsKey<T> {
require_restart?: boolean;
}
class SettingsBase {
export class SettingsBase {
protected static readonly UPDATE_DIRECT: boolean = true;
protected static transformStO?<T>(input?: string, _default?: T, default_type?: string) : T {
@ -77,7 +81,7 @@ class SettingsBase {
}
}
class StaticSettings extends SettingsBase {
export class StaticSettings extends SettingsBase {
private static _instance: StaticSettings;
static get instance() : StaticSettings {
if(!this._instance)
@ -139,7 +143,7 @@ class StaticSettings extends SettingsBase {
}
}
class Settings extends StaticSettings {
export class Settings extends StaticSettings {
static readonly KEY_USER_IS_NEW: SettingsKey<boolean> = {
key: 'user_is_new_user',
default_value: true
@ -433,7 +437,7 @@ class Settings extends StaticSettings {
}
}
class ServerSettings extends SettingsBase {
export class ServerSettings extends SettingsBase {
private cacheServer = {};
private _server_unique_id: string;
private _server_save_worker: NodeJS.Timer;
@ -511,4 +515,4 @@ class ServerSettings extends SettingsBase {
}
}
let settings: Settings;
export let settings: Settings;

View File

@ -1,4 +1,10 @@
enum Sound {
import * as log from "tc-shared/log";
import {LogCategory} from "tc-shared/log";
import {settings} from "tc-shared/settings";
import {ConnectionHandler} from "tc-shared/ConnectionHandler";
import * as sbackend from "tc-backend/audio/sounds";
export enum Sound {
SOUND_TEST = "sound.test",
SOUND_EGG = "sound.egg",
@ -61,235 +67,211 @@ enum Sound {
GROUP_CHANNEL_CHANGED_SELF = "group.channel.changed.self"
}
namespace sound {
export interface SoundHandle {
key: string;
filename: string;
}
export interface SoundHandle {
key: string;
filename: string;
}
export interface SoundFile {
path: string;
volume?: number;
}
export interface SoundFile {
path: string;
volume?: number;
}
let speech_mapping: {[key: string]:SoundHandle} = {};
let speech_mapping: {[key: string]:SoundHandle} = {};
let volume_require_save = false;
let speech_volume: {[key: string]:number} = {};
let master_volume: number;
let volume_require_save = false;
let speech_volume: {[key: string]:number} = {};
let master_volume: number;
let overlap_sounds: boolean;
let ignore_muted: boolean;
let overlap_sounds: boolean;
let ignore_muted: boolean;
let master_mixed: GainNode;
let master_mixed: GainNode;
function register_sound(key: string, file: string) {
speech_mapping[key] = {key: key, filename: file} as SoundHandle;
}
function register_sound(key: string, file: string) {
speech_mapping[key] = {key: key, filename: file} as SoundHandle;
}
export function get_sound_volume(sound: Sound, default_volume?: number) : number {
let result = speech_volume[sound];
if(typeof(result) === "undefined") {
if(typeof(default_volume) !== "undefined")
result = default_volume;
else
result = 1;
}
return result;
}
export function set_sound_volume(sound: Sound, volume: number) {
volume_require_save = volume_require_save || speech_volume[sound] != volume;
speech_volume[sound] = volume == 1 ? undefined : volume;
}
export function get_master_volume() : number {
return master_volume;
}
export function set_master_volume(volume: number) {
volume_require_save = volume_require_save || master_volume != volume;
master_volume = volume;
if(master_mixed) {
if(master_mixed.gain.setValueAtTime)
master_mixed.gain.setValueAtTime(volume, 0);
else
master_mixed.gain.value = volume;
}
}
export function overlap_activated() : boolean {
return overlap_sounds;
}
export function set_overlap_activated(flag: boolean) {
volume_require_save = volume_require_save || overlap_sounds != flag;
overlap_sounds = flag;
}
export function ignore_output_muted() : boolean {
return ignore_muted;
}
export function set_ignore_output_muted(flag: boolean) {
volume_require_save = volume_require_save || ignore_muted != flag;
ignore_muted = flag;
}
export function reinitialisize_audio() {
const context = audio.player.context();
const destination = audio.player.destination();
if(master_mixed)
master_mixed.disconnect();
master_mixed = context.createGain();
if(master_mixed.gain.setValueAtTime)
master_mixed.gain.setValueAtTime(master_volume, 0);
export function get_sound_volume(sound: Sound, default_volume?: number) : number {
let result = speech_volume[sound];
if(typeof(result) === "undefined") {
if(typeof(default_volume) !== "undefined")
result = default_volume;
else
master_mixed.gain.value = master_volume;
master_mixed.connect(destination);
result = 1;
}
return result;
}
export function save() {
if(volume_require_save) {
volume_require_save = false;
export function set_sound_volume(sound: Sound, volume: number) {
volume_require_save = volume_require_save || speech_volume[sound] != volume;
speech_volume[sound] = volume == 1 ? undefined : volume;
}
const data: any = {};
data.version = 1;
export function get_master_volume() : number {
return master_volume;
}
for(const key in Sound) {
if(typeof(speech_volume[Sound[key]]) !== "undefined")
data[Sound[key]] = speech_volume[Sound[key]];
}
data.master = master_volume;
data.overlap = overlap_sounds;
data.ignore_muted = ignore_muted;
export function set_master_volume(volume: number) {
volume_require_save = volume_require_save || master_volume != volume;
master_volume = volume;
if(master_mixed) {
if(master_mixed.gain.setValueAtTime)
master_mixed.gain.setValueAtTime(volume, 0);
else
master_mixed.gain.value = volume;
}
}
settings.changeGlobal("sound_volume", JSON.stringify(data));
export function overlap_activated() : boolean {
return overlap_sounds;
}
export function set_overlap_activated(flag: boolean) {
volume_require_save = volume_require_save || overlap_sounds != flag;
overlap_sounds = flag;
}
export function ignore_output_muted() : boolean {
return ignore_muted;
}
export function set_ignore_output_muted(flag: boolean) {
volume_require_save = volume_require_save || ignore_muted != flag;
ignore_muted = flag;
}
export function save() {
if(volume_require_save) {
volume_require_save = false;
const data: any = {};
data.version = 1;
for(const key of Object.keys(Sound)) {
if(typeof(speech_volume[Sound[key]]) !== "undefined")
data[Sound[key]] = speech_volume[Sound[key]];
}
data.master = master_volume;
data.overlap = overlap_sounds;
data.ignore_muted = ignore_muted;
settings.changeGlobal("sound_volume", JSON.stringify(data));
}
}
export function initialize() : Promise<void> {
$.ajaxSetup({
beforeSend: function(jqXHR,settings){
if (settings.dataType === 'binary') {
settings.xhr().responseType = 'arraybuffer';
settings.processData = false;
}
}
});
/* volumes */
{
const data = JSON.parse(settings.static_global("sound_volume", "{}"));
for(const sound_key of Object.keys(Sound)) {
if(typeof(data[Sound[sound_key]]) !== "undefined")
speech_volume[Sound[sound_key]] = data[Sound[sound_key]];
}
master_volume = typeof(data.master) === "number" ? data.master : 1;
overlap_sounds = typeof(data.overlap) === "boolean" ? data.overlap : true;
ignore_muted = typeof(data.ignore_muted) === "boolean" ? data.ignore_muted : false;
}
export function initialize() : Promise<void> {
$.ajaxSetup({
beforeSend: function(jqXHR,settings){
if (settings.dataType === 'binary') {
settings.xhr().responseType = 'arraybuffer';
settings.processData = false;
}
}
register_sound("message.received", "effects/message_received.wav");
register_sound("message.send", "effects/message_send.wav");
manager = new SoundManager(undefined);
return new Promise<void>((resolve, reject) => {
$.ajax({
url: "audio/speech/mapping.json",
success: response => {
if(typeof(response) === "string")
response = JSON.parse(response);
for(const entry of response)
register_sound(entry.key, "speech/" + entry.file);
resolve();
},
error: error => {
log.error(LogCategory.AUDIO, "error: %o", error);
reject();
},
timeout: 5000,
async: true,
type: 'GET'
});
});
}
/* volumes */
{
const data = JSON.parse(settings.static_global("sound_volume", "{}"));
for(const sound_key in Sound) {
if(typeof(data[Sound[sound_key]]) !== "undefined")
speech_volume[Sound[sound_key]] = data[Sound[sound_key]];
}
export interface PlaybackOptions {
ignore_muted?: boolean;
ignore_overlap?: boolean;
master_volume = typeof(data.master) === "number" ? data.master : 1;
overlap_sounds = typeof(data.overlap) === "boolean" ? data.overlap : true;
ignore_muted = typeof(data.ignore_muted) === "boolean" ? data.ignore_muted : false;
}
default_volume?: number;
register_sound("message.received", "effects/message_received.wav");
register_sound("message.send", "effects/message_send.wav");
callback?: (flag: boolean) => any;
}
manager = new SoundManager(undefined);
audio.player.on_ready(reinitialisize_audio);
return new Promise<void>((resolve, reject) => {
$.ajax({
url: "audio/speech/mapping.json",
success: response => {
if(typeof(response) === "string")
response = JSON.parse(response);
for(const entry of response)
register_sound(entry.key, "speech/" + entry.file);
resolve();
},
error: error => {
log.error(LogCategory.AUDIO, "error: %o", error);
reject();
},
timeout: 5000,
async: true,
type: 'GET'
});
});
export async function resolve_sound(sound: Sound) : Promise<SoundHandle> {
const file: SoundHandle = speech_mapping[sound];
if(!file) throw tr("Missing sound handle");
return file;
}
export let manager: SoundManager;
export class SoundManager {
private readonly _handle: ConnectionHandler;
private _playing_sounds: {[key: string]:number} = {};
constructor(handle: ConnectionHandler) {
this._handle = handle;
}
export interface PlaybackOptions {
ignore_muted?: boolean;
ignore_overlap?: boolean;
play(_sound: Sound, options?: PlaybackOptions) {
options = options || {};
default_volume?: number;
const volume = get_sound_volume(_sound, options.default_volume);
log.info(LogCategory.AUDIO, tr("Replaying sound %s (Sound volume: %o | Master volume %o)"), _sound, volume, master_volume);
callback?: (flag: boolean) => any;
}
if(volume == 0 || master_volume == 0)
return;
export async function resolve_sound(sound: Sound) : Promise<SoundHandle> {
const file: SoundHandle = speech_mapping[sound];
if(!file) throw tr("Missing sound handle");
if(this._handle && !options.ignore_muted && !ignore_output_muted() && this._handle.client_status.output_muted)
return;
return file;
}
resolve_sound(_sound).then(handle => {
if(!handle) return;
export let manager: SoundManager;
export class SoundManager {
private readonly _handle: ConnectionHandler;
private _playing_sounds: {[key: string]:number} = {};
constructor(handle: ConnectionHandler) {
this._handle = handle;
}
play(_sound: Sound, options?: PlaybackOptions) {
options = options || {};
const volume = get_sound_volume(_sound, options.default_volume);
log.info(LogCategory.AUDIO, tr("Replaying sound %s (Sound volume: %o | Master volume %o)"), _sound, volume, master_volume);
if(volume == 0 || master_volume == 0)
return;
if(this._handle && !options.ignore_muted && !sound.ignore_output_muted() && this._handle.client_status.output_muted)
return;
const context = audio.player.context();
if(!context) {
log.warn(LogCategory.AUDIO, tr("Tried to replay a sound without an audio context (Sound: %o). Dropping playback"), _sound);
if(!options.ignore_overlap && (this._playing_sounds[handle.filename] > 0) && !overlap_activated()) {
log.info(LogCategory.AUDIO, tr("Dropping requested playback for sound %s because it would overlap."), _sound);
return;
}
sound.resolve_sound(_sound).then(handle => {
if(!handle) return;
if(!options.ignore_overlap && (this._playing_sounds[handle.filename] > 0) && !sound.overlap_activated()) {
log.info(LogCategory.AUDIO, tr("Dropping requested playback for sound %s because it would overlap."), _sound);
return;
}
this._playing_sounds[handle.filename] = (this._playing_sounds[handle.filename] || 0) + 1;
audio.sounds.play_sound({
path: "audio/" + handle.filename,
volume: volume * master_volume
}).then(() => {
if(options.callback)
options.callback(true);
}).catch(error => {
log.warn(LogCategory.AUDIO, tr("Failed to replay sound %s: %o"), handle.filename, error);
if(options.callback)
options.callback(false);
}).then(() => {
this._playing_sounds[handle.filename]--;
});
this._playing_sounds[handle.filename] = (this._playing_sounds[handle.filename] || 0) + 1;
sbackend.play_sound({
path: "audio/" + handle.filename,
volume: volume * master_volume
}).then(() => {
if(options.callback)
options.callback(true);
}).catch(error => {
log.warn(LogCategory.AUDIO, tr("Failed to replay sound %o because it could not be resolved: %o"), sound, error);
log.warn(LogCategory.AUDIO, tr("Failed to replay sound %s: %o"), handle.filename, error);
if(options.callback)
options.callback(false);
}).then(() => {
this._playing_sounds[handle.filename]--;
});
}
}).catch(error => {
log.warn(LogCategory.AUDIO, tr("Failed to replay sound %o because it could not be resolved: %o"), _sound, error);
if(options.callback)
options.callback(false);
});
}
}

View File

@ -1,243 +1,244 @@
namespace stats {
const LOG_PREFIX = "[Statistics] ";
import {LogCategory} from "tc-shared/log";
import * as log from "tc-shared/log";
export enum CloseCodes {
UNSET = 3000,
RECONNECT = 3001,
INTERNAL_ERROR = 3002,
const LOG_PREFIX = "[Statistics] ";
BANNED = 3100,
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];
}
enum ConnectionState {
CONNECTING,
INITIALIZING,
CONNECTED,
UNSET
}
return target_object;
}
export class SessionConfig {
/*
* All collected statistics will only be cached by the stats server.
* No data will be saved.
*/
volatile_collection_only?: boolean;
export function initialize(config: Config) {
current_config = initialize_config_object(config || {}, DEFAULT_CONFIG);
if(current_config.verbose)
log.info(LogCategory.STATISTICS, tr("Initializing statistics with this config: %o"), current_config);
/*
* 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;
}
connection.start_connection();
}
export class Config extends SessionConfig {
verbose?: boolean;
export function register_user_count_listener(listener: UserCountListener) {
user_count_listener.push(listener);
}
reconnect_interval?: number;
}
export function all_user_count_listener() : UserCountListener[] {
return user_count_listener;
}
export interface UserCountData {
online_users: number;
unique_online_users: number;
}
export function deregister_user_count_listener(listener: UserCountListener) {
user_count_listener.remove(listener);
}
export type UserCountListener = (data: UserCountData) => any;
namespace connection {
let connection: WebSocket;
export let connection_state: ConnectionState = ConnectionState.UNSET;
let reconnect_timer: NodeJS.Timer;
let current_config: Config;
export function start_connection() {
cancel_reconnect();
close_connection();
let last_user_count_update: number;
let user_count_listener: UserCountListener[] = [];
connection_state = ConnectionState.CONNECTING;
const DEFAULT_CONFIG: Config = {
verbose: true,
reconnect_interval: 5000,
anonymize_ip_addresses: true,
volatile_collection_only: false
};
connection = new WebSocket('wss://web-stats.teaspeak.de:27790');
if(!connection)
connection = new WebSocket('wss://localhost:27788');
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]);
{
const connection_copy = connection;
connection.onclose = (event: CloseEvent) => {
if(connection_copy !== connection) return;
if(typeof(target_object[key]) !== 'undefined')
continue;
if(current_config.verbose)
log.warn(LogCategory.STATISTICS, tr("Lost connection to statistics server (Connection closed). Reason: %o. Event object: %o"), CloseCodes[event.code] || event.code, event);
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)
log.info(LogCategory.STATISTICS, 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();
close_connection();
connection_state = ConnectionState.CONNECTING;
connection = new WebSocket('wss://web-stats.teaspeak.de:27790');
if(!connection)
connection = new WebSocket('wss://localhost:27788');
{
const connection_copy = connection;
connection.onclose = (event: CloseEvent) => {
if(connection_copy !== connection) return;
if(current_config.verbose)
log.warn(LogCategory.STATISTICS, 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)
log.info(LogCategory.STATISTICS, 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)
log.warn(LogCategory.STATISTICS, tr("Received an error. Closing connection. Object: %o"), event);
connection.close(CloseCodes.INTERNAL_ERROR);
if(event.code != CloseCodes.BANNED)
invoke_reconnect();
};
};
connection.onmessage = (event: MessageEvent) => {
if(connection_copy !== connection) return;
connection.onopen = () => {
if(connection_copy !== connection) return;
if(typeof(event.data) !== 'string') {
if(current_config.verbose)
log.info(LogCategory.STATISTICS, tr("Received an message which isn't a string. Event object: %o"), event);
return;
}
if(current_config.verbose)
log.info(LogCategory.STATISTICS, tr("Successfully connected to server. Initializing session."));
handle_message(event.data as string);
};
}
connection_state = ConnectionState.INITIALIZING;
initialize_session();
};
connection.onerror = (event: ErrorEvent) => {
if(connection_copy !== connection) return;
if(current_config.verbose)
log.warn(LogCategory.STATISTICS, tr("Received an error. Closing connection. Object: %o"), event);
connection.close(CloseCodes.INTERNAL_ERROR);
invoke_reconnect();
};
connection.onmessage = (event: MessageEvent) => {
if(connection_copy !== connection) return;
if(typeof(event.data) !== 'string') {
if(current_config.verbose)
log.info(LogCategory.STATISTICS, tr("Received an message which isn't a string. Event object: %o"), event);
return;
}
handle_message(event.data as string);
};
}
}
export function close_connection() {
if(connection) {
const connection_copy = connection;
connection = undefined;
try {
connection_copy.close(3001);
} catch(_) {}
}
}
function invoke_reconnect() {
close_connection();
if(reconnect_timer) {
clearTimeout(reconnect_timer);
reconnect_timer = undefined;
}
export function close_connection() {
if(connection) {
const connection_copy = connection;
connection = undefined;
try {
connection_copy.close(3001);
} catch(_) {}
}
}
function invoke_reconnect() {
close_connection();
if(reconnect_timer) {
clearTimeout(reconnect_timer);
reconnect_timer = undefined;
}
if(current_config.verbose)
log.info(LogCategory.STATISTICS, tr("Scheduled reconnect in %dms"), current_config.reconnect_interval);
reconnect_timer = setTimeout(() => {
if(current_config.verbose)
log.info(LogCategory.STATISTICS, tr("Scheduled reconnect in %dms"), current_config.reconnect_interval);
log.info(LogCategory.STATISTICS, tr("Reconnecting"));
start_connection();
}, current_config.reconnect_interval);
}
reconnect_timer = setTimeout(() => {
if(current_config.verbose)
log.info(LogCategory.STATISTICS, 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];
}
export function cancel_reconnect() {
if(reconnect_timer) {
clearTimeout(reconnect_timer);
reconnect_timer = undefined;
}
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)
log.debug(LogCategory.STATISTICS, tr("Handling message of type %s"), type);
handler[type](data);
} else if(current_config.verbose) {
log.warn(LogCategory.STATISTICS, 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);
}
function send_message(type: string, data: any) {
connection.send(JSON.stringify({
type: type,
data: data
}));
interface NotifyInitialized {}
function handle_notify_initialized(json: NotifyInitialized) {
if(current_config.verbose)
log.info(LogCategory.STATISTICS, tr("Session successfully initialized."));
connection_state = ConnectionState.CONNECTED;
}
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)
log.debug(LogCategory.STATISTICS, tr("Handling message of type %s"), type);
handler[type](data);
} else if(current_config.verbose) {
log.warn(LogCategory.STATISTICS, 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)
log.info(LogCategory.STATISTICS, tr("Session successfully initialized."));
connection_state = ConnectionState.CONNECTED;
}
handler["notifyinitialized"] = handle_notify_initialized;
handler["notifyusercount"] = handle_notify_user_count;
}
handler["notifyinitialized"] = handle_notify_initialized;
handler["notifyusercount"] = handle_notify_user_count;
}
}

View File

@ -1,12 +1,26 @@
/// <reference path="view.ts" />
/// <reference path="../utils/helpers.ts" />
import {ChannelTree} from "tc-shared/ui/view";
import {ClientEntry} from "tc-shared/ui/client";
import * as log from "tc-shared/log";
import {LogCategory, LogType} from "tc-shared/log";
import PermissionType from "tc-shared/permission/PermissionType";
import {settings, Settings} from "tc-shared/settings";
import * as contextmenu from "tc-shared/ui/elements/ContextMenu";
import {Sound} from "tc-shared/sound/Sounds";
import {createErrorModal, createInfoModal, createInputModal} from "tc-shared/ui/elements/Modal";
import {CommandResult} from "tc-shared/connection/ServerConnectionDeclaration";
import * as htmltags from "./htmltags";
import {hashPassword} from "tc-shared/utils/helpers";
import * as server_log from "tc-shared/ui/frames/server_log";
import {openChannelInfo} from "tc-shared/ui/modal/ModalChannelInfo";
import {createChannelModal} from "tc-shared/ui/modal/ModalCreateChannel";
import {formatMessage} from "tc-shared/ui/frames/chat";
enum ChannelType {
export enum ChannelType {
PERMANENT,
SEMI_PERMANENT,
TEMPORARY
}
namespace ChannelType {
export namespace ChannelType {
export function normalize(mode: ChannelType) {
let value: string = ChannelType[mode];
value = value.toLowerCase();
@ -14,13 +28,13 @@ namespace ChannelType {
}
}
enum ChannelSubscribeMode {
export enum ChannelSubscribeMode {
SUBSCRIBED,
UNSUBSCRIBED,
INHERITED
}
class ChannelProperties {
export class ChannelProperties {
channel_order: number = 0;
channel_name: string = "";
channel_name_phonetic: string = "";
@ -55,7 +69,7 @@ class ChannelProperties {
channel_conversation_history_length: number = -1;
}
class ChannelEntry {
export class ChannelEntry {
channelTree: ChannelTree;
channelId: number;
parent?: ChannelEntry;
@ -525,7 +539,7 @@ class ChannelEntry {
name: tr("Show channel info"),
callback: () => {
trigger_close = false;
Modals.openChannelInfo(this);
openChannelInfo(this);
},
icon_class: "client-about"
},
@ -565,7 +579,7 @@ class ChannelEntry {
name: tr("Edit channel"),
invalidPermission: !channelModify,
callback: () => {
Modals.createChannelModal(this.channelTree.client, this, undefined, this.channelTree.client.permissions, (changes?, permissions?) => {
createChannelModal(this.channelTree.client, this, undefined, this.channelTree.client.permissions, (changes?, permissions?) => {
if(changes) {
changes["cid"] = this.channelId;
this.channelTree.client.serverConnection.send_command("channeledit", changes);
@ -617,7 +631,7 @@ class ChannelEntry {
error = error.extra_message || error.message;
}
createErrorModal(tr("Failed to create bot"), MessageHelper.formatMessage(tr("Failed to create the music bot:<br>{0}"), error)).open();
createErrorModal(tr("Failed to create bot"), formatMessage(tr("Failed to create the music bot:<br>{0}"), error)).open();
});
}
},
@ -834,7 +848,7 @@ class ChannelEntry {
!this.channelTree.client.permissions.neededPermission(PermissionType.B_CHANNEL_JOIN_IGNORE_PASSWORD).granted(1)) {
createInputModal(tr("Channel password"), tr("Channel password:"), () => true, text => {
if(typeof(text) == typeof(true)) return;
helpers.hashPassword(text as string).then(result => {
hashPassword(text as string).then(result => {
this._cachedPassword = result;
this.joinChannel();
this.updateChannelTypeIcon();
@ -923,7 +937,7 @@ class ChannelEntry {
this._tag_channel.find(".marker-text-unread").toggleClass("hidden", !flag);
}
log_data() : log.server.base.Channel {
log_data() : server_log.base.Channel {
return {
channel_name: this.channelName(),
channel_id: this.channelId

View File

@ -1,8 +1,34 @@
/// <reference path="channel.ts" />
/// <reference path="modal/ModalChangeVolume.ts" />
/// <reference path="client_move.ts" />
import * as contextmenu from "tc-shared/ui/elements/ContextMenu";
import {channel_tree, Registry} from "tc-shared/events";
import {ChannelTree} from "tc-shared/ui/view";
import * as log from "tc-shared/log";
import {LogCategory, LogType} from "tc-shared/log";
import {Settings, settings} from "tc-shared/settings";
import {KeyCode, SpecialKey} from "tc-shared/PPTListener";
import {Sound} from "tc-shared/sound/Sounds";
import {Group, GroupManager, GroupTarget, GroupType} from "tc-shared/permission/GroupManager";
import PermissionType from "tc-shared/permission/PermissionType";
import {createErrorModal, createInputModal} from "tc-shared/ui/elements/Modal";
import * as htmltags from "tc-shared/ui/htmltags";
import * as server_log from "tc-shared/ui/frames/server_log";
import {CommandResult} from "tc-shared/connection/ServerConnectionDeclaration";
import {ChannelEntry} from "tc-shared/ui/channel";
import {ConnectionHandler, ViewReasonId} from "tc-shared/ConnectionHandler";
import {voice} from "tc-shared/connection/ConnectionBase";
import VoiceClient = voice.VoiceClient;
import {spawnPermissionEdit} from "tc-shared/ui/modal/permission/ModalPermissionEdit";
import {createServerGroupAssignmentModal} from "tc-shared/ui/modal/ModalGroupAssignment";
import {openClientInfo} from "tc-shared/ui/modal/ModalClientInfo";
import {spawnBanClient} from "tc-shared/ui/modal/ModalBanClient";
import {spawnChangeVolume} from "tc-shared/ui/modal/ModalChangeVolume";
import {spawnChangeLatency} from "tc-shared/ui/modal/ModalChangeLatency";
import {spawnPlaylistEdit} from "tc-shared/ui/modal/ModalPlaylistEdit";
import {formatMessage} from "tc-shared/ui/frames/chat";
import {spawnYesNo} from "tc-shared/ui/modal/ModalYesNo";
import * as ppt from "tc-backend/ppt";
import * as hex from "tc-shared/crypto/hex";
enum ClientType {
export enum ClientType {
CLIENT_VOICE,
CLIENT_QUERY,
CLIENT_INTERNAL,
@ -11,7 +37,7 @@ enum ClientType {
CLIENT_UNDEFINED
}
class ClientProperties {
export class ClientProperties {
client_type: ClientType = ClientType.CLIENT_VOICE; //TeamSpeaks type
client_type_exact: ClientType = ClientType.CLIENT_VOICE;
@ -57,7 +83,7 @@ class ClientProperties {
client_is_priority_speaker: boolean = false;
}
class ClientConnectionInfo {
export 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;
@ -109,8 +135,8 @@ class ClientConnectionInfo {
connection_client_port: number = -1;
}
class ClientEntry {
readonly events: events.Registry<events.channel_tree.client>;
export class ClientEntry {
readonly events: Registry<channel_tree.client>;
protected _clientId: number;
protected _channel: ChannelEntry;
@ -121,7 +147,7 @@ class ClientEntry {
protected _speaking: boolean;
protected _listener_initialized: boolean;
protected _audio_handle: connection.voice.VoiceClient;
protected _audio_handle: VoiceClient;
protected _audio_volume: number;
protected _audio_muted: boolean;
@ -136,7 +162,7 @@ class ClientEntry {
channelTree: ChannelTree;
constructor(clientId: number, clientName, properties: ClientProperties = new ClientProperties()) {
this.events = new events.Registry<events.channel_tree.client>();
this.events = new Registry<channel_tree.client>();
this._properties = properties;
this._properties.client_nickname = clientName;
@ -181,7 +207,7 @@ class ClientEntry {
this._channel = undefined;
}
set_audio_handle(handle: connection.voice.VoiceClient) {
set_audio_handle(handle: VoiceClient) {
if(this._audio_handle === handle)
return;
@ -200,7 +226,7 @@ class ClientEntry {
handle.callback_stopped = () => this.speaking = false;
}
get_audio_handle() : connection.voice.VoiceClient {
get_audio_handle() : VoiceClient {
return this._audio_handle;
}
@ -285,7 +311,7 @@ class ClientEntry {
let clients = this.channelTree.currently_selected as (ClientEntry | ClientEntry[]);
if(ppt.key_pressed(ppt.SpecialKey.SHIFT)) {
if(ppt.key_pressed(SpecialKey.SHIFT)) {
if(clients != this && !($.isArray(clients) && clients.indexOf(this) != -1))
clients = $.isArray(clients) ? [...clients, this] : [clients, this];
} else {
@ -418,20 +444,20 @@ class ClientEntry {
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()
callback: () => 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()
callback: () => 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) => {
createServerGroupAssignmentModal(this, (groups, flag) => {
if(groups.length == 0) return Promise.resolve(true);
if(groups.length == 1) {
@ -490,7 +516,7 @@ class ClientEntry {
type: contextmenu.MenuEntryType.ENTRY,
icon_class: "client-about",
name: tr("Show client info"),
callback: () => Modals.openClientInfo(this)
callback: () => openClientInfo(this)
},
contextmenu.Entry.HR(),
{
@ -582,7 +608,7 @@ class ClientEntry {
name: tr("Ban client"),
invalidPermission: !this.channelTree.client.permissions.neededPermission(PermissionType.I_CLIENT_BAN_MAX_BANTIME).granted(1),
callback: () => {
Modals.spawnBanClient(this.channelTree.client, [{
spawnBanClient(this.channelTree.client, [{
name: this.properties.client_nickname,
unique_id: this.properties.client_unique_identifier
}], (data) => {
@ -622,7 +648,7 @@ class ClientEntry {
icon_class: "client-volume",
name: tr("Change Volume"),
callback: () => {
Modals.spawnChangeVolume(this, true, this._audio_volume, undefined, volume => {
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)
@ -635,7 +661,7 @@ class ClientEntry {
type: contextmenu.MenuEntryType.ENTRY,
name: tr("Change playback latency"),
callback: () => {
Modals.spawnChangeLatency(this, this._audio_handle.latency_settings(), () => {
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), this._audio_handle.support_flush ? () => {
@ -843,7 +869,7 @@ class ClientEntry {
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, {
this.channelTree.client.log.log(server_log.Type.CLIENT_NICKNAME_CHANGED, {
own_client: false,
client: this.log_data(),
new_name: variable.value,
@ -1082,7 +1108,7 @@ class ClientEntry {
this.tag.css('padding-left', (5 + (index + 2) * 16) + "px");
}
log_data() : log.server.base.Client {
log_data() : server_log.base.Client {
return {
client_unique_id: this.properties.client_unique_identifier,
client_name: this.clientNickName(),
@ -1123,7 +1149,7 @@ class ClientEntry {
}
}
class LocalClientEntry extends ClientEntry {
export class LocalClientEntry extends ClientEntry {
handle: ConnectionHandler;
private renaming: boolean;
@ -1216,14 +1242,14 @@ class LocalClientEntry extends ClientEntry {
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, {
this.channelTree.client.log.log(server_log.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, {
this.channelTree.client.log.log(server_log.Type.CLIENT_NICKNAME_CHANGE_FAILED, {
reason: e.extra_message
});
this.openRename();
@ -1232,7 +1258,7 @@ class LocalClientEntry extends ClientEntry {
}
}
class MusicClientProperties extends ClientProperties {
export class MusicClientProperties extends ClientProperties {
player_state: number = 0;
player_volume: number = 0;
@ -1264,7 +1290,7 @@ class MusicClientProperties extends ClientProperties {
}
*/
class SongInfo {
export class SongInfo {
song_id: number = 0;
song_url: string = "";
song_invoker: number = 0;
@ -1277,7 +1303,7 @@ class SongInfo {
song_length: number = 0;
}
class MusicClientPlayerInfo extends SongInfo {
export class MusicClientPlayerInfo extends SongInfo {
bot_id: number = 0;
player_state: number = 0;
@ -1290,7 +1316,7 @@ class MusicClientPlayerInfo extends SongInfo {
player_description: string = "";
}
class MusicClientEntry extends ClientEntry {
export class MusicClientEntry extends ClientEntry {
private _info_promise: Promise<MusicClientPlayerInfo>;
private _info_promise_age: number = 0;
private _info_promise_resolve: any;
@ -1366,7 +1392,7 @@ class MusicClientEntry extends ClientEntry {
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);
spawnPlaylistEdit(this.channelTree.client, entry);
return;
}
}
@ -1435,7 +1461,7 @@ class MusicClientEntry extends ClientEntry {
icon_class: "client-volume",
name: tr("Change local volume"),
callback: () => {
Modals.spawnChangeVolume(this, true, this._audio_handle.get_volume(), undefined, volume => {
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);
});
@ -1450,7 +1476,7 @@ class MusicClientEntry extends ClientEntry {
if(max_volume < 0)
max_volume = 100;
Modals.spawnChangeVolume(this, false, this.properties.player_volume, max_volume / 100, value => {
spawnChangeVolume(this, false, this.properties.player_volume, max_volume / 100, value => {
if(typeof(value) !== "number")
return;
@ -1467,7 +1493,7 @@ class MusicClientEntry extends ClientEntry {
type: contextmenu.MenuEntryType.ENTRY,
name: tr("Change playback latency"),
callback: () => {
Modals.spawnChangeLatency(this, this._audio_handle.latency_settings(), () => {
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), this._audio_handle.support_flush ? () => {
@ -1482,8 +1508,8 @@ class MusicClientEntry extends ClientEntry {
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 => {
const tag = $.spawn("div").append(formatMessage(tr("Do you really want to delete {0}"), this.createChatTag(false)));
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

View File

@ -1,6 +1,10 @@
/// <reference path="client.ts" />
import {ChannelTree} from "tc-shared/ui/view";
import * as log from "tc-shared/log";
import {LogCategory} from "tc-shared/log";
import {ClientEntry} from "tc-shared/ui/client";
import {ChannelEntry} from "tc-shared/ui/channel";
class ClientMover {
export class ClientMover {
static readonly listener_root = $(document);
static readonly move_element = $("#mouse-move");
readonly channel_tree: ChannelTree;

View File

@ -1,5 +1,15 @@
interface JQuery<TElement = HTMLElement> {
dividerfy() : this;
import {settings} from "tc-shared/settings";
import {LogCategory} from "tc-shared/log";
import * as log from "tc-shared/log";
declare global {
interface JQuery<TElement = HTMLElement> {
dividerfy() : this;
}
}
export function initialize() {
}
if(!$.fn.dividerfy) {
@ -58,8 +68,8 @@ if(!$.fn.dividerfy) {
Math.max(previous_offset.top + previous_element.height(), next_offset.top + next_element.height());
*/
let previous = 0;
let next = 0;
let previous;
let next;
if(current < min) {
previous = 0;
next = 1;
@ -89,7 +99,7 @@ if(!$.fn.dividerfy) {
}));
};
const listener_up = (event: MouseEvent) => {
const listener_up = () => {
document.removeEventListener('mousemove', listener_move);
document.removeEventListener('touchmove', listener_move);

View File

@ -1,82 +1,80 @@
namespace contextmenu {
export interface MenuEntry {
callback?: () => void;
type: MenuEntryType;
name: (() => string) | string;
icon_class?: string;
icon_path?: string;
disabled?: boolean;
visible?: boolean;
export interface MenuEntry {
callback?: () => void;
type: MenuEntryType;
name: (() => string) | string;
icon_class?: string;
icon_path?: string;
disabled?: boolean;
visible?: boolean;
checkbox_checked?: boolean;
checkbox_checked?: boolean;
invalidPermission?: boolean;
sub_menu?: MenuEntry[];
}
invalidPermission?: boolean;
sub_menu?: MenuEntry[];
}
export enum MenuEntryType {
CLOSE,
ENTRY,
CHECKBOX,
HR,
SUB_MENU
}
export enum MenuEntryType {
CLOSE,
ENTRY,
CHECKBOX,
HR,
SUB_MENU
}
export class Entry {
static HR() {
return {
callback: () => {},
type: MenuEntryType.HR,
name: "",
icon: ""
};
export class Entry {
static HR() {
return {
callback: () => {},
type: MenuEntryType.HR,
name: "",
icon: ""
};
};
static CLOSE(callback: () => void) {
return {
callback: callback,
type: MenuEntryType.CLOSE,
name: "",
icon: ""
};
}
}
export interface ContextMenuProvider {
despawn_context_menu();
spawn_context_menu(x: number, y: number, ...entries: MenuEntry[]);
initialize();
finalize();
html_format_enabled() : boolean;
}
let provider: ContextMenuProvider;
export function spawn_context_menu(x: number, y: number, ...entries: MenuEntry[]) {
if(!provider) {
console.error(tr("Failed to spawn context menu! Missing provider!"));
return;
}
provider.spawn_context_menu(x, y, ...entries);
}
export function despawn_context_menu() {
if(!provider)
return;
provider.despawn_context_menu();
}
export function get_provider() : ContextMenuProvider { return provider; }
export function set_provider(_provider: ContextMenuProvider) {
provider = _provider;
provider.initialize();
static CLOSE(callback: () => void) {
return {
callback: callback,
type: MenuEntryType.CLOSE,
name: "",
icon: ""
};
}
}
class HTMLContextMenuProvider implements contextmenu.ContextMenuProvider {
export interface ContextMenuProvider {
despawn_context_menu();
spawn_context_menu(x: number, y: number, ...entries: MenuEntry[]);
initialize();
finalize();
html_format_enabled() : boolean;
}
let provider: ContextMenuProvider;
export function spawn_context_menu(x: number, y: number, ...entries: MenuEntry[]) {
if(!provider) {
console.error(tr("Failed to spawn context menu! Missing provider!"));
return;
}
provider.spawn_context_menu(x, y, ...entries);
}
export function despawn_context_menu() {
if(!provider)
return;
provider.despawn_context_menu();
}
export function get_provider() : ContextMenuProvider { return provider; }
export function set_provider(_provider: ContextMenuProvider) {
provider = _provider;
provider.initialize();
}
class HTMLContextMenuProvider implements ContextMenuProvider {
private _global_click_listener: (event) => any;
private _context_menu: JQuery;
private _close_callbacks: (() => any)[] = [];
@ -118,10 +116,10 @@ class HTMLContextMenuProvider implements contextmenu.ContextMenuProvider {
}
}
private generate_tag(entry: contextmenu.MenuEntry) : JQuery {
if(entry.type == contextmenu.MenuEntryType.HR) {
private generate_tag(entry: MenuEntry) : JQuery {
if(entry.type == MenuEntryType.HR) {
return $.spawn("hr");
} else if(entry.type == contextmenu.MenuEntryType.ENTRY) {
} else if(entry.type == MenuEntryType.ENTRY) {
let icon = entry.icon_class;
if(!icon || icon.length == 0) icon = "icon_empty";
else icon = "icon " + icon;
@ -141,7 +139,7 @@ class HTMLContextMenuProvider implements contextmenu.ContextMenuProvider {
});
}
return tag;
} else if(entry.type == contextmenu.MenuEntryType.CHECKBOX) {
} else if(entry.type == MenuEntryType.CHECKBOX) {
let checkbox = $.spawn("label").addClass("ccheckbox");
$.spawn("input").attr("type", "checkbox").prop("checked", !!entry.checkbox_checked).appendTo(checkbox);
$.spawn("span").addClass("checkmark").appendTo(checkbox);
@ -161,7 +159,7 @@ class HTMLContextMenuProvider implements contextmenu.ContextMenuProvider {
});
}
return tag;
} else if(entry.type == contextmenu.MenuEntryType.SUB_MENU) {
} else if(entry.type == MenuEntryType.SUB_MENU) {
let icon = entry.icon_class;
if(!icon || icon.length == 0) icon = "icon_empty";
else icon = "icon " + icon;
@ -187,7 +185,7 @@ class HTMLContextMenuProvider implements contextmenu.ContextMenuProvider {
return $.spawn("div").text("undefined");
}
spawn_context_menu(x: number, y: number, ...entries: contextmenu.MenuEntry[]) {
spawn_context_menu(x: number, y: number, ...entries: MenuEntry[]) {
this._visible = true;
let menu_tag = this._context_menu || (this._context_menu = $(".context-menu"));
@ -200,7 +198,7 @@ class HTMLContextMenuProvider implements contextmenu.ContextMenuProvider {
if(typeof(entry.visible) === 'boolean' && !entry.visible)
continue;
if(entry.type == contextmenu.MenuEntryType.CLOSE) {
if(entry.type == MenuEntryType.CLOSE) {
if(entry.callback)
this._close_callbacks.push(entry.callback);
} else
@ -225,5 +223,4 @@ class HTMLContextMenuProvider implements contextmenu.ContextMenuProvider {
return true;
}
}
contextmenu.set_provider(new HTMLContextMenuProvider());
set_provider(new HTMLContextMenuProvider());

View File

@ -1,13 +1,13 @@
/// <reference path="../../PPTListener.ts" />
import {KeyCode} from "tc-shared/PPTListener";
enum ElementType {
export enum ElementType {
HEADER,
BODY,
FOOTER
}
type BodyCreator = (() => JQuery | JQuery[] | string) | string | JQuery | JQuery[];
const ModalFunctions = {
export type BodyCreator = (() => JQuery | JQuery[] | string) | string | JQuery | JQuery[];
export const ModalFunctions = {
divify: function (val: JQuery) {
if(val.length > 1)
return $.spawn("div").append(val);
@ -54,7 +54,7 @@ const ModalFunctions = {
}
};
class ModalProperties {
export class ModalProperties {
template?: string;
header: BodyCreator = () => "HEADER";
body: BodyCreator = () => "BODY";
@ -184,7 +184,7 @@ let _global_modal_count = 0;
let _global_modal_last: HTMLElement;
let _global_modal_last_time: number;
class Modal {
export class Modal {
private _htmlTag: JQuery;
properties: ModalProperties;
shown: boolean;
@ -296,11 +296,11 @@ class Modal {
}
}
function createModal(data: ModalProperties | any) : Modal {
export function createModal(data: ModalProperties | any) : Modal {
return new Modal(ModalFunctions.warpProperties(data));
}
class InputModalProperties extends ModalProperties {
export class InputModalProperties extends ModalProperties {
maxLength?: number;
field_title?: string;
@ -310,7 +310,7 @@ class InputModalProperties extends ModalProperties {
error_message?: string;
}
function createInputModal(headMessage: BodyCreator, question: BodyCreator, validator: (input: string) => boolean, callback: (flag: boolean | string) => void, props: InputModalProperties | any = {}) : Modal {
export 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 @@ function createInputModal(headMessage: BodyCreator, question: BodyCreator, valid
return modal;
}
function createErrorModal(header: BodyCreator, message: BodyCreator, props: ModalProperties | any = { footer: undefined }) {
export 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 @@ function createErrorModal(header: BodyCreator, message: BodyCreator, props: Moda
return modal;
}
function createInfoModal(header: BodyCreator, message: BodyCreator, props: ModalProperties | any = { footer: undefined }) {
export 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";
@ -394,52 +394,6 @@ function createInfoModal(header: BodyCreator, message: BodyCreator, props: Modal
return modal;
}
/* extend jquery */
interface ModalElements {
header?: BodyCreator;
body?: BodyCreator;
footer?: BodyCreator;
}
interface JQuery<TElement = HTMLElement> {
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);
};

View File

@ -0,0 +1,433 @@
export type Entry = {
timestamp: number;
upload?: number;
download?: number;
highlight?: boolean;
}
export type Style = {
background_color: string;
separator_color: string;
separator_count: number;
separator_width: number;
upload: {
fill: string;
stroke: string;
strike_width: number;
},
download: {
fill: string;
stroke: string;
strike_width: number;
}
}
export type TimeSpan = {
origin: {
begin: number;
end: number;
time: number;
},
target: {
begin: number;
end: number;
time: number;
}
}
/* Great explanation of Bezier curves: http://en.wikipedia.org/wiki/Bezier_curve#Quadratic_curves
*
* Assuming A was the last point in the line plotted and B is the new point,
* we draw a curve with control points P and Q as below.
*
* A---P
* |
* |
* |
* Q---B
*
* Importantly, A and P are at the same y coordinate, as are B and Q. This is
* so adjacent curves appear to flow as one.
*/
export class Graph {
private static _loops: (() => any)[] = [];
readonly canvas: HTMLCanvasElement;
public style: Style = {
background_color: "#28292b",
//background_color: "red",
separator_color: "#283036",
//separator_color: 'blue',
separator_count: 10,
separator_width: 1,
upload: {
fill: "#2d3f4d",
stroke: "#336e9f",
strike_width: 2,
},
download: {
fill: "#532c26",
stroke: "#a9321c",
strike_width: 2,
}
};
private _canvas_context: CanvasRenderingContext2D;
private _entries: Entry[] = [];
private _entry_max = {
upload: 1,
download: 1,
};
private _max_space = 1.12;
private _max_gap = 5;
private _listener_mouse_move;
private _listener_mouse_out;
private _animate_loop;
_time_span: TimeSpan = {
origin: {
begin: 0,
end: 1,
time: 0
},
target: {
begin: 0,
end: 1,
time: 1
}
};
private _detailed_shown = false;
callback_detailed_info: (upload: number, download: number, timestamp: number, event: MouseEvent) => any;
callback_detailed_hide: () => any;
constructor(canvas: HTMLCanvasElement) {
this.canvas = canvas;
this._animate_loop = () => this.draw();
this.recalculate_cache(); /* initialize cache */
}
initialize() {
this._canvas_context = this.canvas.getContext("2d");
Graph._loops.push(this._animate_loop);
if(Graph._loops.length == 1) {
const static_loop = () => {
Graph._loops.forEach(l => l());
if(Graph._loops.length > 0)
requestAnimationFrame(static_loop);
else
console.log("STATIC terminate!");
};
static_loop();
}
this.canvas.onmousemove = this.on_mouse_move.bind(this);
this.canvas.onmouseleave = this.on_mouse_leave.bind(this);
}
terminate() {
Graph._loops.remove(this._animate_loop);
}
max_gap_size(value?: number) : number { return typeof(value) === "number" ? (this._max_gap = value) : this._max_gap; }
private recalculate_cache(time_span?: boolean) {
this._entries = this._entries.sort((a, b) => a.timestamp - b.timestamp);
this._entry_max = {
download: 1,
upload: 1
};
if(time_span) {
this._time_span = {
origin: {
begin: 0,
end: 0,
time: 0
},
target: {
begin: this._entries.length > 0 ? this._entries[0].timestamp : 0,
end: this._entries.length > 0 ? this._entries.last().timestamp : 0,
time: 0
}
};
}
for(const entry of this._entries) {
if(typeof(entry.upload) === "number")
this._entry_max.upload = Math.max(this._entry_max.upload, entry.upload);
if(typeof(entry.download) === "number")
this._entry_max.download = Math.max(this._entry_max.download, entry.download);
}
this._entry_max.upload *= this._max_space;
this._entry_max.download *= this._max_space;
}
insert_entry(entry: Entry) {
if(this._entries.length > 0 && entry.timestamp < this._entries.last().timestamp)
throw "invalid timestamp";
this._entries.push(entry);
if(typeof(entry.upload) === "number")
this._entry_max.upload = Math.max(this._entry_max.upload, entry.upload * this._max_space);
if(typeof(entry.download) === "number")
this._entry_max.download = Math.max(this._entry_max.download, entry.download * this._max_space);
}
insert_entries(entries: Entry[]) {
this._entries.push(...entries);
this.recalculate_cache();
this.cleanup();
}
resize() {
this.canvas.style.height = "100%";
this.canvas.style.width = "100%";
const cstyle = getComputedStyle(this.canvas);
this.canvas.width = parseInt(cstyle.width);
this.canvas.height = parseInt(cstyle.height);
}
cleanup() {
const time = this.calculate_time_span();
let index = 0;
for(;index < this._entries.length; index++) {
if(this._entries[index].timestamp < time.begin)
continue;
if(index == 0)
return;
break;
}
/* keep the last entry as a reference point to the left */
if(index > 1) {
this._entries.splice(0, index - 1);
this.recalculate_cache();
}
}
calculate_time_span() : { begin: number; end: number } {
const time = Date.now();
if(time >= this._time_span.target.time)
return this._time_span.target;
if(time <= this._time_span.origin.time)
return this._time_span.origin;
const ob = this._time_span.origin.begin;
const oe = this._time_span.origin.end;
const ot = this._time_span.origin.time;
const tb = this._time_span.target.begin;
const te = this._time_span.target.end;
const tt = this._time_span.target.time;
const offset = (time - ot) / (tt - ot);
return {
begin: ob + (tb - ob) * offset,
end: oe + (te - oe) * offset,
};
}
draw() {
let ctx = this._canvas_context;
const height = this.canvas.height;
const width = this.canvas.width;
//console.log("Painting on %ox%o", height, width);
ctx.shadowBlur = 0;
ctx.filter = "";
ctx.lineCap = "square";
ctx.fillStyle = this.style.background_color;
ctx.fillRect(0, 0, width, height);
/* first of all print the separators */
{
const sw = this.style.separator_width;
const swh = this.style.separator_width / 2;
ctx.lineWidth = sw;
ctx.strokeStyle = this.style.separator_color;
ctx.beginPath();
/* horizontal */
{
const dw = width / this.style.separator_count;
let dx = dw / 2;
while(dx < width) {
ctx.moveTo(Math.floor(dx - swh) + .5, .5);
ctx.lineTo(Math.floor(dx - swh) + .5, Math.floor(height) + .5);
dx += dw;
}
}
/* vertical */
{
const dh = height / 3; //tree lines (top, center, bottom)
let dy = dh / 2;
while(dy < height) {
ctx.moveTo(.5, Math.floor(dy - swh) + .5);
ctx.lineTo(Math.floor(width) + .5, Math.floor(dy - swh) + .5);
dy += dh;
}
}
ctx.stroke();
ctx.closePath();
}
/* draw the lines */
{
const t = this.calculate_time_span();
const tb = t.begin; /* time begin */
const dt = t.end - t.begin; /* delta time */
const dtw = width / dt; /* delta time width */
const draw_graph = (type: "upload" | "download", direction: number, max: number) => {
const hy = Math.floor(height / 2); /* half y */
const by = hy - direction * this.style[type].strike_width; /* the "base" line */
const marked_points: ({x: number, y: number})[] = [];
ctx.beginPath();
ctx.moveTo(0, by);
let x, y, lx = 0, ly = by; /* last x, last y */
const floor = a => a; //Math.floor;
for(const entry of this._entries) {
x = floor((entry.timestamp - tb) * dtw);
if(typeof entry[type] === "number")
y = floor(hy - direction * Math.max(hy * (entry[type] / max), this.style[type].strike_width));
else
y = hy - direction * this.style[type].strike_width;
if(entry.timestamp < tb) {
lx = x;
ly = y;
continue;
}
if(x - lx > this._max_gap && this._max_gap > 0) {
ctx.lineTo(lx, by);
ctx.lineTo(x, by);
ctx.lineTo(x, y);
lx = x;
ly = y;
continue;
}
ctx.bezierCurveTo((x + lx) / 2, ly, (x + lx) / 2, y, x, y);
if(entry.highlight)
marked_points.push({x: x, y: y});
lx = x;
ly = y;
}
ctx.strokeStyle = this.style[type].stroke;
ctx.lineWidth = this.style[type].strike_width;
ctx.lineJoin = "miter";
ctx.stroke();
//Close the path and fill
ctx.lineTo(width, hy);
ctx.lineTo(0, hy);
ctx.fillStyle = this.style[type].fill;
ctx.fill();
ctx.closePath();
{
ctx.beginPath();
const radius = 3;
for(const point of marked_points) {
ctx.moveTo(point.x, point.y);
ctx.ellipse(point.x, point.y, radius, radius, 0, 0, 2 * Math.PI, false);
}
ctx.stroke();
ctx.fill();
ctx.closePath();
}
};
const shared_max = Math.max(this._entry_max.upload, this._entry_max.download);
draw_graph("upload", 1, shared_max);
draw_graph("download", -1, shared_max);
}
}
private on_mouse_move(event: MouseEvent) {
const offset = event.offsetX;
const max_offset = this.canvas.width;
if(offset < 0) return;
if(offset > max_offset) return;
const time_span = this.calculate_time_span();
const time = time_span.begin + (time_span.end - time_span.begin) * (offset / max_offset);
let index = 0;
for(;index < this._entries.length; index++) {
if(this._entries[index].timestamp > time)
break;
}
const entry_before = this._entries[index - 1]; /* In JS negative array access is allowed and returns undefined */
const entry_next = this._entries[index]; /* In JS negative array access is allowed and returns undefined */
let entry: Entry;
if(!entry_before || !entry_next) {
entry = entry_before || entry_next;
} else {
const dn = entry_next.timestamp - time;
const db = time - entry_before.timestamp;
if(dn > db)
entry = entry_before;
else
entry = entry_next;
}
if(!entry) {
this.on_mouse_leave(event);
} else {
this._entries.forEach(e => e.highlight = false);
this._detailed_shown = true;
entry.highlight = true;
if(this.callback_detailed_info)
this.callback_detailed_info(entry.upload, entry.download, entry.timestamp, event);
}
}
private on_mouse_leave(event: MouseEvent) {
if(!this._detailed_shown) return;
this._detailed_shown = false;
this._entries.forEach(e => e.highlight = false);
if(this.callback_detailed_hide)
this.callback_detailed_hide();
}
}

View File

@ -1,4 +1,6 @@
interface SliderOptions {
import * as tooltip from "tc-shared/ui/elements/Tooltip";
export interface SliderOptions {
min_value?: number;
max_value?: number;
initial_value?: number;
@ -8,11 +10,11 @@ interface SliderOptions {
value_field?: JQuery | JQuery[];
}
interface Slider {
export interface Slider {
value(value?: number) : number;
}
function sliderfy(slider: JQuery, options?: SliderOptions) : Slider {
export function sliderfy(slider: JQuery, options?: SliderOptions) : Slider {
options = Object.assign( {
initial_value: 0,
min_value: 0,
@ -30,7 +32,7 @@ function sliderfy(slider: JQuery, options?: SliderOptions) : Slider {
throw "invalid step size";
const tool = tooltip(slider); /* add the tooltip functionality */
const tool = tooltip.initialize(slider); /* add the tooltip functionality */
const filler = slider.find(".filler");
const thumb = slider.find(".thumb");
const tooltip_text = slider.find(".tooltip a");

View File

@ -1,13 +1,12 @@
/// <reference path="../../i18n/localize.ts" />
declare global {
interface JQuery<TElement = HTMLElement> {
asTabWidget(copy?: boolean) : JQuery<TElement>;
tabify(copy?: boolean) : this;
interface JQuery<TElement = HTMLElement> {
asTabWidget(copy?: boolean) : JQuery<TElement>;
tabify(copy?: boolean) : this;
changeElementType(type: string) : JQuery<TElement>;
changeElementType(type: string) : JQuery<TElement>;
}
}
if(typeof (customElements) !== "undefined") {
try {
class X_Tab extends HTMLElement {}
@ -26,7 +25,7 @@ if(typeof (customElements) !== "undefined") {
console.warn(tr("Could not defied tab customElements!"));
}
var TabFunctions = {
export const TabFunctions = {
tabify(template: JQuery, copy: boolean = true) : JQuery {
console.log("Tabify: copy=" + copy);
console.log(template);

View File

@ -0,0 +1,79 @@
let _global_tooltip: JQuery;
export type Handle = {
show();
is_shown();
hide();
update();
}
export function initialize(entry: JQuery, callbacks?: {
on_show?(tag: JQuery),
on_hide?(tag: JQuery)
}) : Handle {
if(!callbacks) callbacks = {};
let _show;
let _hide;
let _shown;
let _update;
entry.find(".container-tooltip").each((index, _node) => {
const node = $(_node) as JQuery;
const node_content = node.find(".tooltip");
let _force_show = false, _flag_shown = false;
const mouseenter = (event?) => {
const bounds = node[0].getBoundingClientRect();
if(!_global_tooltip) {
_global_tooltip = $("#global-tooltip");
}
_global_tooltip[0].style.left = (bounds.left + bounds.width / 2) + "px";
_global_tooltip[0].style.top = bounds.top + "px";
_global_tooltip[0].classList.add("shown");
_global_tooltip[0].innerHTML = node_content[0].innerHTML;
callbacks.on_show && callbacks.on_show(_global_tooltip);
_flag_shown = _flag_shown || !!event; /* if event is undefined then it has been triggered by hand */
};
const mouseexit = () => {
if(_global_tooltip) {
if(!_force_show) {
callbacks.on_hide && callbacks.on_hide(_global_tooltip);
_global_tooltip[0].classList.remove("shown");
}
_flag_shown = false;
}
};
_node.addEventListener("mouseenter", mouseenter);
_node.addEventListener("mouseleave", mouseexit);
_show = () => {
_force_show = true;
mouseenter();
};
_hide = () => {
_force_show = false;
if(!_flag_shown)
mouseexit();
};
_update = () => {
if(_flag_shown || _force_show)
mouseenter();
};
_shown = () => _flag_shown || _force_show;
});
return {
hide: _hide || (() => {}),
show: _show || (() => {}),
is_shown: _shown || (() => false),
update: _update || (() => {})
};
}

View File

@ -1,435 +0,0 @@
namespace net.graph {
export type Entry = {
timestamp: number;
upload?: number;
download?: number;
highlight?: boolean;
}
export type Style = {
background_color: string;
separator_color: string;
separator_count: number;
separator_width: number;
upload: {
fill: string;
stroke: string;
strike_width: number;
},
download: {
fill: string;
stroke: string;
strike_width: number;
}
}
export type TimeSpan = {
origin: {
begin: number;
end: number;
time: number;
},
target: {
begin: number;
end: number;
time: number;
}
}
/* Great explanation of Bezier curves: http://en.wikipedia.org/wiki/Bezier_curve#Quadratic_curves
*
* Assuming A was the last point in the line plotted and B is the new point,
* we draw a curve with control points P and Q as below.
*
* A---P
* |
* |
* |
* Q---B
*
* Importantly, A and P are at the same y coordinate, as are B and Q. This is
* so adjacent curves appear to flow as one.
*/
export class Graph {
private static _loops: (() => any)[] = [];
readonly canvas: HTMLCanvasElement;
public style: Style = {
background_color: "#28292b",
//background_color: "red",
separator_color: "#283036",
//separator_color: 'blue',
separator_count: 10,
separator_width: 1,
upload: {
fill: "#2d3f4d",
stroke: "#336e9f",
strike_width: 2,
},
download: {
fill: "#532c26",
stroke: "#a9321c",
strike_width: 2,
}
};
private _canvas_context: CanvasRenderingContext2D;
private _entries: Entry[] = [];
private _entry_max = {
upload: 1,
download: 1,
};
private _max_space = 1.12;
private _max_gap = 5;
private _listener_mouse_move;
private _listener_mouse_out;
private _animate_loop;
_time_span: TimeSpan = {
origin: {
begin: 0,
end: 1,
time: 0
},
target: {
begin: 0,
end: 1,
time: 1
}
};
private _detailed_shown = false;
callback_detailed_info: (upload: number, download: number, timestamp: number, event: MouseEvent) => any;
callback_detailed_hide: () => any;
constructor(canvas: HTMLCanvasElement) {
this.canvas = canvas;
this._animate_loop = () => this.draw();
this.recalculate_cache(); /* initialize cache */
}
initialize() {
this._canvas_context = this.canvas.getContext("2d");
Graph._loops.push(this._animate_loop);
if(Graph._loops.length == 1) {
const static_loop = () => {
Graph._loops.forEach(l => l());
if(Graph._loops.length > 0)
requestAnimationFrame(static_loop);
else
console.log("STATIC terminate!");
};
static_loop();
}
this.canvas.onmousemove = this.on_mouse_move.bind(this);
this.canvas.onmouseleave = this.on_mouse_leave.bind(this);
}
terminate() {
Graph._loops.remove(this._animate_loop);
}
max_gap_size(value?: number) : number { return typeof(value) === "number" ? (this._max_gap = value) : this._max_gap; }
private recalculate_cache(time_span?: boolean) {
this._entries = this._entries.sort((a, b) => a.timestamp - b.timestamp);
this._entry_max = {
download: 1,
upload: 1
};
if(time_span) {
this._time_span = {
origin: {
begin: 0,
end: 0,
time: 0
},
target: {
begin: this._entries.length > 0 ? this._entries[0].timestamp : 0,
end: this._entries.length > 0 ? this._entries.last().timestamp : 0,
time: 0
}
};
}
for(const entry of this._entries) {
if(typeof(entry.upload) === "number")
this._entry_max.upload = Math.max(this._entry_max.upload, entry.upload);
if(typeof(entry.download) === "number")
this._entry_max.download = Math.max(this._entry_max.download, entry.download);
}
this._entry_max.upload *= this._max_space;
this._entry_max.download *= this._max_space;
}
insert_entry(entry: Entry) {
if(this._entries.length > 0 && entry.timestamp < this._entries.last().timestamp)
throw "invalid timestamp";
this._entries.push(entry);
if(typeof(entry.upload) === "number")
this._entry_max.upload = Math.max(this._entry_max.upload, entry.upload * this._max_space);
if(typeof(entry.download) === "number")
this._entry_max.download = Math.max(this._entry_max.download, entry.download * this._max_space);
}
insert_entries(entries: Entry[]) {
this._entries.push(...entries);
this.recalculate_cache();
this.cleanup();
}
resize() {
this.canvas.style.height = "100%";
this.canvas.style.width = "100%";
const cstyle = getComputedStyle(this.canvas);
this.canvas.width = parseInt(cstyle.width);
this.canvas.height = parseInt(cstyle.height);
}
cleanup() {
const time = this.calculate_time_span();
let index = 0;
for(;index < this._entries.length; index++) {
if(this._entries[index].timestamp < time.begin)
continue;
if(index == 0)
return;
break;
}
/* keep the last entry as a reference point to the left */
if(index > 1) {
this._entries.splice(0, index - 1);
this.recalculate_cache();
}
}
calculate_time_span() : { begin: number; end: number } {
const time = Date.now();
if(time >= this._time_span.target.time)
return this._time_span.target;
if(time <= this._time_span.origin.time)
return this._time_span.origin;
const ob = this._time_span.origin.begin;
const oe = this._time_span.origin.end;
const ot = this._time_span.origin.time;
const tb = this._time_span.target.begin;
const te = this._time_span.target.end;
const tt = this._time_span.target.time;
const offset = (time - ot) / (tt - ot);
return {
begin: ob + (tb - ob) * offset,
end: oe + (te - oe) * offset,
};
}
draw() {
let ctx = this._canvas_context;
const height = this.canvas.height;
const width = this.canvas.width;
//console.log("Painting on %ox%o", height, width);
ctx.shadowBlur = 0;
ctx.filter = "";
ctx.lineCap = "square";
ctx.fillStyle = this.style.background_color;
ctx.fillRect(0, 0, width, height);
/* first of all print the separators */
{
const sw = this.style.separator_width;
const swh = this.style.separator_width / 2;
ctx.lineWidth = sw;
ctx.strokeStyle = this.style.separator_color;
ctx.beginPath();
/* horizontal */
{
const dw = width / this.style.separator_count;
let dx = dw / 2;
while(dx < width) {
ctx.moveTo(Math.floor(dx - swh) + .5, .5);
ctx.lineTo(Math.floor(dx - swh) + .5, Math.floor(height) + .5);
dx += dw;
}
}
/* vertical */
{
const dh = height / 3; //tree lines (top, center, bottom)
let dy = dh / 2;
while(dy < height) {
ctx.moveTo(.5, Math.floor(dy - swh) + .5);
ctx.lineTo(Math.floor(width) + .5, Math.floor(dy - swh) + .5);
dy += dh;
}
}
ctx.stroke();
ctx.closePath();
}
/* draw the lines */
{
const t = this.calculate_time_span();
const tb = t.begin; /* time begin */
const dt = t.end - t.begin; /* delta time */
const dtw = width / dt; /* delta time width */
const draw_graph = (type: "upload" | "download", direction: number, max: number) => {
const hy = Math.floor(height / 2); /* half y */
const by = hy - direction * this.style[type].strike_width; /* the "base" line */
const marked_points: ({x: number, y: number})[] = [];
ctx.beginPath();
ctx.moveTo(0, by);
let x, y, lx = 0, ly = by; /* last x, last y */
const floor = a => a; //Math.floor;
for(const entry of this._entries) {
x = floor((entry.timestamp - tb) * dtw);
if(typeof entry[type] === "number")
y = floor(hy - direction * Math.max(hy * (entry[type] / max), this.style[type].strike_width));
else
y = hy - direction * this.style[type].strike_width;
if(entry.timestamp < tb) {
lx = x;
ly = y;
continue;
}
if(x - lx > this._max_gap && this._max_gap > 0) {
ctx.lineTo(lx, by);
ctx.lineTo(x, by);
ctx.lineTo(x, y);
lx = x;
ly = y;
continue;
}
ctx.bezierCurveTo((x + lx) / 2, ly, (x + lx) / 2, y, x, y);
if(entry.highlight)
marked_points.push({x: x, y: y});
lx = x;
ly = y;
}
ctx.strokeStyle = this.style[type].stroke;
ctx.lineWidth = this.style[type].strike_width;
ctx.lineJoin = "miter";
ctx.stroke();
//Close the path and fill
ctx.lineTo(width, hy);
ctx.lineTo(0, hy);
ctx.fillStyle = this.style[type].fill;
ctx.fill();
ctx.closePath();
{
ctx.beginPath();
const radius = 3;
for(const point of marked_points) {
ctx.moveTo(point.x, point.y);
ctx.ellipse(point.x, point.y, radius, radius, 0, 0, 2 * Math.PI, false);
}
ctx.stroke();
ctx.fill();
ctx.closePath();
}
};
const shared_max = Math.max(this._entry_max.upload, this._entry_max.download);
draw_graph("upload", 1, shared_max);
draw_graph("download", -1, shared_max);
}
}
private on_mouse_move(event: MouseEvent) {
const offset = event.offsetX;
const max_offset = this.canvas.width;
if(offset < 0) return;
if(offset > max_offset) return;
const time_span = this.calculate_time_span();
const time = time_span.begin + (time_span.end - time_span.begin) * (offset / max_offset);
let index = 0;
for(;index < this._entries.length; index++) {
if(this._entries[index].timestamp > time)
break;
}
const entry_before = this._entries[index - 1]; /* In JS negative array access is allowed and returns undefined */
const entry_next = this._entries[index]; /* In JS negative array access is allowed and returns undefined */
let entry: Entry;
if(!entry_before || !entry_next) {
entry = entry_before || entry_next;
} else {
const dn = entry_next.timestamp - time;
const db = time - entry_before.timestamp;
if(dn > db)
entry = entry_before;
else
entry = entry_next;
}
if(!entry) {
this.on_mouse_leave(event);
} else {
this._entries.forEach(e => e.highlight = false);
this._detailed_shown = true;
entry.highlight = true;
if(this.callback_detailed_info)
this.callback_detailed_info(entry.upload, entry.download, entry.timestamp, event);
}
}
private on_mouse_leave(event: MouseEvent) {
if(!this._detailed_shown) return;
this._detailed_shown = false;
this._entries.forEach(e => e.highlight = false);
if(this.callback_detailed_hide)
this.callback_detailed_hide();
}
}
}

View File

@ -1,85 +0,0 @@
function tooltip(entry: JQuery) {
return tooltip.initialize(entry);
}
namespace tooltip {
let _global_tooltip: JQuery;
export type Handle = {
show();
is_shown();
hide();
update();
}
export function initialize(entry: JQuery, callbacks?: {
on_show?(tag: JQuery),
on_hide?(tag: JQuery)
}) : Handle {
if(!callbacks) callbacks = {};
let _show;
let _hide;
let _shown;
let _update;
entry.find(".container-tooltip").each((index, _node) => {
const node = $(_node) as JQuery;
const node_content = node.find(".tooltip");
let _force_show = false, _flag_shown = false;
const mouseenter = (event?) => {
const bounds = node[0].getBoundingClientRect();
if(!_global_tooltip) {
_global_tooltip = $("#global-tooltip");
}
_global_tooltip[0].style.left = (bounds.left + bounds.width / 2) + "px";
_global_tooltip[0].style.top = bounds.top + "px";
_global_tooltip[0].classList.add("shown");
_global_tooltip[0].innerHTML = node_content[0].innerHTML;
callbacks.on_show && callbacks.on_show(_global_tooltip);
_flag_shown = _flag_shown || !!event; /* if event is undefined then it has been triggered by hand */
};
const mouseexit = () => {
if(_global_tooltip) {
if(!_force_show) {
callbacks.on_hide && callbacks.on_hide(_global_tooltip);
_global_tooltip[0].classList.remove("shown");
}
_flag_shown = false;
}
};
_node.addEventListener("mouseenter", mouseenter);
_node.addEventListener("mouseleave", mouseexit);
_show = () => {
_force_show = true;
mouseenter();
};
_hide = () => {
_force_show = false;
if(!_flag_shown)
mouseexit();
};
_update = () => {
if(_flag_shown || _force_show)
mouseenter();
};
_shown = () => _flag_shown || _force_show;
});
return {
hide: _hide || (() => {}),
show: _show || (() => {}),
is_shown: _shown || (() => false),
update: _update || (() => {})
};
}
}

View File

@ -1,24 +1,40 @@
/// <reference path="../../ConnectionHandler.ts" />
/// <reference path="../modal/ModalSettings.ts" />
/// <reference path="../modal/ModalBanList.ts" />
/*
client_output_hardware Value: '1'
client_output_muted Value: '0'
client_outputonly_muted Value: '0'
import {ConnectionHandler, DisconnectReason} from "tc-shared/ConnectionHandler";
import {createErrorModal, createInfoModal, createInputModal} from "tc-shared/ui/elements/Modal";
import {manager, Sound} from "tc-shared/sound/Sounds";
import {default_recorder} from "tc-shared/voice/RecorderProfile";
import {Settings, settings} from "tc-shared/settings";
import {spawnSettingsModal} from "tc-shared/ui/modal/ModalSettings";
import {spawnConnectModal} from "tc-shared/ui/modal/ModalConnect";
import {CommandResult} from "tc-shared/connection/ServerConnectionDeclaration";
import PermissionType from "tc-shared/permission/PermissionType";
import {spawnPermissionEdit} from "tc-shared/ui/modal/permission/ModalPermissionEdit";
import {openBanList} from "tc-shared/ui/modal/ModalBanList";
import {
add_current_server,
Bookmark,
bookmarks,
BookmarkType,
boorkmak_connect,
DirectoryBookmark
} from "tc-shared/bookmarks";
import {IconManager} from "tc-shared/FileManager";
import {spawnBookmarkModal} from "tc-shared/ui/modal/ModalBookmarks";
import {spawnQueryCreate} from "tc-shared/ui/modal/ModalQuery";
import {spawnQueryManage} from "tc-shared/ui/modal/ModalQueryManage";
import {spawnPlaylistManage} from "tc-shared/ui/modal/ModalPlaylistList";
import * as contextmenu from "tc-shared/ui/elements/ContextMenu";
import {server_connections} from "tc-shared/ui/frames/connection_handlers";
import {formatMessage} from "tc-shared/ui/frames/chat";
import * as slog from "tc-shared/ui/frames/server_log";
import * as top_menu from "./MenuBar";
client_input_hardware Value: '1'
client_input_muted Value: '0'
export let control_bar: ControlBar; /* global variable to access the control bar */
export function set_control_bar(bar: ControlBar) { control_bar = bar; }
client_away Value: '0'
client_away_message Value: ''
*/
let control_bar: ControlBar; /* global variable to access the control bar */
type MicrophoneState = "disabled" | "muted" | "enabled";
type HeadphoneState = "muted" | "enabled";
type AwayState = "away-global" | "away" | "online";
class ControlBar {
export type MicrophoneState = "disabled" | "muted" | "enabled";
export type HeadphoneState = "muted" | "enabled";
export type AwayState = "away-global" | "away" | "online";
export class ControlBar {
private _button_away_active: AwayState;
private _button_microphone: MicrophoneState;
private _button_speakers: HeadphoneState;
@ -363,10 +379,10 @@ class ControlBar {
private on_toggle_microphone() {
if(this._button_microphone === "disabled" || this._button_microphone === "muted") {
this.button_microphone = "enabled";
sound.manager.play(Sound.MICROPHONE_ACTIVATED);
manager.play(Sound.MICROPHONE_ACTIVATED);
} else {
this.button_microphone = "muted";
sound.manager.play(Sound.MICROPHONE_MUTED);
manager.play(Sound.MICROPHONE_MUTED);
}
if(this.connection_handler) {
@ -384,10 +400,10 @@ class ControlBar {
private on_toggle_sound() {
if(this._button_speakers === "muted") {
this.button_speaker = "enabled";
sound.manager.play(Sound.SOUND_ACTIVATED);
manager.play(Sound.SOUND_ACTIVATED);
} else {
this.button_speaker = "muted";
sound.manager.play(Sound.SOUND_MUTED);
manager.play(Sound.SOUND_MUTED);
}
if(this.connection_handler) {
@ -421,20 +437,20 @@ class ControlBar {
}
private on_open_settings() {
Modals.spawnSettingsModal();
spawnSettingsModal();
}
private on_open_connect() {
if(this.connection_handler)
this.connection_handler.cancel_reconnect(true);
Modals.spawnConnectModal({}, {
spawnConnectModal({}, {
url: "ts.TeaSpeak.de",
enforce: false
});
}
private on_open_connect_new_tab() {
Modals.spawnConnectModal({
spawnConnectModal({
default_connect_new_tab: true
}, {
url: "ts.TeaSpeak.de",
@ -470,7 +486,7 @@ class ControlBar {
this.connection_handler.handleDisconnect(DisconnectReason.REQUESTED); //TODO message?
this.update_connection_state();
this.connection_handler.sound.play(Sound.CONNECTION_DISCONNECTED);
this.connection_handler.log.log(log.server.Type.DISCONNECTED, {});
this.connection_handler.log.log(slog.Type.DISCONNECTED, {});
}
private on_token_use() {
@ -483,7 +499,7 @@ class ControlBar {
createInfoModal(tr("Use token"), tr("Toke successfully used!")).open();
}).catch(error => {
//TODO tr
createErrorModal(tr("Use token"), MessageHelper.formatMessage(tr("Failed to use token: {}"), error instanceof CommandResult ? error.message : error)).open();
createErrorModal(tr("Use token"), formatMessage(tr("Failed to use token: {}"), error instanceof CommandResult ? error.message : error)).open();
});
}).open();
}
@ -497,7 +513,7 @@ class ControlBar {
button.addClass("activated");
setTimeout(() => {
if(this.connection_handler)
Modals.spawnPermissionEdit(this.connection_handler).open();
spawnPermissionEdit(this.connection_handler).open();
else
createErrorModal(tr("You have to be connected"), tr("You have to be connected!")).open();
button.removeClass("activated");
@ -508,7 +524,7 @@ class ControlBar {
if(!this.connection_handler.serverConnection) return;
if(this.connection_handler.permissions.neededPermission(PermissionType.B_CLIENT_BAN_LIST).granted(1)) {
Modals.openBanList(this.connection_handler);
openBanList(this.connection_handler);
} else {
createErrorModal(tr("You dont have the permission"), tr("You dont have the permission to view the ban list")).open();
this.connection_handler.sound.play(Sound.ERROR_INSUFFICIENT_PERMISSIONS);
@ -516,7 +532,7 @@ class ControlBar {
}
private on_bookmark_server_add() {
bookmarks.add_current_server();
add_current_server();
}
update_bookmark_status() {
@ -530,13 +546,13 @@ class ControlBar {
let tag_bookmark = this.htmlTag.find(".btn_bookmark > .dropdown");
tag_bookmark.find(".bookmark, .directory").remove();
const build_entry = (bookmark: bookmarks.DirectoryBookmark | bookmarks.Bookmark) => {
if(bookmark.type == bookmarks.BookmarkType.ENTRY) {
const mark = <bookmarks.Bookmark>bookmark;
const build_entry = (bookmark: DirectoryBookmark | Bookmark) => {
if(bookmark.type == BookmarkType.ENTRY) {
const mark = <Bookmark>bookmark;
const bookmark_connect = (new_tab: boolean) => {
this.htmlTag.find(".btn_bookmark").find(".dropdown").removeClass("displayed"); //FIXME Not working
bookmarks.boorkmak_connect(mark, new_tab);
boorkmak_connect(mark, new_tab);
};
return $.spawn("div")
@ -580,7 +596,7 @@ class ControlBar {
})
)
} else {
const mark = <bookmarks.DirectoryBookmark>bookmark;
const mark = <DirectoryBookmark>bookmark;
const container = $.spawn("div").addClass("sub-menu dropdown");
const result = $.spawn("div")
@ -609,19 +625,19 @@ class ControlBar {
}
};
for(const bookmark of bookmarks.bookmarks().content) {
for(const bookmark of bookmarks().content) {
const entry = build_entry(bookmark);
tag_bookmark.append(entry);
}
}
private on_bookmark_manage() {
Modals.spawnBookmarkModal();
spawnBookmarkModal();
}
private on_open_query_create() {
if(this.connection_handler.permissions.neededPermission(PermissionType.B_CLIENT_CREATE_MODIFY_SERVERQUERY_LOGIN).granted(1)) {
Modals.spawnQueryCreate(this.connection_handler);
spawnQueryCreate(this.connection_handler);
} else {
createErrorModal(tr("You dont have the permission"), tr("You dont have the permission to create a server query login")).open();
this.connection_handler.sound.play(Sound.ERROR_INSUFFICIENT_PERMISSIONS);
@ -630,7 +646,7 @@ class ControlBar {
private on_open_query_manage() {
if(this.connection_handler && this.connection_handler.connected) {
Modals.spawnQueryManage(this.connection_handler);
spawnQueryManage(this.connection_handler);
} else {
createErrorModal(tr("You have to be connected"), tr("You have to be connected!")).open();
}
@ -638,7 +654,7 @@ class ControlBar {
private on_open_playlist_manage() {
if(this.connection_handler && this.connection_handler.connected) {
Modals.spawnPlaylistManage(this.connection_handler);
spawnPlaylistManage(this.connection_handler);
} else {
createErrorModal(tr("You have to be connected"), tr("You have to be connected to use this function!")).open();
}

File diff suppressed because it is too large Load Diff

View File

@ -1,4 +1,10 @@
enum ChatType {
import {LogCategory} from "tc-shared/log";
import {settings, Settings} from "tc-shared/settings";
import * as log from "tc-shared/log";
import {bbcode} from "tc-shared/MessageFormatter";
import * as loader from "tc-loader";
export enum ChatType {
GENERAL,
SERVER,
CHANNEL,
@ -6,245 +12,243 @@ enum ChatType {
}
declare const xbbcode: any;
namespace MessageHelper {
export function htmlEscape(message: string) : string[] {
const div = document.createElement('div');
div.innerText = message;
message = div.innerHTML;
return message.replace(/ /g, '&nbsp;').split(/<br>/);
}
export function htmlEscape(message: string) : string[] {
const div = document.createElement('div');
div.innerText = message;
message = div.innerHTML;
return message.replace(/ /g, '&nbsp;').split(/<br>/);
}
export function formatElement(object: any, escape_html: boolean = true) : JQuery[] {
if($.isArray(object)) {
let result = [];
for(let element of object)
result.push(...formatElement(element, escape_html));
return result;
} else if(typeof(object) == "string") {
if(object.length == 0) return [];
export function formatElement(object: any, escape_html: boolean = true) : JQuery[] {
if($.isArray(object)) {
let result = [];
for(let element of object)
result.push(...formatElement(element, escape_html));
return result;
} else if(typeof(object) == "string") {
if(object.length == 0) return [];
return escape_html ?
htmlEscape(object).map((entry, idx, array) => $.spawn("a").css("display", (idx == 0 || idx + 1 == array.length ? "inline" : "") + "block").html(entry == "" && idx != 0 ? "&nbsp;" : entry)) :
[$.spawn("div").css("display", "inline-block").html(object)];
} else if(typeof(object) === "object") {
if(object instanceof $)
return [object as any];
return formatElement("<unknwon object>");
} else if(typeof(object) === "function") return formatElement(object(), escape_html);
else if(typeof(object) === "undefined") return formatElement("<undefined>");
else if(typeof(object) === "number") return [$.spawn("a").text(object)];
return formatElement("<unknown object type " + typeof object + ">");
}
return escape_html ?
htmlEscape(object).map((entry, idx, array) => $.spawn("a").css("display", (idx == 0 || idx + 1 == array.length ? "inline" : "") + "block").html(entry == "" && idx != 0 ? "&nbsp;" : entry)) :
[$.spawn("div").css("display", "inline-block").html(object)];
} else if(typeof(object) === "object") {
if(object instanceof $)
return [object as any];
return formatElement("<unknwon object>");
} else if(typeof(object) === "function") return formatElement(object(), escape_html);
else if(typeof(object) === "undefined") return formatElement("<undefined>");
else if(typeof(object) === "number") return [$.spawn("a").text(object)];
return formatElement("<unknown object type " + typeof object + ">");
}
export function formatMessage(pattern: string, ...objects: any[]) : JQuery[] {
let begin = 0, found = 0;
export function formatMessage(pattern: string, ...objects: any[]) : JQuery[] {
let begin = 0, found = 0;
let result: JQuery[] = [];
do {
found = pattern.indexOf('{', found);
if(found == -1 || pattern.length <= found + 1) {
result.push(...formatElement(pattern.substr(begin)));
break;
}
let result: JQuery[] = [];
do {
found = pattern.indexOf('{', found);
if(found == -1 || pattern.length <= found + 1) {
result.push(...formatElement(pattern.substr(begin)));
break;
}
if(found > 0 && pattern[found - 1] == '\\') {
//TODO remove the escape!
if(found > 0 && pattern[found - 1] == '\\') {
//TODO remove the escape!
found++;
continue;
}
result.push(...formatElement(pattern.substr(begin, found - begin))); //Append the text
let offset = 0;
if(pattern[found + 1] == ':') {
offset++; /* the beginning : */
while (pattern[found + 1 + offset] != ':' && found + 1 + offset < pattern.length) offset++;
const tag = pattern.substr(found + 2, offset - 1);
offset++; /* the ending : */
if(pattern[found + offset + 1] != '}' && found + 1 + offset < pattern.length) {
found++;
continue;
}
result.push(...formatElement(pattern.substr(begin, found - begin))); //Append the text
let offset = 0;
if(pattern[found + 1] == ':') {
offset++; /* the beginning : */
while (pattern[found + 1 + offset] != ':' && found + 1 + offset < pattern.length) offset++;
const tag = pattern.substr(found + 2, offset - 1);
offset++; /* the ending : */
if(pattern[found + offset + 1] != '}' && found + 1 + offset < pattern.length) {
found++;
continue;
}
result.push($.spawn(tag as any));
} else {
let number;
while ("0123456789".includes(pattern[found + 1 + offset])) offset++;
number = parseInt(offset > 0 ? pattern.substr(found + 1, offset) : "0");
if(pattern[found + offset + 1] != '}') {
found++;
continue;
}
if(objects.length < number)
log.warn(LogCategory.GENERAL, tr("Message to format contains invalid index (%o)"), number);
result.push(...formatElement(objects[number]));
result.push($.spawn(tag as any));
} else {
let number;
while ("0123456789".includes(pattern[found + 1 + offset])) offset++;
number = parseInt(offset > 0 ? pattern.substr(found + 1, offset) : "0");
if(pattern[found + offset + 1] != '}') {
found++;
continue;
}
found = found + 1 + offset;
begin = found + 1;
} while(found++);
if(objects.length < number)
log.warn(LogCategory.GENERAL, tr("Message to format contains invalid index (%o)"), number);
return result;
}
//TODO: Remove this (only legacy)
export function bbcode_chat(message: string) : JQuery[] {
return messages.formatter.bbcode.format(message, {
is_chat_message: true
});
}
export namespace network {
export const KB = 1024;
export const MB = 1024 * KB;
export const GB = 1024 * MB;
export const TB = 1024 * GB;
export function format_bytes(value: number, options?: {
time?: string,
unit?: string,
exact?: boolean
}) : string {
options = options || {};
if(typeof options.exact !== "boolean")
options.exact = true;
if(typeof options.unit !== "string")
options.unit = "Bytes";
let points = value.toFixed(0).replace(/(\d)(?=(\d{3})+(?!\d))/g, '$1,');
let v, unit;
if(value > 2 * TB) {
unit = "TB";
v = value / TB;
} else if(value > GB) {
unit = "GB";
v = value / GB;
} else if(value > MB) {
unit = "MB";
v = value / MB;
} else if(value > KB) {
unit = "KB";
v = value / KB;
} else {
unit = "";
v = value;
}
let result = "";
if(options.exact || !unit) {
result += points;
if(options.unit) {
result += " " + options.unit;
if(options.time)
result += "/" + options.time;
}
}
if(unit) {
result += (result ? " / " : "") + v.toFixed(2) + " " + unit;
if(options.time)
result += "/" + options.time;
}
return result;
result.push(...formatElement(objects[number]));
}
}
export const K = 1000;
export const M = 1000 * K;
export const G = 1000 * M;
export const T = 1000 * G;
export function format_number(value: number, options?: {
found = found + 1 + offset;
begin = found + 1;
} while(found++);
return result;
}
//TODO: Remove this (only legacy)
export function bbcode_chat(message: string) : JQuery[] {
return bbcode.format(message, {
is_chat_message: true
});
}
export namespace network {
export const KB = 1024;
export const MB = 1024 * KB;
export const GB = 1024 * MB;
export const TB = 1024 * GB;
export function format_bytes(value: number, options?: {
time?: string,
unit?: string
}) {
options = Object.assign(options || {}, {});
unit?: string,
exact?: boolean
}) : string {
options = options || {};
if(typeof options.exact !== "boolean")
options.exact = true;
if(typeof options.unit !== "string")
options.unit = "Bytes";
let points = value.toFixed(0).replace(/(\d)(?=(\d{3})+(?!\d))/g, '$1,');
let v, unit;
if(value > 2 * T) {
unit = "T";
v = value / T;
} else if(value > G) {
unit = "G";
v = value / G;
} else if(value > M) {
unit = "M";
v = value / M;
} else if(value > K) {
unit = "K";
v = value / K;
if(value > 2 * TB) {
unit = "TB";
v = value / TB;
} else if(value > GB) {
unit = "GB";
v = value / GB;
} else if(value > MB) {
unit = "MB";
v = value / MB;
} else if(value > KB) {
unit = "KB";
v = value / KB;
} else {
unit = "";
v = value;
}
if(unit && options.time)
unit = unit + "/" + options.time;
return points + " " + (options.unit || "") + (unit ? (" / " + v.toFixed(2) + " " + unit) : "");
}
export const TIME_SECOND = 1000;
export const TIME_MINUTE = 60 * TIME_SECOND;
export const TIME_HOUR = 60 * TIME_MINUTE;
export const TIME_DAY = 24 * TIME_HOUR;
export const TIME_WEEK = 7 * TIME_DAY;
export function format_time(time: number, default_value: string) {
let result = "";
if(time > TIME_WEEK) {
const amount = Math.floor(time / TIME_WEEK);
result += " " + amount + " " + (amount > 1 ? tr("Weeks") : tr("Week"));
time -= amount * TIME_WEEK;
if(options.exact || !unit) {
result += points;
if(options.unit) {
result += " " + options.unit;
if(options.time)
result += "/" + options.time;
}
}
if(time > TIME_DAY) {
const amount = Math.floor(time / TIME_DAY);
result += " " + amount + " " + (amount > 1 ? tr("Days") : tr("Day"));
time -= amount * TIME_DAY;
if(unit) {
result += (result ? " / " : "") + v.toFixed(2) + " " + unit;
if(options.time)
result += "/" + options.time;
}
return result;
}
}
if(time > TIME_HOUR) {
const amount = Math.floor(time / TIME_HOUR);
result += " " + amount + " " + (amount > 1 ? tr("Hours") : tr("Hour"));
time -= amount * TIME_HOUR;
}
export const K = 1000;
export const M = 1000 * K;
export const G = 1000 * M;
export const T = 1000 * G;
export function format_number(value: number, options?: {
time?: string,
unit?: string
}) {
options = Object.assign(options || {}, {});
if(time > TIME_MINUTE) {
const amount = Math.floor(time / TIME_MINUTE);
result += " " + amount + " " + (amount > 1 ? tr("Minutes") : tr("Minute"));
time -= amount * TIME_MINUTE;
}
let points = value.toFixed(0).replace(/(\d)(?=(\d{3})+(?!\d))/g, '$1,');
if(time > TIME_SECOND) {
const amount = Math.floor(time / TIME_SECOND);
result += " " + amount + " " + (amount > 1 ? tr("Seconds") : tr("Second"));
time -= amount * TIME_SECOND;
}
let v, unit;
if(value > 2 * T) {
unit = "T";
v = value / T;
} else if(value > G) {
unit = "G";
v = value / G;
} else if(value > M) {
unit = "M";
v = value / M;
} else if(value > K) {
unit = "K";
v = value / K;
} else {
unit = "";
v = value;
}
if(unit && options.time)
unit = unit + "/" + options.time;
return points + " " + (options.unit || "") + (unit ? (" / " + v.toFixed(2) + " " + unit) : "");
}
return result.length > 0 ? result.substring(1) : default_value;
export const TIME_SECOND = 1000;
export const TIME_MINUTE = 60 * TIME_SECOND;
export const TIME_HOUR = 60 * TIME_MINUTE;
export const TIME_DAY = 24 * TIME_HOUR;
export const TIME_WEEK = 7 * TIME_DAY;
export function format_time(time: number, default_value: string) {
let result = "";
if(time > TIME_WEEK) {
const amount = Math.floor(time / TIME_WEEK);
result += " " + amount + " " + (amount > 1 ? tr("Weeks") : tr("Week"));
time -= amount * TIME_WEEK;
}
let _icon_size_style: JQuery<HTMLStyleElement>;
export function set_icon_size(size: string) {
if(!_icon_size_style)
_icon_size_style = $.spawn("style").appendTo($("#style"));
_icon_size_style.text("\n" +
".message > .emoji {\n" +
" height: " + size + "!important;\n" +
" width: " + size + "!important;\n" +
"}\n"
);
if(time > TIME_DAY) {
const amount = Math.floor(time / TIME_DAY);
result += " " + amount + " " + (amount > 1 ? tr("Days") : tr("Day"));
time -= amount * TIME_DAY;
}
loader.register_task(loader.Stage.JAVASCRIPT_INITIALIZING, {
name: "icon size init",
function: async () => {
MessageHelper.set_icon_size((settings.static_global(Settings.KEY_ICON_SIZE) / 100).toFixed(2) + "em");
},
priority: 10
});
}
if(time > TIME_HOUR) {
const amount = Math.floor(time / TIME_HOUR);
result += " " + amount + " " + (amount > 1 ? tr("Hours") : tr("Hour"));
time -= amount * TIME_HOUR;
}
if(time > TIME_MINUTE) {
const amount = Math.floor(time / TIME_MINUTE);
result += " " + amount + " " + (amount > 1 ? tr("Minutes") : tr("Minute"));
time -= amount * TIME_MINUTE;
}
if(time > TIME_SECOND) {
const amount = Math.floor(time / TIME_SECOND);
result += " " + amount + " " + (amount > 1 ? tr("Seconds") : tr("Second"));
time -= amount * TIME_SECOND;
}
return result.length > 0 ? result.substring(1) : default_value;
}
let _icon_size_style: JQuery<HTMLStyleElement>;
export function set_icon_size(size: string) {
if(!_icon_size_style)
_icon_size_style = $.spawn("style").appendTo($("#style"));
_icon_size_style.text("\n" +
".message > .emoji {\n" +
" height: " + size + "!important;\n" +
" width: " + size + "!important;\n" +
"}\n"
);
}
loader.register_task(loader.Stage.JAVASCRIPT_INITIALIZING, {
name: "icon size init",
function: async () => {
set_icon_size((settings.static_global(Settings.KEY_ICON_SIZE) / 100).toFixed(2) + "em");
},
priority: 10
});

View File

@ -1,417 +1,426 @@
/* the bar on the right with the chats (Channel & Client) */
namespace chat {
declare function setInterval(handler: TimerHandler, timeout?: number, ...arguments: any[]): number;
declare function setTimeout(handler: TimerHandler, timeout?: number, ...arguments: any[]): number;
import {ClientEntry, MusicClientEntry} from "tc-shared/ui/client";
import {ConnectionHandler} from "tc-shared/ConnectionHandler";
import {ChannelEntry} from "tc-shared/ui/channel";
import {ServerEntry} from "tc-shared/ui/server";
import {openMusicManage} from "tc-shared/ui/modal/ModalMusicManage";
import {formatMessage} from "tc-shared/ui/frames/chat";
import {PrivateConverations} from "tc-shared/ui/frames/side/private_conversations";
import {ClientInfo} from "tc-shared/ui/frames/side/client_info";
import {MusicInfo} from "tc-shared/ui/frames/side/music_info";
import {ConversationManager} from "tc-shared/ui/frames/side/conversations";
export enum InfoFrameMode {
NONE = "none",
CHANNEL_CHAT = "channel_chat",
PRIVATE_CHAT = "private_chat",
CLIENT_INFO = "client_info",
MUSIC_BOT = "music_bot"
declare function setInterval(handler: TimerHandler, timeout?: number, ...arguments: any[]): number;
declare function setTimeout(handler: TimerHandler, timeout?: number, ...arguments: any[]): number;
export enum InfoFrameMode {
NONE = "none",
CHANNEL_CHAT = "channel_chat",
PRIVATE_CHAT = "private_chat",
CLIENT_INFO = "client_info",
MUSIC_BOT = "music_bot"
}
export class InfoFrame {
private readonly handle: Frame;
private _html_tag: JQuery;
private _mode: InfoFrameMode;
private _value_ping: JQuery;
private _ping_updater: number;
private _channel_text: ChannelEntry;
private _channel_voice: ChannelEntry;
private _button_conversation: HTMLElement;
private _button_bot_manage: JQuery;
private _button_song_add: JQuery;
constructor(handle: Frame) {
this.handle = handle;
this._build_html_tag();
this.update_channel_talk();
this.update_channel_text();
this.set_mode(InfoFrameMode.CHANNEL_CHAT);
this._ping_updater = setInterval(() => this.update_ping(), 2000);
this.update_ping();
}
export class InfoFrame {
private readonly handle: Frame;
private _html_tag: JQuery;
private _mode: InfoFrameMode;
private _value_ping: JQuery;
private _ping_updater: number;
html_tag() : JQuery { return this._html_tag; }
destroy() {
clearInterval(this._ping_updater);
private _channel_text: ChannelEntry;
private _channel_voice: ChannelEntry;
this._html_tag && this._html_tag.remove();
this._html_tag = undefined;
this._value_ping = undefined;
}
private _button_conversation: HTMLElement;
private _build_html_tag() {
this._html_tag = $("#tmpl_frame_chat_info").renderTag();
this._html_tag.find(".button-switch-chat-channel").on('click', () => this.handle.show_channel_conversations());
this._value_ping = this._html_tag.find(".value-ping");
this._html_tag.find(".chat-counter").on('click', event => this.handle.show_private_conversations());
this._button_conversation = this._html_tag.find(".button.open-conversation").on('click', event => {
const selected_client = this.handle.client_info().current_client();
if(!selected_client) return;
private _button_bot_manage: JQuery;
private _button_song_add: JQuery;
const conversation = selected_client ? this.handle.private_conversations().find_conversation({
name: selected_client.properties.client_nickname,
unique_id: selected_client.properties.client_unique_identifier,
client_id: selected_client.clientId()
}, { create: true, attach: true }) : undefined;
if(!conversation) return;
constructor(handle: Frame) {
this.handle = handle;
this._build_html_tag();
this.update_channel_talk();
this.update_channel_text();
this.set_mode(InfoFrameMode.CHANNEL_CHAT);
this._ping_updater = setInterval(() => this.update_ping(), 2000);
this.update_ping();
this.handle.private_conversations().set_selected_conversation(conversation);
this.handle.show_private_conversations();
})[0];
this._button_bot_manage = this._html_tag.find(".bot-manage").on('click', event => {
const bot = this.handle.music_info().current_bot();
if(!bot) return;
openMusicManage(this.handle.handle, bot);
});
this._button_song_add = this._html_tag.find(".bot-add-song").on('click', event => {
this.handle.music_info().events.fire("action_song_add");
});
}
update_ping() {
this._value_ping.removeClass("very-good good medium poor very-poor");
const connection = this.handle.handle.serverConnection;
if(!this.handle.handle.connected || !connection) {
this._value_ping.text("Not connected");
return;
}
html_tag() : JQuery { return this._html_tag; }
destroy() {
clearInterval(this._ping_updater);
this._html_tag && this._html_tag.remove();
this._html_tag = undefined;
this._value_ping = undefined;
const ping = connection.ping();
if(!ping || typeof(ping.native) !== "number") {
this._value_ping.text("Not available");
return;
}
private _build_html_tag() {
this._html_tag = $("#tmpl_frame_chat_info").renderTag();
this._html_tag.find(".button-switch-chat-channel").on('click', () => this.handle.show_channel_conversations());
this._value_ping = this._html_tag.find(".value-ping");
this._html_tag.find(".chat-counter").on('click', event => this.handle.show_private_conversations());
this._button_conversation = this._html_tag.find(".button.open-conversation").on('click', event => {
const selected_client = this.handle.client_info().current_client();
if(!selected_client) return;
const conversation = selected_client ? this.handle.private_conversations().find_conversation({
name: selected_client.properties.client_nickname,
unique_id: selected_client.properties.client_unique_identifier,
client_id: selected_client.clientId()
}, { create: true, attach: true }) : undefined;
if(!conversation) return;
this.handle.private_conversations().set_selected_conversation(conversation);
this.handle.show_private_conversations();
})[0];
this._button_bot_manage = this._html_tag.find(".bot-manage").on('click', event => {
const bot = this.handle.music_info().current_bot();
if(!bot) return;
Modals.openMusicManage(this.handle.handle, bot);
});
this._button_song_add = this._html_tag.find(".bot-add-song").on('click', event => {
this.handle.music_info().events.fire("action_song_add");
});
let value;
if(typeof(ping.javascript) !== "undefined") {
value = ping.javascript;
this._value_ping.text(ping.javascript.toFixed(0) + "ms").attr('title', 'Native: ' + ping.native.toFixed(3) + "ms \nJavascript: " + ping.javascript.toFixed(3) + "ms");
} else {
value = ping.native;
this._value_ping.text(ping.native.toFixed(0) + "ms").attr('title', "Ping: " + ping.native.toFixed(3) + "ms");
}
update_ping() {
this._value_ping.removeClass("very-good good medium poor very-poor");
const connection = this.handle.handle.serverConnection;
if(!this.handle.handle.connected || !connection) {
this._value_ping.text("Not connected");
return;
}
if(value <= 10)
this._value_ping.addClass("very-good");
else if(value <= 30)
this._value_ping.addClass("good");
else if(value <= 60)
this._value_ping.addClass("medium");
else if(value <= 150)
this._value_ping.addClass("poor");
else
this._value_ping.addClass("very-poor");
}
const ping = connection.ping();
if(!ping || typeof(ping.native) !== "number") {
this._value_ping.text("Not available");
return;
}
update_channel_talk() {
const client = this.handle.handle.getClient();
const channel = client ? client.currentChannel() : undefined;
this._channel_voice = channel;
let value;
if(typeof(ping.javascript) !== "undefined") {
value = ping.javascript;
this._value_ping.text(ping.javascript.toFixed(0) + "ms").attr('title', 'Native: ' + ping.native.toFixed(3) + "ms \nJavascript: " + ping.javascript.toFixed(3) + "ms");
} else {
value = ping.native;
this._value_ping.text(ping.native.toFixed(0) + "ms").attr('title', "Ping: " + ping.native.toFixed(3) + "ms");
}
const html_tag = this._html_tag.find(".value-voice-channel");
const html_limit_tag = this._html_tag.find(".value-voice-limit");
if(value <= 10)
this._value_ping.addClass("very-good");
else if(value <= 30)
this._value_ping.addClass("good");
else if(value <= 60)
this._value_ping.addClass("medium");
else if(value <= 150)
this._value_ping.addClass("poor");
html_limit_tag.text("");
html_tag.children().remove();
if(channel) {
if(channel.properties.channel_icon_id != 0)
client.handle.fileManager.icons.generateTag(channel.properties.channel_icon_id).appendTo(html_tag);
$.spawn("div").text(channel.formattedChannelName()).appendTo(html_tag);
this.update_channel_limit(channel, html_limit_tag);
} else {
$.spawn("div").text("Not connected").appendTo(html_tag);
}
}
update_channel_text() {
const channel_tree = this.handle.handle.connected ? this.handle.handle.channelTree : undefined;
const current_channel_id = channel_tree ? this.handle.channel_conversations().current_channel() : 0;
const channel = channel_tree ? channel_tree.findChannel(current_channel_id) : undefined;
this._channel_text = channel;
const tag_container = this._html_tag.find(".mode-channel_chat.channel");
const html_tag_title = tag_container.find(".title");
const html_tag = tag_container.find(".value-text-channel");
const html_limit_tag = tag_container.find(".value-text-limit");
/* reset */
html_tag_title.text(tr("You're chatting in Channel"));
html_limit_tag.text("");
html_tag.children().detach();
/* initialize */
if(channel) {
if(channel.properties.channel_icon_id != 0)
this.handle.handle.fileManager.icons.generateTag(channel.properties.channel_icon_id).appendTo(html_tag);
$.spawn("div").text(channel.formattedChannelName()).appendTo(html_tag);
this.update_channel_limit(channel, html_limit_tag);
} else if(channel_tree && current_channel_id > 0) {
html_tag.append(formatMessage(tr("Unknown channel id {}"), current_channel_id));
} else if(channel_tree && current_channel_id == 0) {
const server = this.handle.handle.channelTree.server;
if(server.properties.virtualserver_icon_id != 0)
this.handle.handle.fileManager.icons.generateTag(server.properties.virtualserver_icon_id).appendTo(html_tag);
$.spawn("div").text(server.properties.virtualserver_name).appendTo(html_tag);
html_tag_title.text(tr("You're chatting in Server"));
this.update_server_limit(server, html_limit_tag);
} else if(this.handle.handle.connected) {
$.spawn("div").text("No channel selected").appendTo(html_tag);
} else {
$.spawn("div").text("Not connected").appendTo(html_tag);
}
}
update_channel_client_count(channel: ChannelEntry) {
if(channel === this._channel_text)
this.update_channel_limit(channel, this._html_tag.find(".value-text-limit"));
if(channel === this._channel_voice)
this.update_channel_limit(channel, this._html_tag.find(".value-voice-limit"));
}
private update_channel_limit(channel: ChannelEntry, tag: JQuery) {
let channel_limit = tr("Unlimited");
if(!channel.properties.channel_flag_maxclients_unlimited)
channel_limit = "" + channel.properties.channel_maxclients;
else if(!channel.properties.channel_flag_maxfamilyclients_unlimited) {
if(channel.properties.channel_maxfamilyclients >= 0)
channel_limit = "" + channel.properties.channel_maxfamilyclients;
}
tag.text(channel.clients(false).length + " / " + channel_limit);
}
private update_server_limit(server: ServerEntry, tag: JQuery) {
const fn = () => {
let text = server.properties.virtualserver_clientsonline + " / " + server.properties.virtualserver_maxclients;
if(server.properties.virtualserver_reserved_slots)
text += " (" + server.properties.virtualserver_reserved_slots + " " + tr("Reserved") + ")";
tag.text(text);
};
server.updateProperties().then(fn).catch(error => tag.text(tr("Failed to update info")));
fn();
}
update_chat_counter() {
const conversations = this.handle.private_conversations().conversations();
{
const count = conversations.filter(e => e.is_unread()).length;
const count_container = this._html_tag.find(".container-indicator");
const count_tag = count_container.find(".chat-unread-counter");
count_container.toggle(count > 0);
count_tag.text(count);
}
{
const count_tag = this._html_tag.find(".chat-counter");
if(conversations.length == 0)
count_tag.text(tr("No conversations"));
else if(conversations.length == 1)
count_tag.text(tr("One conversation"));
else
this._value_ping.addClass("very-poor");
}
update_channel_talk() {
const client = this.handle.handle.getClient();
const channel = client ? client.currentChannel() : undefined;
this._channel_voice = channel;
const html_tag = this._html_tag.find(".value-voice-channel");
const html_limit_tag = this._html_tag.find(".value-voice-limit");
html_limit_tag.text("");
html_tag.children().remove();
if(channel) {
if(channel.properties.channel_icon_id != 0)
client.handle.fileManager.icons.generateTag(channel.properties.channel_icon_id).appendTo(html_tag);
$.spawn("div").text(channel.formattedChannelName()).appendTo(html_tag);
this.update_channel_limit(channel, html_limit_tag);
} else {
$.spawn("div").text("Not connected").appendTo(html_tag);
}
}
update_channel_text() {
const channel_tree = this.handle.handle.connected ? this.handle.handle.channelTree : undefined;
const current_channel_id = channel_tree ? this.handle.channel_conversations().current_channel() : 0;
const channel = channel_tree ? channel_tree.findChannel(current_channel_id) : undefined;
this._channel_text = channel;
const tag_container = this._html_tag.find(".mode-channel_chat.channel");
const html_tag_title = tag_container.find(".title");
const html_tag = tag_container.find(".value-text-channel");
const html_limit_tag = tag_container.find(".value-text-limit");
/* reset */
html_tag_title.text(tr("You're chatting in Channel"));
html_limit_tag.text("");
html_tag.children().detach();
/* initialize */
if(channel) {
if(channel.properties.channel_icon_id != 0)
this.handle.handle.fileManager.icons.generateTag(channel.properties.channel_icon_id).appendTo(html_tag);
$.spawn("div").text(channel.formattedChannelName()).appendTo(html_tag);
this.update_channel_limit(channel, html_limit_tag);
} else if(channel_tree && current_channel_id > 0) {
html_tag.append(MessageHelper.formatMessage(tr("Unknown channel id {}"), current_channel_id));
} else if(channel_tree && current_channel_id == 0) {
const server = this.handle.handle.channelTree.server;
if(server.properties.virtualserver_icon_id != 0)
this.handle.handle.fileManager.icons.generateTag(server.properties.virtualserver_icon_id).appendTo(html_tag);
$.spawn("div").text(server.properties.virtualserver_name).appendTo(html_tag);
html_tag_title.text(tr("You're chatting in Server"));
this.update_server_limit(server, html_limit_tag);
} else if(this.handle.handle.connected) {
$.spawn("div").text("No channel selected").appendTo(html_tag);
} else {
$.spawn("div").text("Not connected").appendTo(html_tag);
}
}
update_channel_client_count(channel: ChannelEntry) {
if(channel === this._channel_text)
this.update_channel_limit(channel, this._html_tag.find(".value-text-limit"));
if(channel === this._channel_voice)
this.update_channel_limit(channel, this._html_tag.find(".value-voice-limit"));
}
private update_channel_limit(channel: ChannelEntry, tag: JQuery) {
let channel_limit = tr("Unlimited");
if(!channel.properties.channel_flag_maxclients_unlimited)
channel_limit = "" + channel.properties.channel_maxclients;
else if(!channel.properties.channel_flag_maxfamilyclients_unlimited) {
if(channel.properties.channel_maxfamilyclients >= 0)
channel_limit = "" + channel.properties.channel_maxfamilyclients;
}
tag.text(channel.clients(false).length + " / " + channel_limit);
}
private update_server_limit(server: ServerEntry, tag: JQuery) {
const fn = () => {
let text = server.properties.virtualserver_clientsonline + " / " + server.properties.virtualserver_maxclients;
if(server.properties.virtualserver_reserved_slots)
text += " (" + server.properties.virtualserver_reserved_slots + " " + tr("Reserved") + ")";
tag.text(text);
};
server.updateProperties().then(fn).catch(error => tag.text(tr("Failed to update info")));
fn();
}
update_chat_counter() {
const conversations = this.handle.private_conversations().conversations();
{
const count = conversations.filter(e => e.is_unread()).length;
const count_container = this._html_tag.find(".container-indicator");
const count_tag = count_container.find(".chat-unread-counter");
count_container.toggle(count > 0);
count_tag.text(count);
}
{
const count_tag = this._html_tag.find(".chat-counter");
if(conversations.length == 0)
count_tag.text(tr("No conversations"));
else if(conversations.length == 1)
count_tag.text(tr("One conversation"));
else
count_tag.text(conversations.length + " " + tr("conversations"));
}
}
current_mode() : InfoFrameMode {
return this._mode;
}
set_mode(mode: InfoFrameMode) {
for(const mode in InfoFrameMode)
this._html_tag.removeClass("mode-" + InfoFrameMode[mode]);
this._html_tag.addClass("mode-" + mode);
if(mode === InfoFrameMode.CLIENT_INFO && this._button_conversation) {
//Will be called every time a client is shown
const selected_client = this.handle.client_info().current_client();
const conversation = selected_client ? this.handle.private_conversations().find_conversation({
name: selected_client.properties.client_nickname,
unique_id: selected_client.properties.client_unique_identifier,
client_id: selected_client.clientId()
}, { create: false, attach: false }) : undefined;
const visibility = (selected_client && selected_client.clientId() !== this.handle.handle.clientId) ? "visible" : "hidden";
if(this._button_conversation.style.visibility !== visibility)
this._button_conversation.style.visibility = visibility;
if(conversation) {
this._button_conversation.innerText = tr("Open conversation");
} else {
this._button_conversation.innerText = tr("Start a conversation");
}
} else if(mode === InfoFrameMode.MUSIC_BOT) {
//TODO?
}
count_tag.text(conversations.length + " " + tr("conversations"));
}
}
export enum FrameContent {
NONE,
PRIVATE_CHAT,
CHANNEL_CHAT,
CLIENT_INFO,
MUSIC_BOT
current_mode() : InfoFrameMode {
return this._mode;
}
export class Frame {
readonly handle: ConnectionHandler;
private _info_frame: InfoFrame;
private _html_tag: JQuery;
private _container_info: JQuery;
private _container_chat: JQuery;
private _content_type: FrameContent;
set_mode(mode: InfoFrameMode) {
for(const mode in InfoFrameMode)
this._html_tag.removeClass("mode-" + InfoFrameMode[mode]);
this._html_tag.addClass("mode-" + mode);
private _conversations: PrivateConverations;
private _client_info: ClientInfo;
private _music_info: MusicInfo;
private _channel_conversations: channel.ConversationManager;
if(mode === InfoFrameMode.CLIENT_INFO && this._button_conversation) {
//Will be called every time a client is shown
const selected_client = this.handle.client_info().current_client();
const conversation = selected_client ? this.handle.private_conversations().find_conversation({
name: selected_client.properties.client_nickname,
unique_id: selected_client.properties.client_unique_identifier,
client_id: selected_client.clientId()
}, { create: false, attach: false }) : undefined;
constructor(handle: ConnectionHandler) {
this.handle = handle;
const visibility = (selected_client && selected_client.clientId() !== this.handle.handle.clientId) ? "visible" : "hidden";
if(this._button_conversation.style.visibility !== visibility)
this._button_conversation.style.visibility = visibility;
if(conversation) {
this._button_conversation.innerText = tr("Open conversation");
} else {
this._button_conversation.innerText = tr("Start a conversation");
}
} else if(mode === InfoFrameMode.MUSIC_BOT) {
//TODO?
}
}
}
this._content_type = FrameContent.NONE;
this._info_frame = new InfoFrame(this);
this._conversations = new PrivateConverations(this);
this._channel_conversations = new channel.ConversationManager(this);
this._client_info = new ClientInfo(this);
this._music_info = new MusicInfo(this);
export enum FrameContent {
NONE,
PRIVATE_CHAT,
CHANNEL_CHAT,
CLIENT_INFO,
MUSIC_BOT
}
this._build_html_tag();
export class Frame {
readonly handle: ConnectionHandler;
private _info_frame: InfoFrame;
private _html_tag: JQuery;
private _container_info: JQuery;
private _container_chat: JQuery;
private _content_type: FrameContent;
private _conversations: PrivateConverations;
private _client_info: ClientInfo;
private _music_info: MusicInfo;
private _channel_conversations: ConversationManager;
constructor(handle: ConnectionHandler) {
this.handle = handle;
this._content_type = FrameContent.NONE;
this._info_frame = new InfoFrame(this);
this._conversations = new PrivateConverations(this);
this._channel_conversations = new ConversationManager(this);
this._client_info = new ClientInfo(this);
this._music_info = new MusicInfo(this);
this._build_html_tag();
this.show_channel_conversations();
this.info_frame().update_chat_counter();
}
html_tag() : JQuery { return this._html_tag; }
info_frame() : InfoFrame { return this._info_frame; }
content_type() : FrameContent { return this._content_type; }
destroy() {
this._html_tag && this._html_tag.remove();
this._html_tag = undefined;
this._info_frame && this._info_frame.destroy();
this._info_frame = undefined;
this._conversations && this._conversations.destroy();
this._conversations = undefined;
this._client_info && this._client_info.destroy();
this._client_info = undefined;
this._music_info && this._music_info.destroy();
this._music_info = undefined;
this._channel_conversations && this._channel_conversations.destroy();
this._channel_conversations = undefined;
this._container_info && this._container_info.remove();
this._container_info = undefined;
this._container_chat && this._container_chat.remove();
this._container_chat = undefined;
}
private _build_html_tag() {
this._html_tag = $("#tmpl_frame_chat").renderTag();
this._container_info = this._html_tag.find(".container-info");
this._container_chat = this._html_tag.find(".container-chat");
this._info_frame.html_tag().appendTo(this._container_info);
}
private_conversations() : PrivateConverations {
return this._conversations;
}
channel_conversations() : ConversationManager {
return this._channel_conversations;
}
client_info() : ClientInfo {
return this._client_info;
}
music_info() : MusicInfo {
return this._music_info;
}
private _clear() {
this._content_type = FrameContent.NONE;
this._container_chat.children().detach();
}
show_private_conversations() {
if(this._content_type === FrameContent.PRIVATE_CHAT)
return;
this._clear();
this._content_type = FrameContent.PRIVATE_CHAT;
this._container_chat.append(this._conversations.html_tag());
this._conversations.on_show();
this._info_frame.set_mode(InfoFrameMode.PRIVATE_CHAT);
}
show_channel_conversations() {
if(this._content_type === FrameContent.CHANNEL_CHAT)
return;
this._clear();
this._content_type = FrameContent.CHANNEL_CHAT;
this._container_chat.append(this._channel_conversations.html_tag());
this._channel_conversations.on_show();
this._info_frame.set_mode(InfoFrameMode.CHANNEL_CHAT);
}
show_client_info(client: ClientEntry) {
this._client_info.set_current_client(client);
this._info_frame.set_mode(InfoFrameMode.CLIENT_INFO); /* specially needs an update here to update the conversation button */
if(this._content_type === FrameContent.CLIENT_INFO)
return;
this._client_info.previous_frame_content = this._content_type;
this._clear();
this._content_type = FrameContent.CLIENT_INFO;
this._container_chat.append(this._client_info.html_tag());
}
show_music_player(client: MusicClientEntry) {
this._music_info.set_current_bot(client);
if(this._content_type === FrameContent.MUSIC_BOT)
return;
this._info_frame.set_mode(InfoFrameMode.MUSIC_BOT);
this._music_info.previous_frame_content = this._content_type;
this._clear();
this._content_type = FrameContent.MUSIC_BOT;
this._container_chat.append(this._music_info.html_tag());
}
set_content(type: FrameContent) {
if(this._content_type === type)
return;
if(type === FrameContent.CHANNEL_CHAT)
this.show_channel_conversations();
this.info_frame().update_chat_counter();
}
html_tag() : JQuery { return this._html_tag; }
info_frame() : InfoFrame { return this._info_frame; }
content_type() : FrameContent { return this._content_type; }
destroy() {
this._html_tag && this._html_tag.remove();
this._html_tag = undefined;
this._info_frame && this._info_frame.destroy();
this._info_frame = undefined;
this._conversations && this._conversations.destroy();
this._conversations = undefined;
this._client_info && this._client_info.destroy();
this._client_info = undefined;
this._music_info && this._music_info.destroy();
this._music_info = undefined;
this._channel_conversations && this._channel_conversations.destroy();
this._channel_conversations = undefined;
this._container_info && this._container_info.remove();
this._container_info = undefined;
this._container_chat && this._container_chat.remove();
this._container_chat = undefined;
}
private _build_html_tag() {
this._html_tag = $("#tmpl_frame_chat").renderTag();
this._container_info = this._html_tag.find(".container-info");
this._container_chat = this._html_tag.find(".container-chat");
this._info_frame.html_tag().appendTo(this._container_info);
}
private_conversations() : PrivateConverations {
return this._conversations;
}
channel_conversations() : channel.ConversationManager {
return this._channel_conversations;
}
client_info() : ClientInfo {
return this._client_info;
}
music_info() : MusicInfo {
return this._music_info;
}
private _clear() {
else if(type === FrameContent.PRIVATE_CHAT)
this.show_private_conversations();
else {
this._clear();
this._content_type = FrameContent.NONE;
this._container_chat.children().detach();
}
show_private_conversations() {
if(this._content_type === FrameContent.PRIVATE_CHAT)
return;
this._clear();
this._content_type = FrameContent.PRIVATE_CHAT;
this._container_chat.append(this._conversations.html_tag());
this._conversations.on_show();
this._info_frame.set_mode(InfoFrameMode.PRIVATE_CHAT);
}
show_channel_conversations() {
if(this._content_type === FrameContent.CHANNEL_CHAT)
return;
this._clear();
this._content_type = FrameContent.CHANNEL_CHAT;
this._container_chat.append(this._channel_conversations.html_tag());
this._channel_conversations.on_show();
this._info_frame.set_mode(InfoFrameMode.CHANNEL_CHAT);
}
show_client_info(client: ClientEntry) {
this._client_info.set_current_client(client);
this._info_frame.set_mode(InfoFrameMode.CLIENT_INFO); /* specially needs an update here to update the conversation button */
if(this._content_type === FrameContent.CLIENT_INFO)
return;
this._client_info.previous_frame_content = this._content_type;
this._clear();
this._content_type = FrameContent.CLIENT_INFO;
this._container_chat.append(this._client_info.html_tag());
}
show_music_player(client: MusicClientEntry) {
this._music_info.set_current_bot(client);
if(this._content_type === FrameContent.MUSIC_BOT)
return;
this._info_frame.set_mode(InfoFrameMode.MUSIC_BOT);
this._music_info.previous_frame_content = this._content_type;
this._clear();
this._content_type = FrameContent.MUSIC_BOT;
this._container_chat.append(this._music_info.html_tag());
}
set_content(type: FrameContent) {
if(this._content_type === type)
return;
if(type === FrameContent.CHANNEL_CHAT)
this.show_channel_conversations();
else if(type === FrameContent.PRIVATE_CHAT)
this.show_private_conversations();
else {
this._clear();
this._content_type = FrameContent.NONE;
this._info_frame.set_mode(InfoFrameMode.NONE);
}
this._info_frame.set_mode(InfoFrameMode.NONE);
}
}
}

View File

@ -1,7 +1,13 @@
import {ConnectionHandler, DisconnectReason} from "tc-shared/ConnectionHandler";
import {Settings, settings} from "tc-shared/settings";
import {control_bar} from "tc-shared/ui/frames/ControlBar";
import * as top_menu from "./MenuBar";
let server_connections: ServerConnectionManager;
class ServerConnectionManager {
export let server_connections: ServerConnectionManager;
export function initialize(manager: ServerConnectionManager) {
server_connections = manager;
}
export class ServerConnectionManager {
private connection_handlers: ConnectionHandler[] = [];
private active_handler: ConnectionHandler | undefined;

View File

@ -1,4 +1,9 @@
class Hostbanner {
import {ConnectionHandler} from "tc-shared/ConnectionHandler";
import {settings, Settings} from "tc-shared/settings";
import {LogCategory} from "tc-shared/log";
import * as log from "tc-shared/log";
export class Hostbanner {
readonly html_tag: JQuery<HTMLElement>;
readonly client: ConnectionHandler;

View File

@ -1,81 +1,81 @@
namespace image_preview {
let preview_overlay: JQuery<HTMLDivElement>;
let container_image: JQuery<HTMLDivElement>;
let button_open_in_browser: JQuery;
import * as loader from "tc-loader";
export function preview_image(url: string, original_url: string) {
if(!preview_overlay) return;
let preview_overlay: JQuery<HTMLDivElement>;
let container_image: JQuery<HTMLDivElement>;
let button_open_in_browser: JQuery;
container_image.empty();
$.spawn("img").attr({
"src": url,
"title": original_url,
"x-original-src": original_url
}).appendTo(container_image);
export function preview_image(url: string, original_url: string) {
if(!preview_overlay) return;
preview_overlay.removeClass("hidden");
button_open_in_browser.show();
container_image.empty();
$.spawn("img").attr({
"src": url,
"title": original_url,
"x-original-src": original_url
}).appendTo(container_image);
preview_overlay.removeClass("hidden");
button_open_in_browser.show();
}
export function preview_image_tag(tag: JQuery) {
if(!preview_overlay) return;
container_image.empty();
container_image.append(tag);
preview_overlay.removeClass("hidden");
button_open_in_browser.hide();
}
export function current_url() {
const image_tag = container_image.find("img");
return image_tag.attr("x-original-src") || image_tag.attr("src") || "";
}
export function close_preview() {
preview_overlay.addClass("hidden");
}
loader.register_task(loader.Stage.LOADED, {
priority: 0,
name: "image preview init",
function: async () => {
preview_overlay = $("#overlay-image-preview");
container_image = preview_overlay.find(".container-image") as any;
preview_overlay.find("img").on('click', event => event.preventDefault());
preview_overlay.on('click', event => {
if(event.isDefaultPrevented()) return;
close_preview();
});
preview_overlay.find(".button-close").on('click', event => {
event.preventDefault();
close_preview();
});
preview_overlay.find(".button-download").on('click', event => {
event.preventDefault();
const link = document.createElement('a');
link.href = current_url();
link.target = "_blank";
const findex = link.href.lastIndexOf("/") + 1;
link.download = link.href.substr(findex);
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
});
button_open_in_browser = preview_overlay.find(".button-open-in-window");
button_open_in_browser.on('click', event => {
event.preventDefault();
const win = window.open(current_url(), '_blank');
win.focus();
});
}
export function preview_image_tag(tag: JQuery) {
if(!preview_overlay) return;
container_image.empty();
container_image.append(tag);
preview_overlay.removeClass("hidden");
button_open_in_browser.hide();
}
export function current_url() {
const image_tag = container_image.find("img");
return image_tag.attr("x-original-src") || image_tag.attr("src") || "";
}
export function close_preview() {
preview_overlay.addClass("hidden");
}
loader.register_task(loader.Stage.LOADED, {
priority: 0,
name: "image preview init",
function: async () => {
preview_overlay = $("#overlay-image-preview");
container_image = preview_overlay.find(".container-image") as any;
preview_overlay.find("img").on('click', event => event.preventDefault());
preview_overlay.on('click', event => {
if(event.isDefaultPrevented()) return;
close_preview();
});
preview_overlay.find(".button-close").on('click', event => {
event.preventDefault();
close_preview();
});
preview_overlay.find(".button-download").on('click', event => {
event.preventDefault();
const link = document.createElement('a');
link.href = current_url();
link.target = "_blank";
const findex = link.href.lastIndexOf("/") + 1;
link.download = link.href.substr(findex);
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
});
button_open_in_browser = preview_overlay.find(".button-open-in-window");
button_open_in_browser.on('click', event => {
event.preventDefault();
const win = window.open(current_url(), '_blank');
win.focus();
});
}
});
}
});

File diff suppressed because it is too large Load Diff

View File

@ -1,267 +1,268 @@
namespace chat {
declare function setInterval(handler: TimerHandler, timeout?: number, ...arguments: any[]): number;
declare function setTimeout(handler: TimerHandler, timeout?: number, ...arguments: any[]): number;
import {Settings, settings} from "tc-shared/settings";
import {helpers} from "tc-shared/ui/frames/side/chat_helper";
export class ChatBox {
private _html_tag: JQuery;
private _html_input: JQuery<HTMLDivElement>;
private _enabled: boolean;
private __callback_text_changed;
private __callback_key_down;
private __callback_key_up;
private __callback_paste;
declare function setInterval(handler: TimerHandler, timeout?: number, ...arguments: any[]): number;
declare function setTimeout(handler: TimerHandler, timeout?: number, ...arguments: any[]): number;
private _typing_timeout: number; /* ID when the next callback_typing will be called */
private _typing_last_event: number; /* timestamp of the last typing event */
export class ChatBox {
private _html_tag: JQuery;
private _html_input: JQuery<HTMLDivElement>;
private _enabled: boolean;
private __callback_text_changed;
private __callback_key_down;
private __callback_key_up;
private __callback_paste;
private _message_history: string[] = [];
private _message_history_length = 100;
private _message_history_index = 0;
private _typing_timeout: number; /* ID when the next callback_typing will be called */
private _typing_last_event: number; /* timestamp of the last typing event */
typing_interval: number = 2000; /* update frequency */
callback_typing: () => any;
callback_text: (text: string) => any;
private _message_history: string[] = [];
private _message_history_length = 100;
private _message_history_index = 0;
constructor() {
this._enabled = true;
this.__callback_key_up = this._callback_key_up.bind(this);
this.__callback_key_down = this._callback_key_down.bind(this);
this.__callback_text_changed = this._callback_text_changed.bind(this);
this.__callback_paste = event => this._callback_paste(event);
typing_interval: number = 2000; /* update frequency */
callback_typing: () => any;
callback_text: (text: string) => any;
this._build_html_tag();
this._initialize_listener();
}
constructor() {
this._enabled = true;
this.__callback_key_up = this._callback_key_up.bind(this);
this.__callback_key_down = this._callback_key_down.bind(this);
this.__callback_text_changed = this._callback_text_changed.bind(this);
this.__callback_paste = event => this._callback_paste(event);
html_tag() : JQuery {
return this._html_tag;
}
this._build_html_tag();
this._initialize_listener();
}
destroy() {
this._html_tag && this._html_tag.remove();
this._html_tag = undefined;
this._html_input = undefined;
html_tag() : JQuery {
return this._html_tag;
}
clearTimeout(this._typing_timeout);
destroy() {
this._html_tag && this._html_tag.remove();
this._html_tag = undefined;
this._html_input = undefined;
this.__callback_text_changed = undefined;
this.__callback_key_down = undefined;
this.__callback_paste = undefined;
clearTimeout(this._typing_timeout);
this.callback_text = undefined;
this.callback_typing = undefined;
}
this.__callback_text_changed = undefined;
this.__callback_key_down = undefined;
this.__callback_paste = undefined;
private _initialize_listener() {
this._html_input.on("cut paste drop keydown keyup", (event) => this.__callback_text_changed(event));
this._html_input.on("change", this.__callback_text_changed);
this._html_input.on("keydown", this.__callback_key_down);
this._html_input.on("keyup", this.__callback_key_up);
this._html_input.on("paste", this.__callback_paste);
}
this.callback_text = undefined;
this.callback_typing = undefined;
}
private _build_html_tag() {
this._html_tag = $("#tmpl_frame_chat_chatbox").renderTag({
emojy_support: settings.static_global(Settings.KEY_CHAT_COLORED_EMOJIES)
});
this._html_input = this._html_tag.find(".textarea") as any;
private _initialize_listener() {
this._html_input.on("cut paste drop keydown keyup", (event) => this.__callback_text_changed(event));
this._html_input.on("change", this.__callback_text_changed);
this._html_input.on("keydown", this.__callback_key_down);
this._html_input.on("keyup", this.__callback_key_up);
this._html_input.on("paste", this.__callback_paste);
}
const tag: JQuery & { lsxEmojiPicker(args: any); } = this._html_tag.find('.button-emoji') as any;
tag.lsxEmojiPicker({
width: 300,
height: 400,
twemoji: typeof(window.twemoji) !== "undefined",
onSelect: emoji => this._html_input.html(this._html_input.html() + emoji.value),
closeOnSelect: false
});
}
private _build_html_tag() {
this._html_tag = $("#tmpl_frame_chat_chatbox").renderTag({
emojy_support: settings.static_global(Settings.KEY_CHAT_COLORED_EMOJIES)
});
this._html_input = this._html_tag.find(".textarea") as any;
private _callback_text_changed(event: Event) {
if(event && event.defaultPrevented)
const tag: JQuery & { lsxEmojiPicker(args: any); } = this._html_tag.find('.button-emoji') as any;
tag.lsxEmojiPicker({
width: 300,
height: 400,
twemoji: typeof(window.twemoji) !== "undefined",
onSelect: emoji => this._html_input.html(this._html_input.html() + emoji.value),
closeOnSelect: false
});
}
private _callback_text_changed(event: Event) {
if(event && event.defaultPrevented)
return;
/* Auto resize */
const text = this._html_input[0];
text.style.height = "1em";
text.style.height = text.scrollHeight + 'px';
if(!event || (event.type !== "keydown" && event.type !== "keyup" && event.type !== "change"))
return;
this._typing_last_event = Date.now();
if(this._typing_timeout)
return;
const _trigger_typing = (last_time: number) => {
if(this._typing_last_event <= last_time) {
this._typing_timeout = 0;
return;
/* Auto resize */
const text = this._html_input[0];
text.style.height = "1em";
text.style.height = text.scrollHeight + 'px';
if(!event || (event.type !== "keydown" && event.type !== "keyup" && event.type !== "change"))
return;
this._typing_last_event = Date.now();
if(this._typing_timeout)
return;
const _trigger_typing = (last_time: number) => {
if(this._typing_last_event <= last_time) {
this._typing_timeout = 0;
return;
}
try {
if(this.callback_typing)
this.callback_typing();
} finally {
this._typing_timeout = setTimeout(_trigger_typing, this.typing_interval, this._typing_last_event);
}
};
_trigger_typing(0); /* We def want that*/
}
private _text(element: HTMLElement) {
if(typeof(element) !== "object")
return element;
if(element instanceof HTMLImageElement)
return element.alt || element.title;
if(element instanceof HTMLBRElement) {
return '\n';
}
if(element.childNodes.length > 0)
return [...element.childNodes].map(e => this._text(e as HTMLElement)).join("");
try {
if(this.callback_typing)
this.callback_typing();
} finally {
this._typing_timeout = setTimeout(_trigger_typing, this.typing_interval, this._typing_last_event);
}
};
_trigger_typing(0); /* We def want that*/
}
if(element.nodeType == Node.TEXT_NODE)
return element.textContent;
return typeof(element.innerText) === "string" ? element.innerText : "";
private _text(element: HTMLElement) {
if(typeof(element) !== "object")
return element;
if(element instanceof HTMLImageElement)
return element.alt || element.title;
if(element instanceof HTMLBRElement) {
return '\n';
}
private htmlEscape(message: string) : string {
const div = document.createElement('div');
div.innerText = message;
message = div.innerHTML;
return message.replace(/ /g, '&nbsp;');
}
private _callback_paste(event: ClipboardEvent) {
const _event = (<any>event).originalEvent as ClipboardEvent || event;
const clipboard = _event.clipboardData || (<any>window).clipboardData;
if(!clipboard) return;
if(element.childNodes.length > 0)
return [...element.childNodes].map(e => this._text(e as HTMLElement)).join("");
if(element.nodeType == Node.TEXT_NODE)
return element.textContent;
return typeof(element.innerText) === "string" ? element.innerText : "";
}
private htmlEscape(message: string) : string {
const div = document.createElement('div');
div.innerText = message;
message = div.innerHTML;
return message.replace(/ /g, '&nbsp;');
}
private _callback_paste(event: ClipboardEvent) {
const _event = (<any>event).originalEvent as ClipboardEvent || event;
const clipboard = _event.clipboardData || (<any>window).clipboardData;
if(!clipboard) return;
const raw_text = clipboard.getData('text/plain');
const selection = window.getSelection();
if (!selection.rangeCount)
return false;
const raw_text = clipboard.getData('text/plain');
const selection = window.getSelection();
if (!selection.rangeCount)
return false;
let html_xml = clipboard.getData('text/html');
if(!html_xml)
html_xml = $.spawn("div").text(raw_text).html();
let html_xml = clipboard.getData('text/html');
if(!html_xml)
html_xml = $.spawn("div").text(raw_text).html();
const parser = new DOMParser();
const nodes = parser.parseFromString(html_xml, "text/html");
const parser = new DOMParser();
const nodes = parser.parseFromString(html_xml, "text/html");
let data = this._text(nodes.body);
let data = this._text(nodes.body);
/* fix prefix & suffix new lines */
/* fix prefix & suffix new lines */
{
let prefix_length = 0, suffix_length = 0;
{
let prefix_length = 0, suffix_length = 0;
{
for(let i = 0; i < raw_text.length; i++)
if(raw_text.charAt(i) === '\n')
prefix_length++;
else if(raw_text.charAt(i) !== '\r')
break;
for(let i = raw_text.length - 1; i >= 0; i++)
if(raw_text.charAt(i) === '\n')
suffix_length++;
else if(raw_text.charAt(i) !== '\r')
break;
}
data = data.replace(/^[\n\r]+|[\n\r]+$/g, '');
data = "\n".repeat(prefix_length) + data + "\n".repeat(suffix_length);
for(let i = 0; i < raw_text.length; i++)
if(raw_text.charAt(i) === '\n')
prefix_length++;
else if(raw_text.charAt(i) !== '\r')
break;
for(let i = raw_text.length - 1; i >= 0; i++)
if(raw_text.charAt(i) === '\n')
suffix_length++;
else if(raw_text.charAt(i) !== '\r')
break;
}
data = data.replace(/^[\n\r]+|[\n\r]+$/g, '');
data = "\n".repeat(prefix_length) + data + "\n".repeat(suffix_length);
}
event.preventDefault();
selection.deleteFromDocument();
document.execCommand('insertHTML', false, this.htmlEscape(data));
}
private test_message(message: string) : boolean {
message = message
.replace(/ /gi, "")
.replace(/<br>/gi, "")
.replace(/\n/gi, "")
.replace(/<br\/>/gi, "");
return message.length > 0;
}
private _callback_key_down(event: KeyboardEvent) {
if(event.key.toLowerCase() === "enter" && !event.shiftKey) {
event.preventDefault();
selection.deleteFromDocument();
document.execCommand('insertHTML', false, this.htmlEscape(data));
}
private test_message(message: string) : boolean {
message = message
.replace(/ /gi, "")
.replace(/<br>/gi, "")
.replace(/\n/gi, "")
.replace(/<br\/>/gi, "");
return message.length > 0;
}
private _callback_key_down(event: KeyboardEvent) {
if(event.key.toLowerCase() === "enter" && !event.shiftKey) {
event.preventDefault();
/* deactivate chatbox when no callback? */
let text = this._html_input[0].innerText as string;
if(!this.test_message(text))
return;
this._message_history.push(text);
this._message_history_index = this._message_history.length;
if(this._message_history.length > this._message_history_length)
this._message_history = this._message_history.slice(this._message_history.length - this._message_history_length);
if(this.callback_text) {
this.callback_text(helpers.preprocess_chat_message(text));
}
if(this._typing_timeout)
clearTimeout(this._typing_timeout);
this._typing_timeout = 1; /* enforce no typing update while sending */
this._html_input.text("");
setTimeout(() => {
this.__callback_text_changed();
this._typing_timeout = 0; /* enable text change listener again */
});
} else if(event.key.toLowerCase() === "arrowdown") {
//TODO: Test for at the last line within the box
if(this._message_history_index < 0) return;
if(this._message_history_index >= this._message_history.length) return; /* OOB, even with the empty message */
this._message_history_index++;
this._html_input[0].innerText = this._message_history[this._message_history_index] || ""; /* OOB just returns "undefined" */
} else if(event.key.toLowerCase() === "arrowup") {
//TODO: Test for at the first line within the box
if(this._message_history_index <= 0) return; /* we cant go "down" */
this._message_history_index--;
this._html_input[0].innerText = this._message_history[this._message_history_index];
} else {
if(this._message_history_index >= 0) {
if(this._message_history_index >= this._message_history.length) {
if("" !== this._html_input[0].innerText)
this._message_history_index = -1;
} else if(this._message_history[this._message_history_index] !== this._html_input[0].innerText)
this._message_history_index = -1;
}
}
}
private _callback_key_up(event: KeyboardEvent) {
if("" === this._html_input[0].innerText)
this._message_history_index = this._message_history.length;
}
private _context_task: number;
set_enabled(flag: boolean) {
if(this._enabled === flag)
/* deactivate chatbox when no callback? */
let text = this._html_input[0].innerText as string;
if(!this.test_message(text))
return;
if(!this._context_task) {
this._enabled = flag;
/* Allow the browser to asynchronously recalculate everything */
this._context_task = setTimeout(() => {
this._context_task = undefined;
this._html_input.each((_, e) => { e.contentEditable = this._enabled ? "true" : "false"; });
});
this._html_tag.find('.button-emoji').toggleClass("disabled", !flag);
this._message_history.push(text);
this._message_history_index = this._message_history.length;
if(this._message_history.length > this._message_history_length)
this._message_history = this._message_history.slice(this._message_history.length - this._message_history_length);
if(this.callback_text) {
this.callback_text(helpers.preprocess_chat_message(text));
}
if(this._typing_timeout)
clearTimeout(this._typing_timeout);
this._typing_timeout = 1; /* enforce no typing update while sending */
this._html_input.text("");
setTimeout(() => {
this.__callback_text_changed();
this._typing_timeout = 0; /* enable text change listener again */
});
} else if(event.key.toLowerCase() === "arrowdown") {
//TODO: Test for at the last line within the box
if(this._message_history_index < 0) return;
if(this._message_history_index >= this._message_history.length) return; /* OOB, even with the empty message */
this._message_history_index++;
this._html_input[0].innerText = this._message_history[this._message_history_index] || ""; /* OOB just returns "undefined" */
} else if(event.key.toLowerCase() === "arrowup") {
//TODO: Test for at the first line within the box
if(this._message_history_index <= 0) return; /* we cant go "down" */
this._message_history_index--;
this._html_input[0].innerText = this._message_history[this._message_history_index];
} else {
if(this._message_history_index >= 0) {
if(this._message_history_index >= this._message_history.length) {
if("" !== this._html_input[0].innerText)
this._message_history_index = -1;
} else if(this._message_history[this._message_history_index] !== this._html_input[0].innerText)
this._message_history_index = -1;
}
}
}
is_enabled() {
return this._enabled;
}
private _callback_key_up(event: KeyboardEvent) {
if("" === this._html_input[0].innerText)
this._message_history_index = this._message_history.length;
}
focus_input() {
this._html_input.focus();
private _context_task: number;
set_enabled(flag: boolean) {
if(this._enabled === flag)
return;
if(!this._context_task) {
this._enabled = flag;
/* Allow the browser to asynchronously recalculate everything */
this._context_task = setTimeout(() => {
this._context_task = undefined;
this._html_input.each((_, e) => { e.contentEditable = this._enabled ? "true" : "false"; });
});
this._html_tag.find('.button-emoji').toggleClass("disabled", !flag);
}
}
is_enabled() {
return this._enabled;
}
focus_input() {
this._html_input.focus();
}
}

View File

@ -1,423 +1,426 @@
namespace chat {
export namespace helpers {
//https://regex101.com/r/YQbfcX/2
//static readonly URL_REGEX = /^(?<hostname>([a-zA-Z0-9-]+\.)+[a-zA-Z0-9-]{2,63})(?:\/(?<path>(?:[^\s?]+)?)(?:\?(?<query>\S+))?)?$/gm;
const URL_REGEX = /^(([a-zA-Z0-9-]+\.)+[a-zA-Z0-9-]{2,63})(?:\/((?:[^\s?]+)?)(?:\?(\S+))?)?$/gm;
function process_urls(message: string) : string {
const words = message.split(/[ \n]/);
for(let index = 0; index < words.length; index++) {
const flag_escaped = words[index].startsWith('!');
const unescaped = flag_escaped ? words[index].substr(1) : words[index];
import * as log from "tc-shared/log";
import {LogCategory} from "tc-shared/log";
import {Settings, settings} from "tc-shared/settings";
_try:
try {
const url = new URL(unescaped);
log.debug(LogCategory.GENERAL, tr("Chat message contains URL: %o"), url);
if(url.protocol !== 'http:' && url.protocol !== 'https:')
break _try;
if(flag_escaped) {
message = undefined;
words[index] = unescaped;
} else {
message = undefined;
words[index] = "[url=" + url.toString() + "]" + url.toString() + "[/url]";
}
} catch(e) { /* word isn't an url */ }
declare const xbbcode;
export namespace helpers {
//https://regex101.com/r/YQbfcX/2
//static readonly URL_REGEX = /^(?<hostname>([a-zA-Z0-9-]+\.)+[a-zA-Z0-9-]{2,63})(?:\/(?<path>(?:[^\s?]+)?)(?:\?(?<query>\S+))?)?$/gm;
const URL_REGEX = /^(([a-zA-Z0-9-]+\.)+[a-zA-Z0-9-]{2,63})(?:\/((?:[^\s?]+)?)(?:\?(\S+))?)?$/gm;
function process_urls(message: string) : string {
const words = message.split(/[ \n]/);
for(let index = 0; index < words.length; index++) {
const flag_escaped = words[index].startsWith('!');
const unescaped = flag_escaped ? words[index].substr(1) : words[index];
if(unescaped.match(URL_REGEX)) {
_try:
try {
const url = new URL(unescaped);
log.debug(LogCategory.GENERAL, tr("Chat message contains URL: %o"), url);
if(url.protocol !== 'http:' && url.protocol !== 'https:')
break _try;
if(flag_escaped) {
message = undefined;
words[index] = unescaped;
} else {
message = undefined;
words[index] = "[url=" + unescaped + "]" + unescaped + "[/url]";
words[index] = "[url=" + url.toString() + "]" + url.toString() + "[/url]";
}
} catch(e) { /* word isn't an url */ }
if(unescaped.match(URL_REGEX)) {
if(flag_escaped) {
message = undefined;
words[index] = unescaped;
} else {
message = undefined;
words[index] = "[url=" + unescaped + "]" + unescaped + "[/url]";
}
}
return message || words.join(" ");
}
namespace md2bbc {
export type RemarkToken = {
type: string;
tight: boolean;
lines: number[];
level: number;
return message || words.join(" ");
}
/* img */
alt?: string;
src?: string;
namespace md2bbc {
export type RemarkToken = {
type: string;
tight: boolean;
lines: number[];
level: number;
/* link */
href?: string;
/* img */
alt?: string;
src?: string;
/* table */
align?: string;
/* link */
href?: string;
/* code */
params?: string;
/* table */
align?: string;
content?: string;
hLevel?: number;
children?: RemarkToken[];
}
/* code */
params?: string;
export class Renderer {
private static renderers = {
"text": (renderer: Renderer, token: RemarkToken) => renderer.options().process_url ? process_urls(renderer.maybe_escape_bb(token.content)) : renderer.maybe_escape_bb(token.content),
"softbreak": () => "\n",
"hardbreak": () => "\n",
content?: string;
hLevel?: number;
children?: RemarkToken[];
}
"paragraph_open": (renderer: Renderer, token: RemarkToken) => {
const last_line = !renderer.last_paragraph || !renderer.last_paragraph.lines ? 0 : renderer.last_paragraph.lines[1];
const lines = token.lines[0] - last_line;
return [...new Array(lines)].map(e => "[br]").join("");
},
"paragraph_close": () => "",
export class Renderer {
private static renderers = {
"text": (renderer: Renderer, token: RemarkToken) => renderer.options().process_url ? process_urls(renderer.maybe_escape_bb(token.content)) : renderer.maybe_escape_bb(token.content),
"softbreak": () => "\n",
"hardbreak": () => "\n",
"strong_open": (renderer: Renderer, token: RemarkToken) => "[b]",
"strong_close": (renderer: Renderer, token: RemarkToken) => "[/b]",
"paragraph_open": (renderer: Renderer, token: RemarkToken) => {
const last_line = !renderer.last_paragraph || !renderer.last_paragraph.lines ? 0 : renderer.last_paragraph.lines[1];
const lines = token.lines[0] - last_line;
return [...new Array(lines)].map(e => "[br]").join("");
},
"paragraph_close": () => "",
"em_open": (renderer: Renderer, token: RemarkToken) => "[i]",
"em_close": (renderer: Renderer, token: RemarkToken) => "[/i]",
"strong_open": (renderer: Renderer, token: RemarkToken) => "[b]",
"strong_close": (renderer: Renderer, token: RemarkToken) => "[/b]",
"del_open": () => "[s]",
"del_close": () => "[/s]",
"em_open": (renderer: Renderer, token: RemarkToken) => "[i]",
"em_close": (renderer: Renderer, token: RemarkToken) => "[/i]",
"sup": (renderer: Renderer, token: RemarkToken) => "[sup]" + renderer.maybe_escape_bb(token.content) + "[/sup]",
"sub": (renderer: Renderer, token: RemarkToken) => "[sub]" + renderer.maybe_escape_bb(token.content) + "[/sub]",
"del_open": () => "[s]",
"del_close": () => "[/s]",
"bullet_list_open": () => "[ul]",
"bullet_list_close": () => "[/ul]",
"sup": (renderer: Renderer, token: RemarkToken) => "[sup]" + renderer.maybe_escape_bb(token.content) + "[/sup]",
"sub": (renderer: Renderer, token: RemarkToken) => "[sub]" + renderer.maybe_escape_bb(token.content) + "[/sub]",
"ordered_list_open": () => "[ol]",
"ordered_list_close": () => "[/ol]",
"bullet_list_open": () => "[ul]",
"bullet_list_close": () => "[/ul]",
"list_item_open": () => "[li]",
"list_item_close": () => "[/li]",
"ordered_list_open": () => "[ol]",
"ordered_list_close": () => "[/ol]",
"table_open": () => "[table]",
"table_close": () => "[/table]",
"list_item_open": () => "[li]",
"list_item_close": () => "[/li]",
"thead_open": () => "",
"thead_close": () => "",
"table_open": () => "[table]",
"table_close": () => "[/table]",
"tbody_open": () => "",
"tbody_close": () => "",
"thead_open": () => "",
"thead_close": () => "",
"tr_open": () => "[tr]",
"tr_close": () => "[/tr]",
"tbody_open": () => "",
"tbody_close": () => "",
"th_open": (renderer: Renderer, token: RemarkToken) => "[th" + (token.align ? ("=" + token.align) : "") + "]",
"th_close": () => "[/th]",
"tr_open": () => "[tr]",
"tr_close": () => "[/tr]",
"td_open": () => "[td]",
"td_close": () => "[/td]",
"th_open": (renderer: Renderer, token: RemarkToken) => "[th" + (token.align ? ("=" + token.align) : "") + "]",
"th_close": () => "[/th]",
"link_open": (renderer: Renderer, token: RemarkToken) => "[url" + (token.href ? ("=" + token.href) : "") + "]",
"link_close": () => "[/url]",
"td_open": () => "[td]",
"td_close": () => "[/td]",
"image": (renderer: Renderer, token: RemarkToken) => "[img=" + (token.src) + "]" + (token.alt || token.src) + "[/img]",
"link_open": (renderer: Renderer, token: RemarkToken) => "[url" + (token.href ? ("=" + token.href) : "") + "]",
"link_close": () => "[/url]",
//footnote_ref
"image": (renderer: Renderer, token: RemarkToken) => "[img=" + (token.src) + "]" + (token.alt || token.src) + "[/img]",
//"content": "==Marked text==",
//mark_open
//mark_close
//footnote_ref
//++Inserted text++
"ins_open": () => "[u]",
"ins_close": () => "[/u]",
//"content": "==Marked text==",
//mark_open
//mark_close
/*
//++Inserted text++
"ins_open": () => "[u]",
"ins_close": () => "[/u]",
/*
```
test
[/code]
test
```
*/
*/
"code": (renderer: Renderer, token: RemarkToken) => "[i-code]" + xbbcode.escape(token.content) + "[/i-code]",
"fence": (renderer: Renderer, token: RemarkToken) => "[code" + (token.params ? ("=" + token.params) : "") + "]" + xbbcode.escape(token.content) + "[/code]",
"code": (renderer: Renderer, token: RemarkToken) => "[i-code]" + xbbcode.escape(token.content) + "[/i-code]",
"fence": (renderer: Renderer, token: RemarkToken) => "[code" + (token.params ? ("=" + token.params) : "") + "]" + xbbcode.escape(token.content) + "[/code]",
"heading_open": (renderer: Renderer, token: RemarkToken) => "[size=" + (9 - Math.min(4, token.hLevel)) + "]",
"heading_close": (renderer: Renderer, token: RemarkToken) => "[/size][hr]",
"heading_open": (renderer: Renderer, token: RemarkToken) => "[size=" + (9 - Math.min(4, token.hLevel)) + "]",
"heading_close": (renderer: Renderer, token: RemarkToken) => "[/size][hr]",
"hr": () => "[hr]",
"hr": () => "[hr]",
//> Experience real-time editing with Remarkable!
//blockquote_open,
//blockquote_close
};
//> Experience real-time editing with Remarkable!
//blockquote_open,
//blockquote_close
};
private _options;
last_paragraph: RemarkToken;
private _options;
last_paragraph: RemarkToken;
render(tokens: RemarkToken[], options: any, env: any) {
this.last_paragraph = undefined;
this._options = options;
let result = '';
render(tokens: RemarkToken[], options: any, env: any) {
this.last_paragraph = undefined;
this._options = options;
let result = '';
//TODO: Escape BB-Codes
for(let index = 0; index < tokens.length; index++) {
if (tokens[index].type === 'inline') {
result += this.render_inline(tokens[index].children, index);
} else {
result += this.render_token(tokens[index], index);
}
}
this._options = undefined;
return result;
}
private render_token(token: RemarkToken, index: number) {
log.debug(LogCategory.GENERAL, tr("Render Markdown token: %o"), token);
const renderer = Renderer.renderers[token.type];
if(typeof(renderer) === "undefined") {
log.warn(LogCategory.CHAT, tr("Missing markdown to bbcode renderer for token %s: %o"), token.type, token);
return token.content || "";
}
const result = renderer(this, token, index);
if(token.type === "paragraph_open") this.last_paragraph = token;
return result;
}
private render_inline(tokens: RemarkToken[], index: number) {
let result = '';
for(let index = 0; index < tokens.length; index++) {
//TODO: Escape BB-Codes
for(let index = 0; index < tokens.length; index++) {
if (tokens[index].type === 'inline') {
result += this.render_inline(tokens[index].children, index);
} else {
result += this.render_token(tokens[index], index);
}
return result;
}
options() : any {
return this._options;
this._options = undefined;
return result;
}
private render_token(token: RemarkToken, index: number) {
log.debug(LogCategory.GENERAL, tr("Render Markdown token: %o"), token);
const renderer = Renderer.renderers[token.type];
if(typeof(renderer) === "undefined") {
log.warn(LogCategory.CHAT, tr("Missing markdown to bbcode renderer for token %s: %o"), token.type, token);
return token.content || "";
}
maybe_escape_bb(text: string) {
if(this._options.escape_bb)
return xbbcode.escape(text);
return text;
}
}
}
let _renderer: any;
function process_markdown(message: string, options: {
process_url?: boolean,
escape_bb?: boolean
}) : string {
if(typeof(window.remarkable) === "undefined")
return (options.process_url ? process_urls(message) : message);
if(!_renderer) {
_renderer = new window.remarkable.Remarkable('full');
_renderer.set({
typographer: true
});
_renderer.renderer = new md2bbc.Renderer();
_renderer.inline.ruler.disable([ 'newline', 'autolink' ]);
}
_renderer.set({
process_url: !!options.process_url,
escape_bb: !!options.escape_bb
});
let result: string = _renderer.render(message);
if(result.endsWith("\n"))
result = result.substr(0, result.length - 1);
return result;
}
export function preprocess_chat_message(message: string) : string {
const process_url = settings.static_global(Settings.KEY_CHAT_TAG_URLS);
const parse_markdown = settings.static_global(Settings.KEY_CHAT_ENABLE_MARKDOWN);
const escape_bb = !settings.static_global(Settings.KEY_CHAT_ENABLE_BBCODE);
if(parse_markdown)
return process_markdown(message, {
process_url: process_url,
escape_bb: escape_bb
});
if(escape_bb)
message = xbbcode.escape(message);
return process_url ? process_urls(message) : message;
}
export namespace history {
let _local_cache: Cache;
async function get_cache() {
if(_local_cache)
return _local_cache;
if(!('caches' in window))
throw "missing cache extension!";
return (_local_cache = await caches.open('chat_history'));
const result = renderer(this, token, index);
if(token.type === "paragraph_open") this.last_paragraph = token;
return result;
}
export async function load_history(key: string) : Promise<any | undefined> {
const cache = await get_cache();
const request = new Request("https://_local_cache/cache_request_" + key);
const cached_response = await cache.match(request);
if(!cached_response)
return undefined;
private render_inline(tokens: RemarkToken[], index: number) {
let result = '';
return await cached_response.json();
}
export async function save_history(key: string, value: any) {
const cache = await get_cache();
const request = new Request("https://_local_cache/cache_request_" + key);
const data = JSON.stringify(value);
const new_headers = new Headers();
new_headers.set("Content-type", "application/json");
new_headers.set("Content-length", data.length.toString());
await cache.put(request, new Response(data, {
headers: new_headers
}));
}
}
export namespace date {
export function same_day(a: number | Date, b: number | Date) {
a = a instanceof Date ? a : new Date(a);
b = b instanceof Date ? b : new Date(b);
if(a.getDate() !== b.getDate())
return false;
if(a.getMonth() !== b.getMonth())
return false;
return a.getFullYear() === b.getFullYear();
}
}
}
export namespace format {
export namespace date {
export enum ColloquialFormat {
YESTERDAY,
TODAY,
GENERAL
}
export function date_format(date: Date, now: Date, ignore_settings?: boolean) : ColloquialFormat {
if(!ignore_settings && !settings.static_global(Settings.KEY_CHAT_COLLOQUIAL_TIMESTAMPS))
return ColloquialFormat.GENERAL;
let delta_day = now.getDate() - date.getDate();
if(delta_day < 1) /* month change? */
delta_day = date.getDate() - now.getDate();
if(delta_day == 0)
return ColloquialFormat.TODAY;
else if(delta_day == 1)
return ColloquialFormat.YESTERDAY;
return ColloquialFormat.GENERAL;
}
export function format_date_general(date: Date, hours?: boolean) : string {
return ('00' + date.getDate()).substr(-2) + "."
+ ('00' + date.getMonth()).substr(-2) + "."
+ date.getFullYear() +
(typeof(hours) === "undefined" || hours ? " at "
+ ('00' + date.getHours()).substr(-2) + ":"
+ ('00' + date.getMinutes()).substr(-2)
: "");
}
export function format_date_colloquial(date: Date, current_timestamp: Date) : { result: string; format: ColloquialFormat } {
const format = date_format(date, current_timestamp);
if(format == ColloquialFormat.GENERAL) {
return {
result: format_date_general(date),
format: format
};
} else {
let hrs = date.getHours();
let time = "AM";
if(hrs > 12) {
hrs -= 12;
time = "PM";
}
return {
result: (format == ColloquialFormat.YESTERDAY ? tr("Yesterday at") : tr("Today at")) + " " + hrs + ":" + date.getMinutes() + " " + time,
format: format
};
}
}
export function format_chat_time(date: Date) : {
result: string,
next_update: number /* in MS */
} {
const timestamp = date.getTime();
const current_timestamp = new Date();
const result = {
result: "",
next_update: 0
};
if(settings.static_global(Settings.KEY_CHAT_FIXED_TIMESTAMPS)) {
const format = format_date_colloquial(date, current_timestamp);
result.result = format.result;
result.next_update = 0; /* TODO: Update on day change? */
} else {
const delta = current_timestamp.getTime() - timestamp;
if(delta < 2000) {
result.result = "now";
result.next_update = 2500 - delta; /* update after two seconds */
} else if(delta < 30000) { /* 30 seconds */
result.result = Math.floor(delta / 1000) + " " + tr("seconds ago");
result.next_update = 1000; /* update every second */
} else if(delta < 30 * 60 * 1000) { /* 30 minutes */
if(delta < 120 * 1000)
result.result = tr("one minute ago");
else
result.result = Math.floor(delta / (1000 * 60)) + " " + tr("minutes ago");
result.next_update = 60000; /* updater after a minute */
} else {
result.result = format_date_colloquial(date, current_timestamp).result;
result.next_update = 0; /* TODO: Update on day change? */
}
for(let index = 0; index < tokens.length; index++) {
result += this.render_token(tokens[index], index);
}
return result;
}
}
export namespace time {
export function format_online_time(secs: number) : string {
let years = Math.floor(secs / (60 * 60 * 24 * 365));
let days = Math.floor(secs / (60 * 60 * 24)) % 365;
let hours = Math.floor(secs / (60 * 60)) % 24;
let minutes = Math.floor(secs / 60) % 60;
let seconds = Math.floor(secs % 60);
let result = "";
if(years > 0)
result += years + " " + tr("years") + " ";
if(years > 0 || days > 0)
result += days + " " + tr("days") + " ";
if(years > 0 || days > 0 || hours > 0)
result += hours + " " + tr("hours") + " ";
if(years > 0 || days > 0 || hours > 0 || minutes > 0)
result += minutes + " " + tr("minutes") + " ";
if(years > 0 || days > 0 || hours > 0 || minutes > 0 || seconds > 0)
result += seconds + " " + tr("seconds") + " ";
else
result = tr("now") + " ";
options() : any {
return this._options;
}
return result.substr(0, result.length - 1);
maybe_escape_bb(text: string) {
if(this._options.escape_bb)
return xbbcode.escape(text);
return text;
}
}
}
let _renderer: any;
function process_markdown(message: string, options: {
process_url?: boolean,
escape_bb?: boolean
}) : string {
if(typeof(window.remarkable) === "undefined")
return (options.process_url ? process_urls(message) : message);
if(!_renderer) {
_renderer = new window.remarkable.Remarkable('full');
_renderer.set({
typographer: true
});
_renderer.renderer = new md2bbc.Renderer();
_renderer.inline.ruler.disable([ 'newline', 'autolink' ]);
}
_renderer.set({
process_url: !!options.process_url,
escape_bb: !!options.escape_bb
});
let result: string = _renderer.render(message);
if(result.endsWith("\n"))
result = result.substr(0, result.length - 1);
return result;
}
export function preprocess_chat_message(message: string) : string {
const process_url = settings.static_global(Settings.KEY_CHAT_TAG_URLS);
const parse_markdown = settings.static_global(Settings.KEY_CHAT_ENABLE_MARKDOWN);
const escape_bb = !settings.static_global(Settings.KEY_CHAT_ENABLE_BBCODE);
if(parse_markdown)
return process_markdown(message, {
process_url: process_url,
escape_bb: escape_bb
});
if(escape_bb)
message = xbbcode.escape(message);
return process_url ? process_urls(message) : message;
}
export namespace history {
let _local_cache: Cache;
async function get_cache() {
if(_local_cache)
return _local_cache;
if(!('caches' in window))
throw "missing cache extension!";
return (_local_cache = await caches.open('chat_history'));
}
export async function load_history(key: string) : Promise<any | undefined> {
const cache = await get_cache();
const request = new Request("https://_local_cache/cache_request_" + key);
const cached_response = await cache.match(request);
if(!cached_response)
return undefined;
return await cached_response.json();
}
export async function save_history(key: string, value: any) {
const cache = await get_cache();
const request = new Request("https://_local_cache/cache_request_" + key);
const data = JSON.stringify(value);
const new_headers = new Headers();
new_headers.set("Content-type", "application/json");
new_headers.set("Content-length", data.length.toString());
await cache.put(request, new Response(data, {
headers: new_headers
}));
}
}
export namespace date {
export function same_day(a: number | Date, b: number | Date) {
a = a instanceof Date ? a : new Date(a);
b = b instanceof Date ? b : new Date(b);
if(a.getDate() !== b.getDate())
return false;
if(a.getMonth() !== b.getMonth())
return false;
return a.getFullYear() === b.getFullYear();
}
}
}
export namespace format {
export namespace date {
export enum ColloquialFormat {
YESTERDAY,
TODAY,
GENERAL
}
export function date_format(date: Date, now: Date, ignore_settings?: boolean) : ColloquialFormat {
if(!ignore_settings && !settings.static_global(Settings.KEY_CHAT_COLLOQUIAL_TIMESTAMPS))
return ColloquialFormat.GENERAL;
let delta_day = now.getDate() - date.getDate();
if(delta_day < 1) /* month change? */
delta_day = date.getDate() - now.getDate();
if(delta_day == 0)
return ColloquialFormat.TODAY;
else if(delta_day == 1)
return ColloquialFormat.YESTERDAY;
return ColloquialFormat.GENERAL;
}
export function format_date_general(date: Date, hours?: boolean) : string {
return ('00' + date.getDate()).substr(-2) + "."
+ ('00' + date.getMonth()).substr(-2) + "."
+ date.getFullYear() +
(typeof(hours) === "undefined" || hours ? " at "
+ ('00' + date.getHours()).substr(-2) + ":"
+ ('00' + date.getMinutes()).substr(-2)
: "");
}
export function format_date_colloquial(date: Date, current_timestamp: Date) : { result: string; format: ColloquialFormat } {
const format = date_format(date, current_timestamp);
if(format == ColloquialFormat.GENERAL) {
return {
result: format_date_general(date),
format: format
};
} else {
let hrs = date.getHours();
let time = "AM";
if(hrs > 12) {
hrs -= 12;
time = "PM";
}
return {
result: (format == ColloquialFormat.YESTERDAY ? tr("Yesterday at") : tr("Today at")) + " " + hrs + ":" + date.getMinutes() + " " + time,
format: format
};
}
}
export function format_chat_time(date: Date) : {
result: string,
next_update: number /* in MS */
} {
const timestamp = date.getTime();
const current_timestamp = new Date();
const result = {
result: "",
next_update: 0
};
if(settings.static_global(Settings.KEY_CHAT_FIXED_TIMESTAMPS)) {
const format = format_date_colloquial(date, current_timestamp);
result.result = format.result;
result.next_update = 0; /* TODO: Update on day change? */
} else {
const delta = current_timestamp.getTime() - timestamp;
if(delta < 2000) {
result.result = "now";
result.next_update = 2500 - delta; /* update after two seconds */
} else if(delta < 30000) { /* 30 seconds */
result.result = Math.floor(delta / 1000) + " " + tr("seconds ago");
result.next_update = 1000; /* update every second */
} else if(delta < 30 * 60 * 1000) { /* 30 minutes */
if(delta < 120 * 1000)
result.result = tr("one minute ago");
else
result.result = Math.floor(delta / (1000 * 60)) + " " + tr("minutes ago");
result.next_update = 60000; /* updater after a minute */
} else {
result.result = format_date_colloquial(date, current_timestamp).result;
result.next_update = 0; /* TODO: Update on day change? */
}
}
return result;
}
}
export namespace time {
export function format_online_time(secs: number) : string {
let years = Math.floor(secs / (60 * 60 * 24 * 365));
let days = Math.floor(secs / (60 * 60 * 24)) % 365;
let hours = Math.floor(secs / (60 * 60)) % 24;
let minutes = Math.floor(secs / 60) % 60;
let seconds = Math.floor(secs % 60);
let result = "";
if(years > 0)
result += years + " " + tr("years") + " ";
if(years > 0 || days > 0)
result += days + " " + tr("days") + " ";
if(years > 0 || days > 0 || hours > 0)
result += hours + " " + tr("hours") + " ";
if(years > 0 || days > 0 || hours > 0 || minutes > 0)
result += minutes + " " + tr("minutes") + " ";
if(years > 0 || days > 0 || hours > 0 || minutes > 0 || seconds > 0)
result += seconds + " " + tr("seconds") + " ";
else
result = tr("now") + " ";
return result.substr(0, result.length - 1);
}
}
}

View File

@ -1,273 +1,280 @@
namespace chat {
declare function setInterval(handler: TimerHandler, timeout?: number, ...arguments: any[]): number;
declare function setTimeout(handler: TimerHandler, timeout?: number, ...arguments: any[]): number;
import {GroupManager} from "tc-shared/permission/GroupManager";
import {Frame, FrameContent} from "tc-shared/ui/frames/chat_frame";
import {ClientEntry, LocalClientEntry} from "tc-shared/ui/client";
import {openClientInfo} from "tc-shared/ui/modal/ModalClientInfo";
import * as htmltags from "tc-shared/ui/htmltags";
import * as image_preview from "../image_preview";
import {format} from "tc-shared/ui/frames/side/chat_helper";
import * as i18nc from "tc-shared/i18n/country";
export class ClientInfo {
readonly handle: Frame;
private _html_tag: JQuery;
private _current_client: ClientEntry | undefined;
private _online_time_updater: number;
previous_frame_content: FrameContent;
declare function setInterval(handler: TimerHandler, timeout?: number, ...arguments: any[]): number;
declare function setTimeout(handler: TimerHandler, timeout?: number, ...arguments: any[]): number;
constructor(handle: Frame) {
this.handle = handle;
this._build_html_tag();
}
export class ClientInfo {
readonly handle: Frame;
private _html_tag: JQuery;
private _current_client: ClientEntry | undefined;
private _online_time_updater: number;
previous_frame_content: FrameContent;
html_tag() : JQuery {
return this._html_tag;
}
constructor(handle: Frame) {
this.handle = handle;
this._build_html_tag();
}
destroy() {
clearInterval(this._online_time_updater);
html_tag() : JQuery {
return this._html_tag;
}
this._html_tag && this._html_tag.remove();
this._html_tag = undefined;
destroy() {
clearInterval(this._online_time_updater);
this._current_client = undefined;
this.previous_frame_content = undefined;
}
this._html_tag && this._html_tag.remove();
this._html_tag = undefined;
private _build_html_tag() {
this._html_tag = $("#tmpl_frame_chat_client_info").renderTag();
this._html_tag.find(".button-close").on('click', () => {
if(this.previous_frame_content === FrameContent.CLIENT_INFO)
this.previous_frame_content = FrameContent.NONE;
this._current_client = undefined;
this.previous_frame_content = undefined;
}
this.handle.set_content(this.previous_frame_content);
});
this._html_tag.find(".button-more").on('click', () => {
if(!this._current_client)
return;
private _build_html_tag() {
this._html_tag = $("#tmpl_frame_chat_client_info").renderTag();
this._html_tag.find(".button-close").on('click', () => {
if(this.previous_frame_content === FrameContent.CLIENT_INFO)
this.previous_frame_content = FrameContent.NONE;
Modals.openClientInfo(this._current_client);
});
this._html_tag.find('.container-avatar-edit').on('click', () => this.handle.handle.update_avatar());
}
current_client() : ClientEntry {
return this._current_client;
}
set_current_client(client: ClientEntry | undefined, enforce?: boolean) {
if(client) client.updateClientVariables(); /* just to ensure */
if(client === this._current_client && (typeof(enforce) === "undefined" || !enforce))
this.handle.set_content(this.previous_frame_content);
});
this._html_tag.find(".button-more").on('click', () => {
if(!this._current_client)
return;
this._current_client = client;
openClientInfo(this._current_client);
});
this._html_tag.find('.container-avatar-edit').on('click', () => this.handle.handle.update_avatar());
}
/* updating the header */
{
const client_name = this._html_tag.find(".client-name");
client_name.children().remove();
htmltags.generate_client_object({
add_braces: false,
client_name: client ? client.clientNickName() : "undefined",
client_unique_id: client ? client.clientUid() : "",
client_id: client ? client.clientId() : 0
}).appendTo(client_name);
current_client() : ClientEntry {
return this._current_client;
}
const client_description = this._html_tag.find(".client-description");
client_description.text(client ? client.properties.client_description : "").toggle(!!client.properties.client_description);
set_current_client(client: ClientEntry | undefined, enforce?: boolean) {
if(client) client.updateClientVariables(); /* just to ensure */
if(client === this._current_client && (typeof(enforce) === "undefined" || !enforce))
return;
const is_local_entry = client instanceof LocalClientEntry;
const container_avatar = this._html_tag.find(".container-avatar");
container_avatar.find(".avatar").remove();
if(client) {
const avatar = this.handle.handle.fileManager.avatars.generate_chat_tag({id: client.clientId()}, client.clientUid());
if(!is_local_entry) {
avatar.css("cursor", "pointer").on('click', event => {
image_preview.preview_image_tag(this.handle.handle.fileManager.avatars.generate_chat_tag({id: client.clientId()}, client.clientUid()));
});
this._current_client = client;
/* updating the header */
{
const client_name = this._html_tag.find(".client-name");
client_name.children().remove();
htmltags.generate_client_object({
add_braces: false,
client_name: client ? client.clientNickName() : "undefined",
client_unique_id: client ? client.clientUid() : "",
client_id: client ? client.clientId() : 0
}).appendTo(client_name);
const client_description = this._html_tag.find(".client-description");
client_description.text(client ? client.properties.client_description : "").toggle(!!client.properties.client_description);
const is_local_entry = client instanceof LocalClientEntry;
const container_avatar = this._html_tag.find(".container-avatar");
container_avatar.find(".avatar").remove();
if(client) {
const avatar = this.handle.handle.fileManager.avatars.generate_chat_tag({id: client.clientId()}, client.clientUid());
if(!is_local_entry) {
avatar.css("cursor", "pointer").on('click', event => {
image_preview.preview_image_tag(this.handle.handle.fileManager.avatars.generate_chat_tag({id: client.clientId()}, client.clientUid()));
});
}
avatar.appendTo(container_avatar);
} else
this.handle.handle.fileManager.avatars.generate_chat_tag(undefined, undefined).appendTo(container_avatar);
container_avatar.toggleClass("editable", is_local_entry);
}
/* updating the info fields */
{
const online_time = this._html_tag.find(".client-online-time");
online_time.text(format.time.format_online_time(client ? client.calculateOnlineTime() : 0));
if(this._online_time_updater) {
clearInterval(this._online_time_updater);
this._online_time_updater = 0;
}
if(client) {
this._online_time_updater = setInterval(() => {
const client = this._current_client;
if(!client) {
clearInterval(this._online_time_updater);
this._online_time_updater = undefined;
return;
}
avatar.appendTo(container_avatar);
} else
this.handle.handle.fileManager.avatars.generate_chat_tag(undefined, undefined).appendTo(container_avatar);
container_avatar.toggleClass("editable", is_local_entry);
}
/* updating the info fields */
{
const online_time = this._html_tag.find(".client-online-time");
online_time.text(format.time.format_online_time(client ? client.calculateOnlineTime() : 0));
if(this._online_time_updater) {
clearInterval(this._online_time_updater);
this._online_time_updater = 0;
}
if(client) {
this._online_time_updater = setInterval(() => {
const client = this._current_client;
if(!client) {
clearInterval(this._online_time_updater);
this._online_time_updater = undefined;
return;
}
if(client.currentChannel()) /* If he has no channel then he might be disconnected */
online_time.text(format.time.format_online_time(client.calculateOnlineTime()));
else {
online_time.text(online_time.text() + tr(" (left view)"));
clearInterval(this._online_time_updater);
}
}, 1000);
}
const country = this._html_tag.find(".client-country");
country.children().detach();
const country_code = (client ? client.properties.client_country : undefined) || "xx";
$.spawn("div").addClass("country flag-" + country_code.toLowerCase()).appendTo(country);
$.spawn("a").text(i18n.country_name(country_code.toUpperCase())).appendTo(country);
const version = this._html_tag.find(".client-version");
version.children().detach();
if(client) {
let platform = client.properties.client_platform;
if(platform.indexOf("Win32") != 0 && (client.properties.client_version.indexOf("Win64") != -1 || client.properties.client_version.indexOf("WOW64") != -1))
platform = platform.replace("Win32", "Win64");
$.spawn("a").attr("title", client.properties.client_version).text(
client.properties.client_version.split(" ")[0] + " on " + platform
).appendTo(version);
}
const volume = this._html_tag.find(".client-local-volume");
volume.text((client && client.get_audio_handle() ? (client.get_audio_handle().get_volume() * 100) : -1).toFixed(0) + "%");
if(client.currentChannel()) /* If he has no channel then he might be disconnected */
online_time.text(format.time.format_online_time(client.calculateOnlineTime()));
else {
online_time.text(online_time.text() + tr(" (left view)"));
clearInterval(this._online_time_updater);
}
}, 1000);
}
/* teaspeak forum */
{
const container_forum = this._html_tag.find(".container-teaforo");
if(client && client.properties.client_teaforo_id) {
container_forum.show();
const country = this._html_tag.find(".client-country");
country.children().detach();
const country_code = (client ? client.properties.client_country : undefined) || "xx";
$.spawn("div").addClass("country flag-" + country_code.toLowerCase()).appendTo(country);
$.spawn("a").text(i18nc.country_name(country_code.toUpperCase())).appendTo(country);
const container_data = container_forum.find(".client-teaforo-account");
container_data.children().remove();
let text = client.properties.client_teaforo_name;
if((client.properties.client_teaforo_flags & 0x01) > 0)
text += " (" + tr("Banned") + ")";
if((client.properties.client_teaforo_flags & 0x02) > 0)
text += " (" + tr("Stuff") + ")";
if((client.properties.client_teaforo_flags & 0x04) > 0)
text += " (" + tr("Premium") + ")";
const version = this._html_tag.find(".client-version");
version.children().detach();
if(client) {
let platform = client.properties.client_platform;
if(platform.indexOf("Win32") != 0 && (client.properties.client_version.indexOf("Win64") != -1 || client.properties.client_version.indexOf("WOW64") != -1))
platform = platform.replace("Win32", "Win64");
$.spawn("a").attr("title", client.properties.client_version).text(
client.properties.client_version.split(" ")[0] + " on " + platform
).appendTo(version);
}
$.spawn("a")
.attr("href", "https://forum.teaspeak.de/index.php?members/" + client.properties.client_teaforo_id)
.attr("target", "_blank")
.text(text)
.appendTo(container_data);
const volume = this._html_tag.find(".client-local-volume");
volume.text((client && client.get_audio_handle() ? (client.get_audio_handle().get_volume() * 100) : -1).toFixed(0) + "%");
}
/* teaspeak forum */
{
const container_forum = this._html_tag.find(".container-teaforo");
if(client && client.properties.client_teaforo_id) {
container_forum.show();
const container_data = container_forum.find(".client-teaforo-account");
container_data.children().remove();
let text = client.properties.client_teaforo_name;
if((client.properties.client_teaforo_flags & 0x01) > 0)
text += " (" + tr("Banned") + ")";
if((client.properties.client_teaforo_flags & 0x02) > 0)
text += " (" + tr("Stuff") + ")";
if((client.properties.client_teaforo_flags & 0x04) > 0)
text += " (" + tr("Premium") + ")";
$.spawn("a")
.attr("href", "https://forum.teaspeak.de/index.php?members/" + client.properties.client_teaforo_id)
.attr("target", "_blank")
.text(text)
.appendTo(container_data);
} else {
container_forum.hide();
}
}
/* update the client status */
{
//TODO Implement client status!
const container_status = this._html_tag.find(".container-client-status");
const container_status_entries = container_status.find(".client-status");
container_status_entries.children().detach();
if(client) {
if(client.properties.client_away) {
container_status_entries.append(
$.spawn("div").addClass("status-entry").append(
$.spawn("div").addClass("icon_em client-away"),
$.spawn("a").text(tr("Away")),
client.properties.client_away_message ?
$.spawn("a").addClass("away-message").text("(" + client.properties.client_away_message + ")") :
undefined
)
)
}
if(client.is_muted()) {
container_status_entries.append(
$.spawn("div").addClass("status-entry").append(
$.spawn("div").addClass("icon_em client-input_muted_local"),
$.spawn("a").text(tr("Client local muted"))
)
)
}
if(!client.properties.client_output_hardware) {
container_status_entries.append(
$.spawn("div").addClass("status-entry").append(
$.spawn("div").addClass("icon_em client-hardware_output_muted"),
$.spawn("a").text(tr("Speakers/Headphones disabled"))
)
)
}
if(!client.properties.client_input_hardware) {
container_status_entries.append(
$.spawn("div").addClass("status-entry").append(
$.spawn("div").addClass("icon_em client-hardware_input_muted"),
$.spawn("a").text(tr("Microphone disabled"))
)
)
}
if(client.properties.client_output_muted) {
container_status_entries.append(
$.spawn("div").addClass("status-entry").append(
$.spawn("div").addClass("icon_em client-output_muted"),
$.spawn("a").text(tr("Speakers/Headphones Muted"))
)
)
}
if(client.properties.client_input_muted) {
container_status_entries.append(
$.spawn("div").addClass("status-entry").append(
$.spawn("div").addClass("icon_em client-input_muted"),
$.spawn("a").text(tr("Microphone Muted"))
)
)
}
}
container_status.toggle(container_status_entries.children().length > 0);
}
/* update client server groups */
{
const container_groups = this._html_tag.find(".client-group-server");
container_groups.children().detach();
if(client) {
const invalid_groups = [];
const groups = client.assignedServerGroupIds().map(group_id => {
const result = this.handle.handle.groups.serverGroup(group_id);
if(!result)
invalid_groups.push(group_id);
return result;
}).filter(e => !!e).sort(GroupManager.sorter());
for(const invalid_id of invalid_groups) {
container_groups.append($.spawn("a").text("{" + tr("server group ") + invalid_groups + "}").attr("title", tr("Missing server group id!") + " (" + invalid_groups + ")"));
}
for(let group of groups) {
container_groups.append(
$.spawn("div").addClass("group-container")
.append(
this.handle.handle.fileManager.icons.generateTag(group.properties.iconid)
).append(
$.spawn("a").text(group.name).attr("title", tr("Group id: ") + group.id)
)
);
}
}
}
/* update client channel group */
{
const container_group = this._html_tag.find(".client-group-channel");
container_group.children().detach();
if(client) {
const group_id = client.assignedChannelGroup();
let group = this.handle.handle.groups.channelGroup(group_id);
if(group) {
container_group.append(
$.spawn("div").addClass("group-container")
.append(
this.handle.handle.fileManager.icons.generateTag(group.properties.iconid)
).append(
$.spawn("a").text(group.name).attr("title", tr("Group id: ") + group_id)
)
);
} else {
container_forum.hide();
}
}
/* update the client status */
{
//TODO Implement client status!
const container_status = this._html_tag.find(".container-client-status");
const container_status_entries = container_status.find(".client-status");
container_status_entries.children().detach();
if(client) {
if(client.properties.client_away) {
container_status_entries.append(
$.spawn("div").addClass("status-entry").append(
$.spawn("div").addClass("icon_em client-away"),
$.spawn("a").text(tr("Away")),
client.properties.client_away_message ?
$.spawn("a").addClass("away-message").text("(" + client.properties.client_away_message + ")") :
undefined
)
)
}
if(client.is_muted()) {
container_status_entries.append(
$.spawn("div").addClass("status-entry").append(
$.spawn("div").addClass("icon_em client-input_muted_local"),
$.spawn("a").text(tr("Client local muted"))
)
)
}
if(!client.properties.client_output_hardware) {
container_status_entries.append(
$.spawn("div").addClass("status-entry").append(
$.spawn("div").addClass("icon_em client-hardware_output_muted"),
$.spawn("a").text(tr("Speakers/Headphones disabled"))
)
)
}
if(!client.properties.client_input_hardware) {
container_status_entries.append(
$.spawn("div").addClass("status-entry").append(
$.spawn("div").addClass("icon_em client-hardware_input_muted"),
$.spawn("a").text(tr("Microphone disabled"))
)
)
}
if(client.properties.client_output_muted) {
container_status_entries.append(
$.spawn("div").addClass("status-entry").append(
$.spawn("div").addClass("icon_em client-output_muted"),
$.spawn("a").text(tr("Speakers/Headphones Muted"))
)
)
}
if(client.properties.client_input_muted) {
container_status_entries.append(
$.spawn("div").addClass("status-entry").append(
$.spawn("div").addClass("icon_em client-input_muted"),
$.spawn("a").text(tr("Microphone Muted"))
)
)
}
}
container_status.toggle(container_status_entries.children().length > 0);
}
/* update client server groups */
{
const container_groups = this._html_tag.find(".client-group-server");
container_groups.children().detach();
if(client) {
const invalid_groups = [];
const groups = client.assignedServerGroupIds().map(group_id => {
const result = this.handle.handle.groups.serverGroup(group_id);
if(!result)
invalid_groups.push(group_id);
return result;
}).filter(e => !!e).sort(GroupManager.sorter());
for(const invalid_id of invalid_groups) {
container_groups.append($.spawn("a").text("{" + tr("server group ") + invalid_groups + "}").attr("title", tr("Missing server group id!") + " (" + invalid_groups + ")"));
}
for(let group of groups) {
container_groups.append(
$.spawn("div").addClass("group-container")
.append(
this.handle.handle.fileManager.icons.generateTag(group.properties.iconid)
).append(
$.spawn("a").text(group.name).attr("title", tr("Group id: ") + group.id)
)
);
}
}
}
/* update client channel group */
{
const container_group = this._html_tag.find(".client-group-channel");
container_group.children().detach();
if(client) {
const group_id = client.assignedChannelGroup();
let group = this.handle.handle.groups.channelGroup(group_id);
if(group) {
container_group.append(
$.spawn("div").addClass("group-container")
.append(
this.handle.handle.fileManager.icons.generateTag(group.properties.iconid)
).append(
$.spawn("a").text(group.name).attr("title", tr("Group id: ") + group_id)
)
);
} else {
container_group.append($.spawn("a").text(tr("Invalid channel group!")).attr("title", tr("Missing channel group id!") + " (" + group_id + ")"));
}
container_group.append($.spawn("a").text(tr("Invalid channel group!")).attr("title", tr("Missing channel group id!") + " (" + group_id + ")"));
}
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -1,253 +1,236 @@
namespace htmltags {
let mouse_coordinates: {x: number, y: number} = {x: 0, y: 0};
import * as log from "tc-shared/log";
import {LogCategory} from "tc-shared/log";
import {ChannelEntry} from "tc-shared/ui/channel";
import {ClientEntry} from "tc-shared/ui/client";
import {htmlEscape} from "tc-shared/ui/frames/chat";
import {server_connections} from "tc-shared/ui/frames/connection_handlers";
let mouse_coordinates: {x: number, y: number} = {x: 0, y: 0};
function initialize() {
document.addEventListener('mousemove', event => {
mouse_coordinates.x = event.pageX;
mouse_coordinates.y = event.pageY;
});
}
initialize();
export interface ClientProperties {
client_id: number,
client_unique_id: string,
client_name: string,
add_braces?: boolean,
client_database_id?: number; /* not yet used */
}
export interface ChannelProperties {
channel_id: number,
channel_name: string,
channel_display_name?: string,
add_braces?: boolean
}
/* required for the bbcodes */
function generate_client_open(properties: ClientProperties) : string {
let result = "";
/* build the opening tag: <div ...> */
result = result + "<div class='htmltag-client' ";
if(properties.client_id)
result = result + "client-id='" + properties.client_id + "' ";
if(properties.client_unique_id && properties.client_unique_id != "unknown") {
try {
result = result + "client-unique-id='" + encodeURIComponent(properties.client_unique_id) + "' ";
} catch(error) {
console.warn(tr("Failed to generate client tag attribute 'client-unique-id': %o"), error);
}
}
if(properties.client_name) {
try {
result = result + "client-name='" + encodeURIComponent(properties.client_name) + "' ";
} catch(error) {
console.warn(tr("Failed to generate client tag attribute 'client-name': %o"), error);
}
}
/* add the click handler */
result += "oncontextmenu='return htmltags.callbacks.callback_context_client($(this));'";
result = result + ">";
return result;
}
export function generate_client(properties: ClientProperties) : string {
let result = generate_client_open(properties);
/* content */
{
if(properties.add_braces)
result = result + "\"";
result = result + htmlEscape(properties.client_name || "undefined").join(" ");
if(properties.add_braces)
result = result + "\"";
}
/* close tag */
{
result += "</div>";
}
return result;
}
export function generate_client_object(properties: ClientProperties) : JQuery {
return $(this.generate_client(properties));
}
/* required for the bbcodes */
function generate_channel_open(properties: ChannelProperties) : string {
let result = "";
/* build the opening tag: <div ...> */
result = result + "<div class='htmltag-channel' ";
if(properties.channel_id)
result = result + "channel-id='" + properties.channel_id + "' ";
if(properties.channel_name)
result = result + "channel-name='" + encodeURIComponent(properties.channel_name) + "' ";
/* add the click handler */
result += "oncontextmenu='return htmltags.callbacks.callback_context_channel($(this));'";
result = result + ">";
return result;
}
export function generate_channel(properties: ChannelProperties) : string {
let result = generate_channel_open(properties);
/* content */
{
if(properties.add_braces)
result = result + "\"";
result = result + htmlEscape(properties.channel_display_name || properties.channel_name || "undefined").join(" ");
if(properties.add_braces)
result = result + "\"";
}
/* close tag */
{
result += "</div>";
}
return result;
}
export function generate_channel_object(properties: ChannelProperties) : JQuery {
return $(this.generate_channel(properties));
}
export namespace callbacks {
export function callback_context_client(element: JQuery) {
const client_id = parseInt(element.attr("client-id") || "0");
const client_unique_id = decodeURIComponent(element.attr("client-unique-id") || "");
/* we ignore the name, we cant find clients by name because the name is too volatile*/
let client: ClientEntry;
const current_connection = server_connections.active_connection_handler();
if(current_connection && current_connection.channelTree) {
if(!client && client_id) {
client = current_connection.channelTree.findClient(client_id);
if(client && (client_unique_id && client.properties.client_unique_identifier != client_unique_id)) {
client = undefined; /* client id dosn't match anymore, lets search for the unique id */
}
}
if(!client && client_unique_id)
client = current_connection.channelTree.find_client_by_unique_id(client_unique_id);
if(!client) {
if(current_connection.channelTree.server.properties.virtualserver_unique_identifier === client_unique_id) {
current_connection.channelTree.server.spawnContextMenu(mouse_coordinates.x, mouse_coordinates.y);
return;
}
}
}
if(!client) {
/* we may should open a "offline" menu? */
log.debug(LogCategory.GENERAL, "Failed to resolve client from html tag. Client id: %o, Client unique id: %o, Client name: %o",
client_id,
client_unique_id,
decodeURIComponent(element.attr("client-name"))
);
return false;
}
client.showContextMenu(mouse_coordinates.x, mouse_coordinates.y);
return false;
}
export function callback_context_channel(element: JQuery) {
const channel_id = parseInt(element.attr("channel-id") || "0");
const current_connection = server_connections.active_connection_handler();
let channel: ChannelEntry;
if(current_connection && current_connection.channelTree) {
channel = current_connection.channelTree.findChannel(channel_id);
}
if(!channel)
return false;
channel.showContextMenu(mouse_coordinates.x, mouse_coordinates.y);
return false;
}
}
declare const xbbcode;
namespace bbcodes {
/* the = because we sometimes get that */
//const url_client_regex = /?client:\/\/(?<client_id>[0-9]+)\/(?<client_unique_id>[a-zA-Z0-9+=#]+)~(?<client_name>(?:[^%]|%[0-9A-Fa-f]{2})+)$/g;
const url_client_regex = /client:\/\/([0-9]+)\/([a-zA-Z0-9+=/#]+)~((?:[^%]|%[0-9A-Fa-f]{2})+)$/g; /* IDK which browsers already support group naming */
const url_channel_regex = /channel:\/\/([0-9]+)~((?:[^%]|%[0-9A-Fa-f]{2})+)$/g;
function initialize() {
document.addEventListener('mousemove', event => {
mouse_coordinates.x = event.pageX;
mouse_coordinates.y = event.pageY;
const origin_url = xbbcode.register.find_parser('url');
xbbcode.register.register_parser({
tag: 'url',
build_html_tag_open(layer): string {
if(layer.options) {
if(layer.options.match(url_channel_regex)) {
const groups = url_channel_regex.exec(layer.options);
return generate_channel_open({
add_braces: false,
channel_id: parseInt(groups[1]),
channel_name: decodeURIComponent(groups[2])
});
} else if(layer.options.match(url_client_regex)) {
const groups = url_client_regex.exec(layer.options);
return generate_client_open({
add_braces: false,
client_id: parseInt(groups[1]),
client_unique_id: groups[2],
client_name: decodeURIComponent(groups[3])
});
}
}
return origin_url.build_html_tag_open(layer);
},
build_html_tag_close(layer): string {
if(layer.options) {
if(layer.options.match(url_client_regex))
return "</div>";
if(layer.options.match(url_channel_regex))
return "</div>";
}
return origin_url.build_html_tag_close(layer);
}
});
}
initialize();
export interface ClientProperties {
client_id: number,
client_unique_id: string,
client_name: string,
add_braces?: boolean,
client_database_id?: number; /* not yet used */
}
export interface ChannelProperties {
channel_id: number,
channel_name: string,
channel_display_name?: string,
add_braces?: boolean
}
/* required for the bbcodes */
function generate_client_open(properties: ClientProperties) : string {
let result = "";
/* build the opening tag: <div ...> */
result = result + "<div class='htmltag-client' ";
if(properties.client_id)
result = result + "client-id='" + properties.client_id + "' ";
if(properties.client_unique_id && properties.client_unique_id != "unknown") {
try {
result = result + "client-unique-id='" + encodeURIComponent(properties.client_unique_id) + "' ";
} catch(error) {
console.warn(tr("Failed to generate client tag attribute 'client-unique-id': %o"), error);
}
}
if(properties.client_name) {
try {
result = result + "client-name='" + encodeURIComponent(properties.client_name) + "' ";
} catch(error) {
console.warn(tr("Failed to generate client tag attribute 'client-name': %o"), error);
}
}
/* add the click handler */
result += "oncontextmenu='return htmltags.callbacks.callback_context_client($(this));'";
result = result + ">";
return result;
}
export function generate_client(properties: ClientProperties) : string {
let result = generate_client_open(properties);
/* content */
{
if(properties.add_braces)
result = result + "\"";
result = result + MessageHelper.htmlEscape(properties.client_name || "undefined").join(" ");
if(properties.add_braces)
result = result + "\"";
}
/* close tag */
{
result += "</div>";
}
return result;
}
export function generate_client_object(properties: ClientProperties) : JQuery {
return $(this.generate_client(properties));
}
/* required for the bbcodes */
function generate_channel_open(properties: ChannelProperties) : string {
let result = "";
/* build the opening tag: <div ...> */
result = result + "<div class='htmltag-channel' ";
if(properties.channel_id)
result = result + "channel-id='" + properties.channel_id + "' ";
if(properties.channel_name)
result = result + "channel-name='" + encodeURIComponent(properties.channel_name) + "' ";
/* add the click handler */
result += "oncontextmenu='return htmltags.callbacks.callback_context_channel($(this));'";
result = result + ">";
return result;
}
export function generate_channel(properties: ChannelProperties) : string {
let result = generate_channel_open(properties);
/* content */
{
if(properties.add_braces)
result = result + "\"";
result = result + MessageHelper.htmlEscape(properties.channel_display_name || properties.channel_name || "undefined").join(" ");
if(properties.add_braces)
result = result + "\"";
}
/* close tag */
{
result += "</div>";
}
return result;
}
export function generate_channel_object(properties: ChannelProperties) : JQuery {
return $(this.generate_channel(properties));
}
export namespace callbacks {
export function callback_context_client(element: JQuery) {
const client_id = parseInt(element.attr("client-id") || "0");
const client_unique_id = decodeURIComponent(element.attr("client-unique-id") || "");
/* we ignore the name, we cant find clients by name because the name is too volatile*/
let client: ClientEntry;
const current_connection = server_connections.active_connection_handler();
if(current_connection && current_connection.channelTree) {
if(!client && client_id) {
client = current_connection.channelTree.findClient(client_id);
if(client && (client_unique_id && client.properties.client_unique_identifier != client_unique_id)) {
client = undefined; /* client id dosn't match anymore, lets search for the unique id */
}
}
if(!client && client_unique_id)
client = current_connection.channelTree.find_client_by_unique_id(client_unique_id);
if(!client) {
if(current_connection.channelTree.server.properties.virtualserver_unique_identifier === client_unique_id) {
current_connection.channelTree.server.spawnContextMenu(mouse_coordinates.x, mouse_coordinates.y);
return;
}
}
}
if(!client) {
/* we may should open a "offline" menu? */
log.debug(LogCategory.GENERAL, "Failed to resolve client from html tag. Client id: %o, Client unique id: %o, Client name: %o",
client_id,
client_unique_id,
decodeURIComponent(element.attr("client-name"))
);
return false;
}
client.showContextMenu(mouse_coordinates.x, mouse_coordinates.y);
return false;
}
export function callback_context_channel(element: JQuery) {
const channel_id = parseInt(element.attr("channel-id") || "0");
const current_connection = server_connections.active_connection_handler();
let channel: ChannelEntry;
if(current_connection && current_connection.channelTree) {
channel = current_connection.channelTree.findChannel(channel_id);
}
if(!channel)
return false;
channel.showContextMenu(mouse_coordinates.x, mouse_coordinates.y);
return false;
}
}
namespace bbcodes {
/* the = because we sometimes get that */
//const url_client_regex = /?client:\/\/(?<client_id>[0-9]+)\/(?<client_unique_id>[a-zA-Z0-9+=#]+)~(?<client_name>(?:[^%]|%[0-9A-Fa-f]{2})+)$/g;
const url_client_regex = /client:\/\/([0-9]+)\/([a-zA-Z0-9+=/#]+)~((?:[^%]|%[0-9A-Fa-f]{2})+)$/g; /* IDK which browsers already support group naming */
const url_channel_regex = /channel:\/\/([0-9]+)~((?:[^%]|%[0-9A-Fa-f]{2})+)$/g;
function initialize() {
const origin_url = xbbcode.register.find_parser('url');
xbbcode.register.register_parser({
tag: 'url',
build_html_tag_open(layer): string {
if(layer.options) {
if(layer.options.match(url_channel_regex)) {
const groups = url_channel_regex.exec(layer.options);
return generate_channel_open({
add_braces: false,
channel_id: parseInt(groups[1]),
channel_name: decodeURIComponent(groups[2])
});
} else if(layer.options.match(url_client_regex)) {
const groups = url_client_regex.exec(layer.options);
return generate_client_open({
add_braces: false,
client_id: parseInt(groups[1]),
client_unique_id: groups[2],
client_name: decodeURIComponent(groups[3])
});
}
}
return origin_url.build_html_tag_open(layer);
},
build_html_tag_close(layer): string {
if(layer.options) {
if(layer.options.match(url_client_regex))
return "</div>";
if(layer.options.match(url_channel_regex))
return "</div>";
}
return origin_url.build_html_tag_close(layer);
}
})
/*
"img": {
openTag: function(params,content) {
let myUrl;
if (!params) {
myUrl = content.replace(/<.*?>/g,"");
} else {
myUrl = params.substr(1);
}
urlPattern.lastIndex = 0;
if ( !urlPattern.test( myUrl ) ) {
myUrl = "#";
}
return '<a href="' + myUrl + '" target="_blank">';
},
closeTag: function(params,content) {
return '</a>';
}
},
*/
}
initialize();
}
}

View File

@ -1,55 +1,54 @@
/// <reference path="../../ui/elements/modal.ts" />
/// <reference path="../../ConnectionHandler.ts" />
/// <reference path="../../proto.ts" />
import {createModal} from "tc-shared/ui/elements/Modal";
import * as loader from "tc-loader";
import {LogCategory} from "tc-shared/log";
import * as log from "tc-shared/log";
namespace Modals {
function format_date(date: number) {
const d = new Date(date);
function format_date(date: number) {
const d = new Date(date);
return ('00' + d.getDay()).substr(-2) + "." + ('00' + d.getMonth()).substr(-2) + "." + d.getFullYear() + " - " + ('00' + d.getHours()).substr(-2) + ":" + ('00' + d.getMinutes()).substr(-2);
}
return ('00' + d.getDay()).substr(-2) + "." + ('00' + d.getMonth()).substr(-2) + "." + d.getFullYear() + " - " + ('00' + d.getHours()).substr(-2) + ":" + ('00' + d.getMinutes()).substr(-2);
}
export function spawnAbout() {
const app_version = (() => {
const version_node = document.getElementById("app_version");
if(!version_node) return undefined;
export function spawnAbout() {
const app_version = (() => {
const version_node = document.getElementById("app_version");
if(!version_node) return undefined;
const version = version_node.hasAttribute("value") ? version_node.getAttribute("value") : undefined;
if(!version) return undefined;
const version = version_node.hasAttribute("value") ? version_node.getAttribute("value") : undefined;
if(!version) return undefined;
if(version == "unknown" || version.replace(/0+/, "").length == 0)
return undefined;
if(version == "unknown" || version.replace(/0+/, "").length == 0)
return undefined;
return version;
})();
return version;
})();
const connectModal = createModal({
header: tr("About"),
body: () => {
let tag = $("#tmpl_about").renderTag({
client: !app.is_web(),
const connectModal = createModal({
header: tr("About"),
body: () => {
let tag = $("#tmpl_about").renderTag({
client: loader.version().type !== "web",
version_client: app.is_web() ? app_version || "in-dev" : "loading...",
version_ui: app_version || "in-dev",
version_client: loader.version().type === "web" ? app_version || "in-dev" : "loading...",
version_ui: app_version || "in-dev",
version_timestamp: !!app_version ? format_date(Date.now()) : "--"
});
return tag;
},
footer: null,
width: "60em"
});
connectModal.htmlTag.find(".modal-body").addClass("modal-about");
connectModal.open();
if(!app.is_web()) {
(window as any).native.client_version().then(version => {
connectModal.htmlTag.find(".version-client").text(version);
}).catch(error => {
log.error(LogCategory.GENERAL, tr("Failed to load client version: %o"), error);
connectModal.htmlTag.find(".version-client").text("unknown");
version_timestamp: !!app_version ? format_date(Date.now()) : "--"
});
}
return tag;
},
footer: null,
width: "60em"
});
connectModal.htmlTag.find(".modal-body").addClass("modal-about");
connectModal.open();
if(loader.version().type !== "web") {
(window as any).native.client_version().then(version => {
connectModal.htmlTag.find(".version-client").text(version);
}).catch(error => {
log.error(LogCategory.GENERAL, tr("Failed to load client version: %o"), error);
connectModal.htmlTag.find(".version-client").text("unknown");
});
}
}

View File

@ -1,74 +1,72 @@
/// <reference path="../../ui/elements/modal.ts" />
/// <reference path="../../ConnectionHandler.ts" />
/// <reference path="../../proto.ts" />
//TODO: Test if we could render this image and not only the browser by knowing the type.
import {createErrorModal, createModal} from "tc-shared/ui/elements/Modal";
import {tra} from "tc-shared/i18n/localize";
import {arrayBufferBase64} from "tc-shared/utils/buffers";
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({
header: tr("Avatar Upload"),
footer: undefined,
body: () => {
return $("#tmpl_avatar_upload").renderTag({});
}
export function spawnAvatarUpload(callback_data: (data: ArrayBuffer | undefined | null) => any) {
const modal = createModal({
header: tr("Avatar Upload"),
footer: undefined,
body: () => {
return $("#tmpl_avatar_upload").renderTag({});
}
});
let _data_submitted = false;
let _current_avatar;
modal.htmlTag.find(".button-select").on('click', event => {
modal.htmlTag.find(".file-inputs").trigger('click');
});
modal.htmlTag.find(".button-delete").on('click', () => {
if(_data_submitted)
return;
_data_submitted = true;
modal.close();
callback_data(null);
});
modal.htmlTag.find(".button-cancel").on('click', () => modal.close());
const button_upload = modal.htmlTag.find(".button-upload");
button_upload.on('click', event => (!_data_submitted) && (_data_submitted = true, modal.close(), true) && callback_data(_current_avatar));
const set_avatar = (data: string | undefined, type?: string) => {
_current_avatar = data ? arrayBufferBase64(data) : undefined;
button_upload.prop("disabled", !_current_avatar);
modal.htmlTag.find(".preview img").attr("src", data ? ("data:image/" + type + ";base64," + data) : "img/style/avatar.png");
};
const input_node = modal.htmlTag.find(".file-inputs")[0] as HTMLInputElement;
input_node.multiple = false;
modal.htmlTag.find(".file-inputs").on('change', event => {
console.log("Files: %o", input_node.files);
const read_file = (file: File) => new Promise<string>((resolve, reject) => {
const reader = new FileReader();
reader.onload = () => resolve(reader.result as string);
reader.onerror = error => reject(error);
reader.readAsDataURL(file);
});
let _data_submitted = false;
let _current_avatar;
(async () => {
const data = await read_file(input_node.files[0]);
modal.htmlTag.find(".button-select").on('click', event => {
modal.htmlTag.find(".file-inputs").trigger('click');
});
modal.htmlTag.find(".button-delete").on('click', () => {
if(_data_submitted)
if(!data.startsWith("data:image/")) {
console.error(tr("Failed to load file %s: Invalid data media type (%o)"), input_node.files[0].name, data);
createErrorModal(tr("Icon upload failed"), tra("Failed to select avatar {}.<br>File is not an image", input_node.files[0].name)).open();
return;
_data_submitted = true;
modal.close();
callback_data(null);
});
}
const semi = data.indexOf(';');
const type = data.substring(11, semi);
console.log(tr("Given image has type %s"), type);
modal.htmlTag.find(".button-cancel").on('click', () => modal.close());
const button_upload = modal.htmlTag.find(".button-upload");
button_upload.on('click', event => (!_data_submitted) && (_data_submitted = true, modal.close(), true) && callback_data(_current_avatar));
const set_avatar = (data: string | undefined, type?: string) => {
_current_avatar = data ? arrayBufferBase64(data) : undefined;
button_upload.prop("disabled", !_current_avatar);
modal.htmlTag.find(".preview img").attr("src", data ? ("data:image/" + type + ";base64," + data) : "img/style/avatar.png");
};
const input_node = modal.htmlTag.find(".file-inputs")[0] as HTMLInputElement;
input_node.multiple = false;
modal.htmlTag.find(".file-inputs").on('change', event => {
console.log("Files: %o", input_node.files);
const read_file = (file: File) => new Promise<string>((resolve, reject) => {
const reader = new FileReader();
reader.onload = () => resolve(reader.result as string);
reader.onerror = error => reject(error);
reader.readAsDataURL(file);
});
(async () => {
const data = await read_file(input_node.files[0]);
if(!data.startsWith("data:image/")) {
console.error(tr("Failed to load file %s: Invalid data media type (%o)"), input_node.files[0].name, data);
createErrorModal(tr("Icon upload failed"), tra("Failed to select avatar {}.<br>File is not an image", input_node.files[0].name)).open();
return;
}
const semi = data.indexOf(';');
const type = data.substring(11, semi);
console.log(tr("Given image has type %s"), type);
set_avatar(data.substr(semi + 8 /* 8 bytes := base64, */), type);
})();
});
set_avatar(undefined);
modal.close_listener.push(() => !_data_submitted && callback_data(undefined));
modal.open();
}
set_avatar(data.substr(semi + 8 /* 8 bytes := base64, */), type);
})();
});
set_avatar(undefined);
modal.close_listener.push(() => !_data_submitted && callback_data(undefined));
modal.open();
}

View File

@ -1,162 +1,166 @@
/// <reference path="../../ui/elements/modal.ts" />
/// <reference path="../../ConnectionHandler.ts" />
/// <reference path="../../proto.ts" />
import {createErrorModal, createModal} from "tc-shared/ui/elements/Modal";
import {LogCategory} from "tc-shared/log";
import {ConnectionHandler} from "tc-shared/ConnectionHandler";
import {base64_encode_ab} from "tc-shared/utils/buffers";
import {media_image_type} from "tc-shared/FileManager";
import {spawnYesNo} from "tc-shared/ui/modal/ModalYesNo";
import {ClientEntry} from "tc-shared/ui/client";
import * as log from "tc-shared/log";
namespace Modals {
const avatar_to_uid = (id: string) => {
const buffer = new Uint8Array(id.length / 2);
for(let index = 0; index < id.length; index += 2) {
const upper_nibble = id.charCodeAt(index) - 97;
const lower_nibble = id.charCodeAt(index + 1) - 97;
buffer[index / 2] = (upper_nibble << 4) | lower_nibble;
const avatar_to_uid = (id: string) => {
const buffer = new Uint8Array(id.length / 2);
for(let index = 0; index < id.length; index += 2) {
const upper_nibble = id.charCodeAt(index) - 97;
const lower_nibble = id.charCodeAt(index + 1) - 97;
buffer[index / 2] = (upper_nibble << 4) | lower_nibble;
}
return base64_encode_ab(buffer);
};
export const human_file_size = (size: number) => {
if(size < 1000)
return size + "B";
const exp = Math.floor(Math.log2(size) / 10);
return (size / Math.pow(1024, exp)).toFixed(2) + 'KMGTPE'.charAt(exp - 1) + "iB";
};
declare const moment;
export function spawnAvatarList(client: ConnectionHandler) {
const modal = createModal({
header: tr("Avatars"),
footer: undefined,
body: () => {
const template = $("#tmpl_avatar_list").renderTag({});
return template;
}
return base64_encode_ab(buffer);
};
});
export const human_file_size = (size: number) => {
if(size < 1000)
return size + "B";
const exp = Math.floor(Math.log2(size) / 10);
return (size / Math.pow(1024, exp)).toFixed(2) + 'KMGTPE'.charAt(exp - 1) + "iB";
};
let callback_download: () => any;
let callback_delete: () => any;
export function spawnAvatarList(client: ConnectionHandler) {
const modal = createModal({
header: tr("Avatars"),
footer: undefined,
body: () => {
const template = $("#tmpl_avatar_list").renderTag({});
const button_download = modal.htmlTag.find(".button-download");
const button_delete = modal.htmlTag.find(".button-delete");
const container_list = modal.htmlTag.find(".container-list .list-entries-container");
const list_entries = container_list.find(".list-entries");
const container_info = modal.htmlTag.find(".container-info");
const overlay_no_user = container_info.find(".disabled-overlay").show();
return template;
const set_selected_avatar = (unique_id: string, avatar_id: string, size: number) => {
button_download.prop("disabled", true);
callback_download = undefined;
if(!unique_id) {
overlay_no_user.show();
return;
}
const tag_username = container_info.find(".property-username");
const tag_unique_id = container_info.find(".property-unique-id");
const tag_avatar_id = container_info.find(".property-avatar-id");
const container_avatar = container_info.find(".container-image");
const tag_image_bytes = container_info.find(".property-image-size");
const tag_image_width = container_info.find(".property-image-width").val(tr("loading..."));
const tag_image_height = container_info.find(".property-image-height").val(tr("loading..."));
const tag_image_type = container_info.find(".property-image-type").val(tr("loading..."));
tag_username.val("unknown");
tag_unique_id.val(unique_id);
tag_avatar_id.val(avatar_id);
tag_image_bytes.val(size);
container_avatar.empty().append(client.fileManager.avatars.generate_tag(avatar_id, undefined, {
callback_image: image => {
tag_image_width.val(image[0].naturalWidth + 'px');
tag_image_height.val(image[0].naturalHeight + 'px');
},
callback_avatar: avatar => {
tag_image_type.val(media_image_type(avatar.type));
button_download.prop("disabled", false);
callback_download = () => {
const element = $.spawn("a")
.text("download")
.attr("href", avatar.url)
.attr("download", "avatar-" + unique_id + "." + media_image_type(avatar.type, true))
.css("display", "none")
.appendTo($("body"));
element[0].click();
element.remove();
};
}
});
}));
let callback_download: () => any;
let callback_delete: () => any;
const button_download = modal.htmlTag.find(".button-download");
const button_delete = modal.htmlTag.find(".button-delete");
const container_list = modal.htmlTag.find(".container-list .list-entries-container");
const list_entries = container_list.find(".list-entries");
const container_info = modal.htmlTag.find(".container-info");
const overlay_no_user = container_info.find(".disabled-overlay").show();
const set_selected_avatar = (unique_id: string, avatar_id: string, size: number) => {
button_download.prop("disabled", true);
callback_download = undefined;
if(!unique_id) {
overlay_no_user.show();
return;
}
const tag_username = container_info.find(".property-username");
const tag_unique_id = container_info.find(".property-unique-id");
const tag_avatar_id = container_info.find(".property-avatar-id");
const container_avatar = container_info.find(".container-image");
const tag_image_bytes = container_info.find(".property-image-size");
const tag_image_width = container_info.find(".property-image-width").val(tr("loading..."));
const tag_image_height = container_info.find(".property-image-height").val(tr("loading..."));
const tag_image_type = container_info.find(".property-image-type").val(tr("loading..."));
tag_username.val("unknown");
tag_unique_id.val(unique_id);
tag_avatar_id.val(avatar_id);
tag_image_bytes.val(size);
container_avatar.empty().append(client.fileManager.avatars.generate_tag(avatar_id, undefined, {
callback_image: image => {
tag_image_width.val(image[0].naturalWidth + 'px');
tag_image_height.val(image[0].naturalHeight + 'px');
},
callback_avatar: avatar => {
tag_image_type.val(media_image_type(avatar.type));
button_download.prop("disabled", false);
callback_download = () => {
const element = $.spawn("a")
.text("download")
.attr("href", avatar.url)
.attr("download", "avatar-" + unique_id + "." + media_image_type(avatar.type, true))
.css("display", "none")
.appendTo($("body"));
element[0].click();
element.remove();
};
callback_delete = () => {
spawnYesNo(tr("Are you sure?"), tr("Do you really want to delete this avatar?"), result => {
if(result) {
createErrorModal(tr("Not implemented"), tr("Avatar delete hasn't implemented yet")).open();
//TODO Implement avatar delete
}
}));
callback_delete = () => {
spawnYesNo(tr("Are you sure?"), tr("Do you really want to delete this avatar?"), result => {
if(result) {
createErrorModal(tr("Not implemented"), tr("Avatar delete hasn't implemented yet")).open();
//TODO Implement avatar delete
}
});
};
overlay_no_user.hide();
};
set_selected_avatar(undefined, undefined, 0);
const update_avatar_list = () => {
const template_entry = $("#tmpl_avatar_list-list_entry");
list_entries.empty();
client.fileManager.requestFileList("/").then(files => {
const username_resolve: {[unique_id: string]:((name:string) => any)[]} = {};
for(const entry of files) {
const avatar_id = entry.name.substr('avatar_'.length);
const unique_id = avatar_to_uid(avatar_id);
const tag = template_entry.renderTag({
username: 'loading',
unique_id: unique_id,
size: human_file_size(entry.size),
timestamp: moment(entry.datetime * 1000).format('YY-MM-DD HH:mm')
});
(username_resolve[unique_id] || (username_resolve[unique_id] = [])).push(name => {
const tag_username = tag.find(".column-username").empty();
if(name) {
tag_username.append(ClientEntry.chatTag(0, name, unique_id, false));
} else {
tag_username.text("unknown");
}
});
list_entries.append(tag);
tag.on('click', () => {
list_entries.find('.selected').removeClass('selected');
tag.addClass('selected');
set_selected_avatar(unique_id, avatar_id, entry.size);
});
}
if(container_list.hasScrollBar())
container_list.addClass("scrollbar");
client.serverConnection.command_helper.info_from_uid(...Object.keys(username_resolve)).then(result => {
for(const info of result) {
username_resolve[info.client_unique_id].forEach(e => e(info.client_nickname));
delete username_resolve[info.client_unique_id];
}
for(const uid of Object.keys(username_resolve)) {
(username_resolve[uid] || []).forEach(e => e(undefined));
}
}).catch(error => {
log.error(LogCategory.GENERAL, tr("Failed to fetch usernames from avatar names. Error: %o"), error);
createErrorModal(tr("Failed to fetch usernames"), tr("Failed to fetch usernames related to their avatar names"), undefined).open();
})
}).catch(error => {
//TODO: Display no perms error
log.error(LogCategory.GENERAL, tr("Failed to receive avatar list. Error: %o"), error);
createErrorModal(tr("Failed to list avatars"), tr("Failed to receive avatar list."), undefined).open();
});
};
overlay_no_user.hide();
};
set_selected_avatar(undefined, undefined, 0);
button_download.on('click', () => (callback_download || (() => {}))());
button_delete.on('click', () => (callback_delete || (() => {}))());
setTimeout(() => update_avatar_list(), 250);
modal.open();
}
const update_avatar_list = () => {
const template_entry = $("#tmpl_avatar_list-list_entry");
list_entries.empty();
client.fileManager.requestFileList("/").then(files => {
const username_resolve: {[unique_id: string]:((name:string) => any)[]} = {};
for(const entry of files) {
const avatar_id = entry.name.substr('avatar_'.length);
const unique_id = avatar_to_uid(avatar_id);
const tag = template_entry.renderTag({
username: 'loading',
unique_id: unique_id,
size: human_file_size(entry.size),
timestamp: moment(entry.datetime * 1000).format('YY-MM-DD HH:mm')
});
(username_resolve[unique_id] || (username_resolve[unique_id] = [])).push(name => {
const tag_username = tag.find(".column-username").empty();
if(name) {
tag_username.append(ClientEntry.chatTag(0, name, unique_id, false));
} else {
tag_username.text("unknown");
}
});
list_entries.append(tag);
tag.on('click', () => {
list_entries.find('.selected').removeClass('selected');
tag.addClass('selected');
set_selected_avatar(unique_id, avatar_id, entry.size);
});
}
if(container_list.hasScrollBar())
container_list.addClass("scrollbar");
client.serverConnection.command_helper.info_from_uid(...Object.keys(username_resolve)).then(result => {
for(const info of result) {
username_resolve[info.client_unique_id].forEach(e => e(info.client_nickname));
delete username_resolve[info.client_unique_id];
}
for(const uid of Object.keys(username_resolve)) {
(username_resolve[uid] || []).forEach(e => e(undefined));
}
}).catch(error => {
log.error(LogCategory.GENERAL, tr("Failed to fetch usernames from avatar names. Error: %o"), error);
createErrorModal(tr("Failed to fetch usernames"), tr("Failed to fetch usernames related to their avatar names"), undefined).open();
})
}).catch(error => {
//TODO: Display no perms error
log.error(LogCategory.GENERAL, tr("Failed to receive avatar list. Error: %o"), error);
createErrorModal(tr("Failed to list avatars"), tr("Failed to receive avatar list."), undefined).open();
});
};
button_download.on('click', () => (callback_download || (() => {}))());
button_delete.on('click', () => (callback_delete || (() => {}))());
setTimeout(() => update_avatar_list(), 250);
modal.open();
}

View File

@ -2,179 +2,183 @@
/// <reference path="../../ConnectionHandler.ts" />
/// <reference path="../../proto.ts" />
namespace Modals {
export type BanEntry = {
name?: string;
unique_id: string;
}
export function spawnBanClient(client: ConnectionHandler, entries: BanEntry | BanEntry[], callback: (data: {
length: number,
reason: string,
no_name: boolean,
no_ip: boolean,
no_hwid: boolean
}) => void) {
const max_ban_time = client.permissions.neededPermission(PermissionType.I_CLIENT_BAN_MAX_BANTIME).value;
import PermissionType from "tc-shared/permission/PermissionType";
import {ConnectionHandler} from "tc-shared/ConnectionHandler";
import {createModal} from "tc-shared/ui/elements/Modal";
import {duration_data} from "tc-shared/ui/modal/ModalBanList";
import * as tooltip from "tc-shared/ui/elements/Tooltip";
const permission_criteria_hwid = client.permissions.neededPermission(PermissionType.B_CLIENT_BAN_HWID).granted(1);
const permission_criteria_ip = client.permissions.neededPermission(PermissionType.B_CLIENT_BAN_IP).granted(1);
const permission_criteria_name = client.permissions.neededPermission(PermissionType.B_CLIENT_BAN_NAME).granted(1);
export type BanEntry = {
name?: string;
unique_id: string;
}
export function spawnBanClient(client: ConnectionHandler, entries: BanEntry | BanEntry[], callback: (data: {
length: number,
reason: string,
no_name: boolean,
no_ip: boolean,
no_hwid: boolean
}) => void) {
const max_ban_time = client.permissions.neededPermission(PermissionType.I_CLIENT_BAN_MAX_BANTIME).value;
const modal = createModal({
header: Array.isArray(entries) ? tr("Ban clients") : tr("Ban client"),
body: function () {
let template = $("#tmpl_client_ban").renderTag({entries: entries});
const permission_criteria_hwid = client.permissions.neededPermission(PermissionType.B_CLIENT_BAN_HWID).granted(1);
const permission_criteria_ip = client.permissions.neededPermission(PermissionType.B_CLIENT_BAN_IP).granted(1);
const permission_criteria_name = client.permissions.neededPermission(PermissionType.B_CLIENT_BAN_NAME).granted(1);
let update_duration;
let update_button_ok;
const button_ok = template.find(".button-apply");
const button_cancel = template.find(".button-cancel");
const modal = createModal({
header: Array.isArray(entries) ? tr("Ban clients") : tr("Ban client"),
body: function () {
let template = $("#tmpl_client_ban").renderTag({entries: entries});
const input_duration_value = template.find(".container-duration input").on('change keyup', () => update_duration());
const input_duration_type = template.find(".container-duration select").on('change keyup', () => update_duration());
let update_duration;
let update_button_ok;
const button_ok = template.find(".button-apply");
const button_cancel = template.find(".button-cancel");
const container_reason = template.find(".container-reason");
const input_duration_value = template.find(".container-duration input").on('change keyup', () => update_duration());
const input_duration_type = template.find(".container-duration select").on('change keyup', () => update_duration());
const criteria_nickname = template.find(".criteria.nickname input")
.prop('checked', permission_criteria_name).prop("disabled", !permission_criteria_name)
.firstParent(".checkbox").toggleClass("disabled", !permission_criteria_name);
const container_reason = template.find(".container-reason");
const criteria_ip_address = template.find(".criteria.ip-address input")
.prop('checked', permission_criteria_ip).prop("disabled", !permission_criteria_ip)
.firstParent(".checkbox").toggleClass("disabled", !permission_criteria_ip);
const criteria_nickname = template.find(".criteria.nickname input")
.prop('checked', permission_criteria_name).prop("disabled", !permission_criteria_name)
.firstParent(".checkbox").toggleClass("disabled", !permission_criteria_name);
const criteria_hardware_id = template.find(".criteria.hardware-id input")
.prop('checked', permission_criteria_hwid).prop("disabled", !permission_criteria_hwid)
.firstParent(".checkbox").toggleClass("disabled", !permission_criteria_hwid);
const criteria_ip_address = template.find(".criteria.ip-address input")
.prop('checked', permission_criteria_ip).prop("disabled", !permission_criteria_ip)
.firstParent(".checkbox").toggleClass("disabled", !permission_criteria_ip);
/* duration input handler */
{
const tooltip_duration_max = template.find(".tooltip-max-time a.max");
const criteria_hardware_id = template.find(".criteria.hardware-id input")
.prop('checked', permission_criteria_hwid).prop("disabled", !permission_criteria_hwid)
.firstParent(".checkbox").toggleClass("disabled", !permission_criteria_hwid);
update_duration = () => {
const type = input_duration_type.val() as string;
const value = parseInt(input_duration_value.val() as string);
const disabled = input_duration_type.prop("disabled");
/* duration input handler */
{
const tooltip_duration_max = template.find(".tooltip-max-time a.max");
input_duration_value.prop("disabled", type === "perm" || disabled).firstParent(".input-boxed").toggleClass("disabled", type === "perm" || disabled);
if(type !== "perm") {
if(input_duration_value.attr("x-saved-value")) {
input_duration_value.val(parseInt(input_duration_value.attr("x-saved-value")));
input_duration_value.attr("x-saved-value", null);
}
update_duration = () => {
const type = input_duration_type.val() as string;
const value = parseInt(input_duration_value.val() as string);
const disabled = input_duration_type.prop("disabled");
const selected_option = input_duration_type.find("option[value='" + type + "']");
const max = parseInt(selected_option.attr("duration-max"));
input_duration_value.prop("disabled", type === "perm" || disabled).firstParent(".input-boxed").toggleClass("disabled", type === "perm" || disabled);
if(type !== "perm") {
if(input_duration_value.attr("x-saved-value")) {
input_duration_value.val(parseInt(input_duration_value.attr("x-saved-value")));
input_duration_value.attr("x-saved-value", null);
}
input_duration_value.attr("max", max);
if((value > max && max != -1) || value < 1) {
input_duration_value.firstParent(".input-boxed").addClass("is-invalid");
} else {
input_duration_value.firstParent(".input-boxed").removeClass("is-invalid");
}
const selected_option = input_duration_type.find("option[value='" + type + "']");
const max = parseInt(selected_option.attr("duration-max"));
if(max != -1)
tooltip_duration_max.html(tr("You're allowed to ban a maximum of ") + "<b>" + max + " " + duration_data[type][max == 1 ? "1-text" : "text"] + "</b>");
else
tooltip_duration_max.html(tr("You're allowed to ban <b>permanent</b>."));
input_duration_value.attr("max", max);
if((value > max && max != -1) || value < 1) {
input_duration_value.firstParent(".input-boxed").addClass("is-invalid");
} else {
if(value && !Number.isNaN(value))
input_duration_value.attr("x-saved-value", value);
input_duration_value.attr("placeholder", tr("for ever")).val(null);
input_duration_value.firstParent(".input-boxed").removeClass("is-invalid");
}
if(max != -1)
tooltip_duration_max.html(tr("You're allowed to ban a maximum of ") + "<b>" + max + " " + duration_data[type][max == 1 ? "1-text" : "text"] + "</b>");
else
tooltip_duration_max.html(tr("You're allowed to ban <b>permanent</b>."));
}
update_button_ok && update_button_ok();
};
} else {
if(value && !Number.isNaN(value))
input_duration_value.attr("x-saved-value", value);
input_duration_value.attr("placeholder", tr("for ever")).val(null);
tooltip_duration_max.html(tr("You're allowed to ban <b>permanent</b>."));
}
update_button_ok && update_button_ok();
};
/* initialize ban time */
Promise.resolve(max_ban_time).catch(error => { /* TODO: Error handling? */ return 0; }).then(max_time => {
let unlimited = max_time == 0 || max_time == -1;
if(unlimited || typeof(max_time) === "undefined") max_time = 0;
/* initialize ban time */
Promise.resolve(max_ban_time).catch(error => { /* TODO: Error handling? */ return 0; }).then(max_time => {
let unlimited = max_time == 0 || max_time == -1;
if(unlimited || typeof(max_time) === "undefined") max_time = 0;
for(const value of Object.keys(duration_data)) {
input_duration_type.find("option[value='" + value + "']")
.prop("disabled", !unlimited && max_time >= duration_data[value].scale)
.attr("duration-scale", duration_data[value].scale)
.attr("duration-max", unlimited ? -1 : Math.floor(max_time / duration_data[value].scale));
}
input_duration_type.find("option[value='perm']")
.prop("disabled", !unlimited)
.attr("duration-scale", 0)
.attr("duration-max", -1);
update_duration();
});
for(const value of Object.keys(duration_data)) {
input_duration_type.find("option[value='" + value + "']")
.prop("disabled", !unlimited && max_time >= duration_data[value].scale)
.attr("duration-scale", duration_data[value].scale)
.attr("duration-max", unlimited ? -1 : Math.floor(max_time / duration_data[value].scale));
}
input_duration_type.find("option[value='perm']")
.prop("disabled", !unlimited)
.attr("duration-scale", 0)
.attr("duration-max", -1);
update_duration();
}
});
/* ban reason */
{
const input = container_reason.find("textarea");
update_duration();
}
const insert_tag = (open: string, close: string) => {
if(input.prop("disabled"))
return;
/* ban reason */
{
const input = container_reason.find("textarea");
const node = input[0] as HTMLTextAreaElement;
if (node.selectionStart || node.selectionStart == 0) {
const startPos = node.selectionStart;
const endPos = node.selectionEnd;
node.value = node.value.substring(0, startPos) + open + node.value.substring(startPos, endPos) + close + node.value.substring(endPos);
node.selectionEnd = endPos + open.length;
node.selectionStart = node.selectionEnd;
} else {
node.value += open + close;
node.selectionEnd = node.value.length - close.length;
node.selectionStart = node.selectionEnd;
}
const insert_tag = (open: string, close: string) => {
if(input.prop("disabled"))
return;
input.focus().trigger('change');
};
const node = input[0] as HTMLTextAreaElement;
if (node.selectionStart || node.selectionStart == 0) {
const startPos = node.selectionStart;
const endPos = node.selectionEnd;
node.value = node.value.substring(0, startPos) + open + node.value.substring(startPos, endPos) + close + node.value.substring(endPos);
node.selectionEnd = endPos + open.length;
node.selectionStart = node.selectionEnd;
} else {
node.value += open + close;
node.selectionEnd = node.value.length - close.length;
node.selectionStart = node.selectionEnd;
}
container_reason.find(".button-bold").on('click', () => insert_tag('[b]', '[/b]'));
container_reason.find(".button-italic").on('click', () => insert_tag('[i]', '[/i]'));
container_reason.find(".button-underline").on('click', () => insert_tag('[u]', '[/u]'));
container_reason.find(".button-color input").on('change', event => {
insert_tag('[color=' + (event.target as HTMLInputElement).value + ']', '[/color]')
input.focus().trigger('change');
};
container_reason.find(".button-bold").on('click', () => insert_tag('[b]', '[/b]'));
container_reason.find(".button-italic").on('click', () => insert_tag('[i]', '[/i]'));
container_reason.find(".button-underline").on('click', () => insert_tag('[u]', '[/u]'));
container_reason.find(".button-color input").on('change', event => {
insert_tag('[color=' + (event.target as HTMLInputElement).value + ']', '[/color]')
});
}
/* buttons */
{
button_cancel.on('click', event => modal.close());
button_ok.on('click', event => {
const duration = input_duration_type.val() === "perm" ? 0 : (1000 * parseInt(input_duration_type.find("option[value='" + input_duration_type.val() + "']").attr("duration-scale")) * parseInt(input_duration_value.val() as string));
modal.close();
callback({
length: Math.floor(duration / 1000),
reason: container_reason.find("textarea").val() as string,
no_hwid: !criteria_hardware_id.find("input").prop("checked"),
no_ip: !criteria_ip_address.find("input").prop("checked"),
no_name: !criteria_nickname.find("input").prop("checked")
});
}
});
/* buttons */
{
button_cancel.on('click', event => modal.close());
button_ok.on('click', event => {
const duration = input_duration_type.val() === "perm" ? 0 : (1000 * parseInt(input_duration_type.find("option[value='" + input_duration_type.val() + "']").attr("duration-scale")) * parseInt(input_duration_value.val() as string));
const inputs = template.find(".input-boxed");
update_button_ok = () => {
const invalid = [...inputs].find(e => $(e).hasClass("is-invalid"));
button_ok.prop('disabled', !!invalid);
};
update_button_ok();
}
modal.close();
callback({
length: Math.floor(duration / 1000),
reason: container_reason.find("textarea").val() as string,
tooltip.initialize(template);
return template.children();
},
footer: null,
no_hwid: !criteria_hardware_id.find("input").prop("checked"),
no_ip: !criteria_ip_address.find("input").prop("checked"),
no_name: !criteria_nickname.find("input").prop("checked")
});
});
min_width: "10em",
width: "30em"
});
modal.open();
const inputs = template.find(".input-boxed");
update_button_ok = () => {
const invalid = [...inputs].find(e => $(e).hasClass("is-invalid"));
button_ok.prop('disabled', !!invalid);
};
update_button_ok();
}
tooltip(template);
return template.children();
},
footer: null,
min_width: "10em",
width: "30em"
});
modal.open();
modal.htmlTag.find(".modal-body").addClass("modal-ban-client");
}
modal.htmlTag.find(".modal-body").addClass("modal-ban-client");
}

File diff suppressed because it is too large Load Diff

View File

@ -1,368 +1,383 @@
/// <reference path="../../ui/elements/modal.ts" />
/// <reference path="../../ConnectionHandler.ts" />
/// <reference path="../../proto.ts" />
import {createInputModal, createModal, Modal} from "tc-shared/ui/elements/Modal";
import {
Bookmark,
bookmarks,
BookmarkType, boorkmak_connect, create_bookmark, create_bookmark_directory,
delete_bookmark,
DirectoryBookmark,
save_bookmark
} from "tc-shared/bookmarks";
import {connection_log, Regex} from "tc-shared/ui/modal/ModalConnect";
import {IconManager} from "tc-shared/FileManager";
import {profiles} from "tc-shared/profiles/ConnectionProfile";
import {spawnYesNo} from "tc-shared/ui/modal/ModalYesNo";
import {Settings, settings} from "tc-shared/settings";
import {LogCategory} from "tc-shared/log";
import * as log from "tc-shared/log";
import * as i18nc from "tc-shared/i18n/country";
import {formatMessage} from "tc-shared/ui/frames/chat";
import {control_bar} from "tc-shared/ui/frames/ControlBar";
import * as top_menu from "../frames/MenuBar";
namespace Modals {
export function spawnBookmarkModal() {
let modal: Modal;
modal = createModal({
header: tr("Manage bookmarks"),
body: () => {
let template = $("#tmpl_manage_bookmarks").renderTag({ });
let selected_bookmark: bookmarks.Bookmark | bookmarks.DirectoryBookmark | undefined;
export function spawnBookmarkModal() {
let modal: Modal;
modal = createModal({
header: tr("Manage bookmarks"),
body: () => {
let template = $("#tmpl_manage_bookmarks").renderTag({ });
let selected_bookmark: Bookmark | DirectoryBookmark | undefined;
const button_delete = template.find(".button-delete");
const button_add_folder = template.find(".button-add-folder");
const button_add_bookmark = template.find(".button-add-bookmark");
const button_delete = template.find(".button-delete");
const button_add_folder = template.find(".button-add-folder");
const button_add_bookmark = template.find(".button-add-bookmark");
const button_connect = template.find(".button-connect");
const button_connect_tab = template.find(".button-connect-tab");
const button_connect = template.find(".button-connect");
const button_connect_tab = template.find(".button-connect-tab");
const label_bookmark_name = template.find(".header .container-name");
const label_server_address = template.find(".header .container-address");
const label_bookmark_name = template.find(".header .container-name");
const label_server_address = template.find(".header .container-address");
const input_bookmark_name = template.find(".input-bookmark-name");
const input_connect_profile = template.find(".input-connect-profile");
const input_bookmark_name = template.find(".input-bookmark-name");
const input_connect_profile = template.find(".input-connect-profile");
const input_server_address = template.find(".input-server-address");
const input_server_password = template.find(".input-server-password");
const input_server_address = template.find(".input-server-address");
const input_server_password = template.find(".input-server-password");
const label_server_name = template.find(".server-name");
const label_server_region = template.find(".server-region");
const label_last_ping = template.find(".server-ping");
const label_client_count = template.find(".server-client-count");
const label_connection_count = template.find(".server-connection-count");
const label_server_name = template.find(".server-name");
const label_server_region = template.find(".server-region");
const label_last_ping = template.find(".server-ping");
const label_client_count = template.find(".server-client-count");
const label_connection_count = template.find(".server-connection-count");
const update_buttons = () => {
button_delete.prop("disabled", !selected_bookmark);
button_connect.prop("disabled", !selected_bookmark || selected_bookmark.type !== bookmarks.BookmarkType.ENTRY);
button_connect_tab.prop("disabled", !selected_bookmark || selected_bookmark.type !== bookmarks.BookmarkType.ENTRY);
};
const update_buttons = () => {
button_delete.prop("disabled", !selected_bookmark);
button_connect.prop("disabled", !selected_bookmark || selected_bookmark.type !== BookmarkType.ENTRY);
button_connect_tab.prop("disabled", !selected_bookmark || selected_bookmark.type !== BookmarkType.ENTRY);
};
const update_connect_info = () => {
if(selected_bookmark && selected_bookmark.type === bookmarks.BookmarkType.ENTRY) {
const entry = selected_bookmark as bookmarks.Bookmark;
const update_connect_info = () => {
if(selected_bookmark && selected_bookmark.type === BookmarkType.ENTRY) {
const entry = selected_bookmark as Bookmark;
const history = connection_log.history().find(e => e.address.hostname === entry.server_properties.server_address && e.address.port === entry.server_properties.server_port);
if(history) {
label_server_name.text(history.name);
label_server_region.empty().append(
$.spawn("div").addClass("country flag-" + history.country.toLowerCase()),
$.spawn("div").text(i18n.country_name(history.country, tr("Global")))
);
label_client_count.text(history.clients_online + "/" + history.clients_total);
label_connection_count.empty().append(
...MessageHelper.formatMessage(tr("You've connected {} times"), $.spawn("div").addClass("connect-count").text(history.total_connection))
);
} else {
label_server_name.text(tr("Unknown"));
label_server_region.empty().text(tr("Unknown"));
label_client_count.text(tr("Unknown"));
label_connection_count.empty().append(
...MessageHelper.formatMessage(tr("You {} connected to that server address"), $.spawn("div").addClass("connect-never").text("never"))
);
}
label_last_ping.text(tr("Average ping isn't yet supported"));
} else {
label_server_name.text("--");
label_server_region.text("--");
label_last_ping.text("--");
label_client_count.text("--");
label_connection_count.text("--");
}
};
const update_selected = () => {
input_bookmark_name.prop("disabled", !selected_bookmark);
input_connect_profile.prop("disabled", !selected_bookmark || selected_bookmark.type !== bookmarks.BookmarkType.ENTRY);
input_server_address.prop("disabled", !selected_bookmark || selected_bookmark.type !== bookmarks.BookmarkType.ENTRY);
input_server_password.prop("disabled", !selected_bookmark || selected_bookmark.type !== bookmarks.BookmarkType.ENTRY);
if(selected_bookmark) {
input_bookmark_name.val(selected_bookmark.display_name);
label_bookmark_name.text(selected_bookmark.display_name);
}
if(selected_bookmark && selected_bookmark.type === bookmarks.BookmarkType.ENTRY) {
const entry = selected_bookmark as bookmarks.Bookmark;
const address = entry.server_properties.server_address + (entry.server_properties.server_port == 9987 ? "" : (" " + entry.server_properties.server_port));
label_server_address.text(address);
input_server_address.val(address);
let profile = input_connect_profile.find("option[value='" + entry.connect_profile + "']");
if(profile.length == 0)
profile = input_connect_profile.find("option[value=default]");
profile.prop("selected", true);
input_server_password.val(entry.server_properties.server_password_hash || entry.server_properties.server_password ? "WolverinDEV" : "");
} else {
input_server_password.val("");
input_server_address.val("");
input_connect_profile.find("option[value='no-value']").prop('selected', true);
label_server_address.text(" ");
}
update_connect_info();
};
const container_bookmarks = template.find(".container-bookmarks");
const update_bookmark_list = (_current_selected: string) => {
container_bookmarks.empty();
selected_bookmark = undefined;
update_selected();
const hide_links: boolean[] = [];
const build_entry = (entry: bookmarks.Bookmark | bookmarks.DirectoryBookmark, sibling_data: {first: boolean; last: boolean;}, index: number) => {
let container = $.spawn("div")
.addClass(entry.type === bookmarks.BookmarkType.ENTRY ? "bookmark" : "directory")
.addClass(index > 0 ? "linked" : "")
.addClass(sibling_data.first ? "link-start" : "");
for (let i = 0; i < index; i++) {
container.append(
$.spawn("div")
.addClass("link")
.addClass(i + 1 === index ? " connected" : "")
.addClass(hide_links[i + 1] ? "hidden" : "")
);
}
if (entry.type === bookmarks.BookmarkType.ENTRY) {
const bookmark = entry as bookmarks.Bookmark;
container.append(
bookmark.last_icon_id ?
IconManager.generate_tag(IconManager.load_cached_icon(bookmark.last_icon_id || 0), {animate: false}) :
$.spawn("div").addClass("icon-container icon_em")
);
} else {
container.append(
$.spawn("div").addClass("icon-container icon_em client-folder")
);
}
container.append(
$.spawn("div").addClass("name").attr("title", entry.display_name).text(entry.display_name)
const history = connection_log.history().find(e => e.address.hostname === entry.server_properties.server_address && e.address.port === entry.server_properties.server_port);
if(history) {
label_server_name.text(history.name);
label_server_region.empty().append(
$.spawn("div").addClass("country flag-" + history.country.toLowerCase()),
$.spawn("div").text(i18nc.country_name(history.country, tr("Global")))
);
label_client_count.text(history.clients_online + "/" + history.clients_total);
label_connection_count.empty().append(
...formatMessage(tr("You've connected {} times"), $.spawn("div").addClass("connect-count").text(history.total_connection))
);
} else {
label_server_name.text(tr("Unknown"));
label_server_region.empty().text(tr("Unknown"));
label_client_count.text(tr("Unknown"));
label_connection_count.empty().append(
...formatMessage(tr("You {} connected to that server address"), $.spawn("div").addClass("connect-never").text("never"))
);
}
label_last_ping.text(tr("Average ping isn't yet supported"));
} else {
label_server_name.text("--");
label_server_region.text("--");
label_last_ping.text("--");
label_client_count.text("--");
label_connection_count.text("--");
}
};
container.appendTo(container_bookmarks);
container.on('click', event => {
if(selected_bookmark === entry)
return;
const update_selected = () => {
input_bookmark_name.prop("disabled", !selected_bookmark);
input_connect_profile.prop("disabled", !selected_bookmark || selected_bookmark.type !== BookmarkType.ENTRY);
input_server_address.prop("disabled", !selected_bookmark || selected_bookmark.type !== BookmarkType.ENTRY);
input_server_password.prop("disabled", !selected_bookmark || selected_bookmark.type !== BookmarkType.ENTRY);
selected_bookmark = entry;
container_bookmarks.find(".selected").removeClass("selected");
container.addClass("selected");
update_buttons();
update_selected();
});
if(entry.unique_id === _current_selected)
container.trigger('click');
if(selected_bookmark) {
input_bookmark_name.val(selected_bookmark.display_name);
label_bookmark_name.text(selected_bookmark.display_name);
}
hide_links.push(sibling_data.last);
let cindex = 0;
const children = (entry as bookmarks.DirectoryBookmark).content || [];
for (const child of children)
build_entry(child, {first: cindex++ == 0, last: cindex == children.length}, index + 1);
hide_links.pop();
};
if(selected_bookmark && selected_bookmark.type === BookmarkType.ENTRY) {
const entry = selected_bookmark as Bookmark;
const address = entry.server_properties.server_address + (entry.server_properties.server_port == 9987 ? "" : (" " + entry.server_properties.server_port));
label_server_address.text(address);
input_server_address.val(address);
let profile = input_connect_profile.find("option[value='" + entry.connect_profile + "']");
if(profile.length == 0)
profile = input_connect_profile.find("option[value=default]");
profile.prop("selected", true);
input_server_password.val(entry.server_properties.server_password_hash || entry.server_properties.server_password ? "WolverinDEV" : "");
} else {
input_server_password.val("");
input_server_address.val("");
input_connect_profile.find("option[value='no-value']").prop('selected', true);
label_server_address.text(" ");
}
update_connect_info();
};
const container_bookmarks = template.find(".container-bookmarks");
const update_bookmark_list = (_current_selected: string) => {
container_bookmarks.empty();
selected_bookmark = undefined;
update_selected();
const hide_links: boolean[] = [];
const build_entry = (entry: Bookmark | DirectoryBookmark, sibling_data: {first: boolean; last: boolean;}, index: number) => {
let container = $.spawn("div")
.addClass(entry.type === BookmarkType.ENTRY ? "bookmark" : "directory")
.addClass(index > 0 ? "linked" : "")
.addClass(sibling_data.first ? "link-start" : "");
for (let i = 0; i < index; i++) {
container.append(
$.spawn("div")
.addClass("link")
.addClass(i + 1 === index ? " connected" : "")
.addClass(hide_links[i + 1] ? "hidden" : "")
);
}
if (entry.type === BookmarkType.ENTRY) {
const bookmark = entry as Bookmark;
container.append(
bookmark.last_icon_id ?
IconManager.generate_tag(IconManager.load_cached_icon(bookmark.last_icon_id || 0), {animate: false}) :
$.spawn("div").addClass("icon-container icon_em")
);
} else {
container.append(
$.spawn("div").addClass("icon-container icon_em client-folder")
);
}
container.append(
$.spawn("div").addClass("name").attr("title", entry.display_name).text(entry.display_name)
);
container.appendTo(container_bookmarks);
container.on('click', event => {
if(selected_bookmark === entry)
return;
selected_bookmark = entry;
container_bookmarks.find(".selected").removeClass("selected");
container.addClass("selected");
update_buttons();
update_selected();
});
if(entry.unique_id === _current_selected)
container.trigger('click');
hide_links.push(sibling_data.last);
let cindex = 0;
const children = bookmarks.bookmarks().content;
for (const bookmark of children)
build_entry(bookmark, {first: cindex++ == 0, last: cindex == children.length}, 0);
const children = (entry as DirectoryBookmark).content || [];
for (const child of children)
build_entry(child, {first: cindex++ == 0, last: cindex == children.length}, index + 1);
hide_links.pop();
};
/* generate profile list */
{
let cindex = 0;
const children = bookmarks().content;
for (const bookmark of children)
build_entry(bookmark, {first: cindex++ == 0, last: cindex == children.length}, 0);
};
/* generate profile list */
{
input_connect_profile.append(
$.spawn("option")
.attr("value", "no-value")
.text("")
.css("display", "none")
);
for(const profile of profiles()) {
input_connect_profile.append(
$.spawn("option")
.attr("value", "no-value")
.text("")
.css("display", "none")
.attr("value", profile.id)
.text(profile.profile_name)
);
for(const profile of profiles.profiles()) {
input_connect_profile.append(
$.spawn("option")
.attr("value", profile.id)
.text(profile.profile_name)
);
}
}
/* buttons */
{
button_delete.on('click', event => {
if(!selected_bookmark) return;
if(selected_bookmark.type === BookmarkType.DIRECTORY && (selected_bookmark as DirectoryBookmark).content.length > 0) {
spawnYesNo(tr("Are you sure"), tr("Do you really want to delete this non empty directory?"), answer => {
if(answer) {
delete_bookmark(selected_bookmark);
save_bookmark(selected_bookmark);
update_bookmark_list(undefined);
}
});
} else {
delete_bookmark(selected_bookmark);
save_bookmark(selected_bookmark);
update_bookmark_list(undefined);
}
}
/* buttons */
{
button_delete.on('click', event => {
if(!selected_bookmark) return;
if(selected_bookmark.type === bookmarks.BookmarkType.DIRECTORY && (selected_bookmark as bookmarks.DirectoryBookmark).content.length > 0) {
Modals.spawnYesNo(tr("Are you sure"), tr("Do you really want to delete this non empty directory?"), answer => {
if(answer) {
bookmarks.delete_bookmark(selected_bookmark);
bookmarks.save_bookmark(selected_bookmark);
update_bookmark_list(undefined);
}
});
} else {
bookmarks.delete_bookmark(selected_bookmark);
bookmarks.save_bookmark(selected_bookmark);
update_bookmark_list(undefined);
}
});
button_add_folder.on('click', event => {
createInputModal(tr("Enter a folder name"), tr("Enter the folder name"), text => {
return true;
}, result => {
if(result) {
const mark = bookmarks.create_bookmark_directory(
selected_bookmark ?
selected_bookmark.type === bookmarks.BookmarkType.DIRECTORY ?
selected_bookmark as bookmarks.DirectoryBookmark :
selected_bookmark.parent :
bookmarks.bookmarks(),
result as string
);
bookmarks.save_bookmark(mark);
update_bookmark_list(mark.unique_id);
}
}).open();
});
button_add_bookmark.on('click', event => {
createInputModal(tr("Enter a bookmark name"), tr("Enter the bookmark name"), text => {
return true;
}, result => {
if(result) {
const mark = bookmarks.create_bookmark(result as string,
selected_bookmark ?
selected_bookmark.type === bookmarks.BookmarkType.DIRECTORY ?
selected_bookmark as bookmarks.DirectoryBookmark :
selected_bookmark.parent :
bookmarks.bookmarks(), {
server_password: "",
server_port: 9987,
server_address: "",
server_password_hash: ""
}, "");
bookmarks.save_bookmark(mark);
update_bookmark_list(mark.unique_id);
}
}).open();
});
button_connect_tab.on('click', event => {
bookmarks.boorkmak_connect(selected_bookmark as bookmarks.Bookmark, true);
modal.close();
}).toggle(!settings.static_global(Settings.KEY_DISABLE_MULTI_SESSION));
button_connect.on('click', event => {
bookmarks.boorkmak_connect(selected_bookmark as bookmarks.Bookmark, false);
modal.close();
});
}
/* inputs */
{
input_bookmark_name.on('change keydown', event => {
const name = input_bookmark_name.val() as string;
const valid = name.length > 3;
input_bookmark_name.firstParent(".input-boxed").toggleClass("is-invalid", !valid);
if(event.type === "change" && valid) {
selected_bookmark.display_name = name;
label_bookmark_name.text(name);
}
});
input_server_address.on('change keydown', event => {
const address = input_server_address.val() as string;
const valid = !!address.match(Regex.IP_V4) || !!address.match(Regex.IP_V6) || !!address.match(Regex.DOMAIN);
input_server_address.firstParent(".input-boxed").toggleClass("is-invalid", !valid);
if(valid) {
const entry = selected_bookmark as bookmarks.Bookmark;
let _v6_end = address.indexOf(']');
let idx = address.lastIndexOf(':');
if(idx != -1 && idx > _v6_end) {
entry.server_properties.server_port = parseInt(address.substr(idx + 1));
entry.server_properties.server_address = address.substr(0, idx);
} else {
entry.server_properties.server_address = address;
entry.server_properties.server_port = 9987;
}
label_server_address.text(entry.server_properties.server_address + (entry.server_properties.server_port == 9987 ? "" : (" " + entry.server_properties.server_port)));
update_connect_info();
}
});
input_connect_profile.on('change', event => {
const id = input_connect_profile.val() as string;
const profile = profiles.profiles().find(e => e.id === id);
if(profile) {
(selected_bookmark as bookmarks.Bookmark).connect_profile = id;
} else {
log.warn(LogCategory.GENERAL, tr("Failed to change connect profile for profile %s to %s"), selected_bookmark.unique_id, id);
}
})
}
/* Arrow key navigation for the bookmark list */
{
let _focused = false;
let _focus_listener;
let _key_listener;
_focus_listener = event => {
_focused = false;
let element = event.target as HTMLElement;
while(element) {
if(element === container_bookmarks[0]) {
_focused = true;
break;
}
element = element.parentNode as HTMLElement;
}
};
_key_listener = event => {
if(!_focused) return;
if(event.key.toLowerCase() === "arrowdown") {
container_bookmarks.find(".selected").next().trigger('click');
} else if(event.key.toLowerCase() === "arrowup") {
container_bookmarks.find(".selected").prev().trigger('click');
}
};
document.addEventListener('click', _focus_listener);
document.addEventListener('keydown', _key_listener);
modal.close_listener.push(() => {
document.removeEventListener('click', _focus_listener);
document.removeEventListener('keydown', _key_listener);
})
}
update_bookmark_list(undefined);
update_buttons();
template.find(".container-bookmarks").on('keydown', event => {
console.error(event.key);
});
template.find(".button-close").on('click', event => modal.close());
return template.children();
},
footer: undefined,
width: "40em"
});
modal.htmlTag.dividerfy().find(".modal-body").addClass("modal-bookmarks");
modal.close_listener.push(() => {
control_bar.update_bookmarks();
top_menu.rebuild_bookmarks();
});
button_add_folder.on('click', event => {
createInputModal(tr("Enter a folder name"), tr("Enter the folder name"), text => {
return true;
}, result => {
if(result) {
const mark = create_bookmark_directory(
selected_bookmark ?
selected_bookmark.type === BookmarkType.DIRECTORY ?
selected_bookmark as DirectoryBookmark :
selected_bookmark.parent :
bookmarks(),
result as string
);
save_bookmark(mark);
update_bookmark_list(mark.unique_id);
}
}).open();
});
modal.open();
}
button_add_bookmark.on('click', event => {
createInputModal(tr("Enter a bookmark name"), tr("Enter the bookmark name"), text => {
return true;
}, result => {
if(result) {
const mark = create_bookmark(result as string,
selected_bookmark ?
selected_bookmark.type === BookmarkType.DIRECTORY ?
selected_bookmark as DirectoryBookmark :
selected_bookmark.parent :
bookmarks(), {
server_password: "",
server_port: 9987,
server_address: "",
server_password_hash: ""
}, "");
save_bookmark(mark);
update_bookmark_list(mark.unique_id);
}
}).open();
});
button_connect_tab.on('click', event => {
boorkmak_connect(selected_bookmark as Bookmark, true);
modal.close();
}).toggle(!settings.static_global(Settings.KEY_DISABLE_MULTI_SESSION));
button_connect.on('click', event => {
boorkmak_connect(selected_bookmark as Bookmark, false);
modal.close();
});
}
/* inputs */
{
input_bookmark_name.on('change keydown', event => {
const name = input_bookmark_name.val() as string;
const valid = name.length > 3;
input_bookmark_name.firstParent(".input-boxed").toggleClass("is-invalid", !valid);
if(event.type === "change" && valid) {
selected_bookmark.display_name = name;
label_bookmark_name.text(name);
}
});
input_server_address.on('change keydown', event => {
const address = input_server_address.val() as string;
const valid = !!address.match(Regex.IP_V4) || !!address.match(Regex.IP_V6) || !!address.match(Regex.DOMAIN);
input_server_address.firstParent(".input-boxed").toggleClass("is-invalid", !valid);
if(valid) {
const entry = selected_bookmark as Bookmark;
let _v6_end = address.indexOf(']');
let idx = address.lastIndexOf(':');
if(idx != -1 && idx > _v6_end) {
entry.server_properties.server_port = parseInt(address.substr(idx + 1));
entry.server_properties.server_address = address.substr(0, idx);
} else {
entry.server_properties.server_address = address;
entry.server_properties.server_port = 9987;
}
label_server_address.text(entry.server_properties.server_address + (entry.server_properties.server_port == 9987 ? "" : (" " + entry.server_properties.server_port)));
update_connect_info();
}
});
input_connect_profile.on('change', event => {
const id = input_connect_profile.val() as string;
const profile = profiles().find(e => e.id === id);
if(profile) {
(selected_bookmark as Bookmark).connect_profile = id;
} else {
log.warn(LogCategory.GENERAL, tr("Failed to change connect profile for profile %s to %s"), selected_bookmark.unique_id, id);
}
})
}
/* Arrow key navigation for the bookmark list */
{
let _focused = false;
let _focus_listener;
let _key_listener;
_focus_listener = event => {
_focused = false;
let element = event.target as HTMLElement;
while(element) {
if(element === container_bookmarks[0]) {
_focused = true;
break;
}
element = element.parentNode as HTMLElement;
}
};
_key_listener = event => {
if(!_focused) return;
if(event.key.toLowerCase() === "arrowdown") {
container_bookmarks.find(".selected").next().trigger('click');
} else if(event.key.toLowerCase() === "arrowup") {
container_bookmarks.find(".selected").prev().trigger('click');
}
};
document.addEventListener('click', _focus_listener);
document.addEventListener('keydown', _key_listener);
modal.close_listener.push(() => {
document.removeEventListener('click', _focus_listener);
document.removeEventListener('keydown', _key_listener);
})
}
update_bookmark_list(undefined);
update_buttons();
template.find(".container-bookmarks").on('keydown', event => {
console.error(event.key);
});
template.find(".button-close").on('click', event => modal.close());
return template.children();
},
footer: undefined,
width: "40em"
});
modal.htmlTag.dividerfy().find(".modal-body").addClass("modal-bookmarks");
modal.close_listener.push(() => {
control_bar.update_bookmarks();
top_menu.rebuild_bookmarks();
});
modal.open();
}

View File

@ -1,114 +1,115 @@
/// <reference path="../../ui/elements/modal.ts" />
/// <reference path="../../ConnectionHandler.ts" />
/// <reference path="../../proto.ts" />
import {createModal, Modal} from "tc-shared/ui/elements/Modal";
import {ClientEntry} from "tc-shared/ui/client";
import {voice} from "tc-shared/connection/ConnectionBase";
import LatencySettings = voice.LatencySettings;
import {Slider, sliderfy} from "tc-shared/ui/elements/Slider";
import * as htmltags from "tc-shared/ui/htmltags";
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();
let modal: Modal;
export function spawnChangeLatency(client: ClientEntry, current: LatencySettings, reset: () => LatencySettings, apply: (settings: LatencySettings) => any, callback_flush?: () => any) {
if(modal) modal.close();
const begin = Object.assign({}, current);
current = Object.assign({}, current);
const begin = Object.assign({}, current);
current = Object.assign({}, current);
modal = createModal({
header: tr("Change playback latency"),
body: function () {
let tag = $("#tmpl_change_latency").renderTag({
client: htmltags.generate_client_object({
add_braces: false,
client_name: client.clientNickName(),
client_unique_id: client.properties.client_unique_identifier,
client_id: client.clientId()
}),
modal = createModal({
header: tr("Change playback latency"),
body: function () {
let tag = $("#tmpl_change_latency").renderTag({
client: htmltags.generate_client_object({
add_braces: false,
client_name: client.clientNickName(),
client_unique_id: client.properties.client_unique_identifier,
client_id: client.clientId()
}),
have_flush: (typeof(callback_flush) === "function")
have_flush: (typeof(callback_flush) === "function")
});
const update_value = () => {
const valid = current.min_buffer < current.max_buffer;
modal.htmlTag.find(".modal-body").toggleClass("modal-red", !valid);
modal.htmlTag.find(".modal-body").toggleClass("modal-green", valid);
if(!valid)
return;
apply(current);
};
let slider_min: Slider, slider_max: Slider;
{
const container = tag.find(".container-min");
const tag_value = container.find(".value");
const slider_tag = container.find(".container-slider");
slider_min = sliderfy(slider_tag, {
initial_value: current.min_buffer,
step: 20,
max_value: 1000,
min_value: 0,
unit: 'ms'
});
const update_value = () => {
const valid = current.min_buffer < current.max_buffer;
modal.htmlTag.find(".modal-body").toggleClass("modal-red", !valid);
modal.htmlTag.find(".modal-body").toggleClass("modal-green", valid);
if(!valid)
return;
apply(current);
};
let slider_min: Slider, slider_max: Slider;
{
const container = tag.find(".container-min");
const tag_value = container.find(".value");
const slider_tag = container.find(".container-slider");
slider_min = sliderfy(slider_tag, {
initial_value: current.min_buffer,
step: 20,
max_value: 1000,
min_value: 0,
unit: 'ms'
});
slider_tag.on('change', event => {
current.min_buffer = parseInt(slider_tag.attr("value"));
tag_value.text(current.min_buffer + "ms");
update_value();
});
slider_tag.on('change', event => {
current.min_buffer = parseInt(slider_tag.attr("value"));
tag_value.text(current.min_buffer + "ms");
}
update_value();
});
{
const container = tag.find(".container-max");
const tag_value = container.find(".value");
tag_value.text(current.min_buffer + "ms");
}
const slider_tag = container.find(".container-slider");
slider_max = sliderfy(slider_tag, {
initial_value: current.max_buffer,
step: 20,
max_value: 1020,
min_value: 20,
{
const container = tag.find(".container-max");
const tag_value = container.find(".value");
unit: 'ms'
});
const slider_tag = container.find(".container-slider");
slider_max = sliderfy(slider_tag, {
initial_value: current.max_buffer,
step: 20,
max_value: 1020,
min_value: 20,
slider_tag.on('change', event => {
current.max_buffer = parseInt(slider_tag.attr("value"));
tag_value.text(current.max_buffer + "ms");
update_value();
});
unit: 'ms'
});
slider_tag.on('change', event => {
current.max_buffer = parseInt(slider_tag.attr("value"));
tag_value.text(current.max_buffer + "ms");
}
setTimeout(update_value, 0);
tag.find(".button-close").on('click', event => {
modal.close();
update_value();
});
tag.find(".button-cancel").on('click', event => {
apply(begin);
modal.close();
});
tag_value.text(current.max_buffer + "ms");
}
setTimeout(update_value, 0);
tag.find(".button-reset").on('click', event => {
current = Object.assign({}, reset());
slider_max.value(current.max_buffer);
slider_min.value(current.min_buffer);
});
tag.find(".button-close").on('click', event => {
modal.close();
});
tag.find(".button-flush").on('click', event => callback_flush());
tag.find(".button-cancel").on('click', event => {
apply(begin);
modal.close();
});
return tag.children();
},
footer: null,
tag.find(".button-reset").on('click', event => {
current = Object.assign({}, reset());
slider_max.value(current.max_buffer);
slider_min.value(current.min_buffer);
});
width: 600
});
tag.find(".button-flush").on('click', event => callback_flush());
modal.close_listener.push(() => modal = undefined);
modal.open();
modal.htmlTag.find(".modal-body").addClass("modal-latency");
}
return tag.children();
},
footer: null,
width: 600
});
modal.close_listener.push(() => modal = undefined);
modal.open();
modal.htmlTag.find(".modal-body").addClass("modal-latency");
}

View File

@ -1,77 +1,76 @@
/// <reference path="../../ui/elements/modal.ts" />
/// <reference path="../../ConnectionHandler.ts" />
/// <reference path="../../proto.ts" />
//TODO: Use the max limit!
namespace Modals {
//TODO: Use the max limit!
import {sliderfy} from "tc-shared/ui/elements/Slider";
import {createModal, Modal} from "tc-shared/ui/elements/Modal";
import {ClientEntry} from "tc-shared/ui/client";
import * as htmltags from "tc-shared/ui/htmltags";
let modal: Modal;
export function spawnChangeVolume(client: ClientEntry, local: boolean, current: number, max: number | undefined, callback: (number) => void) {
if(modal) modal.close();
let modal: Modal;
export function spawnChangeVolume(client: ClientEntry, local: boolean, current: number, max: number | undefined, callback: (number) => void) {
if(modal) modal.close();
let new_value: number;
modal = createModal({
header: local ? tr("Change local volume") : tr("Change remote volume"),
body: function () {
let tag = $("#tmpl_change_volume").renderTag({
client: htmltags.generate_client_object({
add_braces: false,
client_name: client.clientNickName(),
client_unique_id: client.properties.client_unique_identifier,
client_id: client.clientId()
}),
local: local
});
let new_value: number;
modal = createModal({
header: local ? tr("Change local volume") : tr("Change remote volume"),
body: function () {
let tag = $("#tmpl_change_volume").renderTag({
client: htmltags.generate_client_object({
add_braces: false,
client_name: client.clientNickName(),
client_unique_id: client.properties.client_unique_identifier,
client_id: client.clientId()
}),
local: local
});
const container_value = tag.find(".info .value");
const set_value = value => {
const number = value > 100 ? value - 100 : 100 - value;
container_value.html((value == 100 ? "&plusmn;" : value > 100 ? "+" : "-") + number + "%");
const container_value = tag.find(".info .value");
const set_value = value => {
const number = value > 100 ? value - 100 : 100 - value;
container_value.html((value == 100 ? "&plusmn;" : value > 100 ? "+" : "-") + number + "%");
new_value = value / 100;
if(local) callback(new_value);
};
set_value(current * 100);
new_value = value / 100;
if(local) callback(new_value);
};
set_value(current * 100);
const slider_tag = tag.find(".container-slider");
const slider = sliderfy(slider_tag, {
initial_value: current * 100,
step: 1,
max_value: 200,
min_value: 0,
const slider_tag = tag.find(".container-slider");
const slider = sliderfy(slider_tag, {
initial_value: current * 100,
step: 1,
max_value: 200,
min_value: 0,
unit: '%'
});
slider_tag.on('change', event => set_value(parseInt(slider_tag.attr("value"))));
unit: '%'
});
slider_tag.on('change', event => set_value(parseInt(slider_tag.attr("value"))));
tag.find(".button-save").on('click', event => {
if(typeof(new_value) !== "undefined") callback(new_value);
modal.close();
});
tag.find(".button-save").on('click', event => {
if(typeof(new_value) !== "undefined") callback(new_value);
modal.close();
});
tag.find(".button-cancel").on('click', event => {
callback(current);
modal.close();
});
tag.find(".button-cancel").on('click', event => {
callback(current);
modal.close();
});
tag.find(".button-reset").on('click', event => {
slider.value(100);
});
tag.find(".button-reset").on('click', event => {
slider.value(100);
});
tag.find(".button-apply").on('click', event => {
callback(new_value);
new_value = undefined;
});
tag.find(".button-apply").on('click', event => {
callback(new_value);
new_value = undefined;
});
return tag.children();
},
footer: null,
return tag.children();
},
footer: null,
width: 600
});
width: 600
});
modal.close_listener.push(() => modal = undefined);
modal.open();
modal.htmlTag.find(".modal-body").addClass("modal-volume");
}
modal.close_listener.push(() => modal = undefined);
modal.open();
modal.htmlTag.find(".modal-body").addClass("modal-volume");
}

View File

@ -1,152 +1,157 @@
namespace Modals {
export function openChannelInfo(channel: ChannelEntry) {
let modal: Modal;
import {createInfoModal, createModal, Modal} from "tc-shared/ui/elements/Modal";
import {ChannelEntry} from "tc-shared/ui/channel";
import {copy_to_clipboard} from "tc-shared/utils/helpers";
import * as tooltip from "tc-shared/ui/elements/Tooltip";
import {formatMessage} from "tc-shared/ui/frames/chat";
modal = createModal({
header: tr("Channel information: ") + channel.channelName(),
body: () => {
const template = $("#tmpl_channel_info").renderTag();
export function openChannelInfo(channel: ChannelEntry) {
let modal: Modal;
const update_values = (container) => {
modal = createModal({
header: tr("Channel information: ") + channel.channelName(),
body: () => {
const template = $("#tmpl_channel_info").renderTag();
apply_channel_description(container.find(".container-description"), channel);
apply_general(container, channel);
};
const update_values = (container) => {
template.find(".button-copy").on('click', event => {
copy_to_clipboard(channel.properties.channel_description);
createInfoModal(tr("Description copied"), tr("The channel description has been copied to your clipboard!")).open();
});
apply_channel_description(container.find(".container-description"), channel);
apply_general(container, channel);
};
const button_update = template.find(".button-update");
button_update.on('click', event => update_values(modal.htmlTag));
template.find(".button-copy").on('click', event => {
copy_to_clipboard(channel.properties.channel_description);
createInfoModal(tr("Description copied"), tr("The channel description has been copied to your clipboard!")).open();
});
update_values(template);
tooltip(template);
return template.children();
},
footer: null,
width: "65em"
});
modal.htmlTag.find(".button-close").on('click', event => modal.close());
modal.htmlTag.find(".modal-body").addClass("modal-channel-info");
modal.open();
const button_update = template.find(".button-update");
button_update.on('click', event => update_values(modal.htmlTag));
update_values(template);
tooltip.initialize(template);
return template.children();
},
footer: null,
width: "65em"
});
modal.htmlTag.find(".button-close").on('click', event => modal.close());
modal.htmlTag.find(".modal-body").addClass("modal-channel-info");
modal.open();
}
declare const xbbcode;
function apply_channel_description(container: JQuery, channel: ChannelEntry) {
const container_value = container.find(".value");
const container_no_value = container.find(".no-value");
channel.getChannelDescription().then(description => {
if(description) {
const result = xbbcode.parse(description, {});
container_value[0].innerHTML = result.build_html();
container_no_value.hide();
container_value.show();
} else {
container_no_value.text(tr("Channel has no description"));
}
});
container_value.hide();
container_no_value.text(tr("loading...")).show();
}
const codec_names = [
tr("Speex Narrowband"),
tr("Speex Wideband"),
tr("Speex Ultra-Wideband"),
tr("CELT Mono"),
tr("Opus Voice"),
tr("Opus Music")
];
function apply_general(container: JQuery, channel: ChannelEntry) {
/* channel type */
{
const tag = container.find(".channel-type .value").empty();
if(channel.properties.channel_flag_permanent)
tag.text(tr("Permanent"));
else if(channel.properties.channel_flag_semi_permanent)
tag.text(tr("Semi permanent"));
else
//TODO: Channel delete delay!
tag.text(tr("Temporary"));
}
function apply_channel_description(container: JQuery, channel: ChannelEntry) {
const container_value = container.find(".value");
const container_no_value = container.find(".no-value");
channel.getChannelDescription().then(description => {
if(description) {
const result = xbbcode.parse(description, {});
container_value[0].innerHTML = result.build_html();
container_no_value.hide();
container_value.show();
} else {
container_no_value.text(tr("Channel has no description"));
}
});
container_value.hide();
container_no_value.text(tr("loading...")).show();
/* chat mode */
{
const tag = container.find(".chat-mode .value").empty();
if(channel.properties.channel_flag_conversation_private || channel.properties.channel_flag_password) {
tag.text(tr("Private"));
} else {
if(channel.properties.channel_conversation_history_length == -1)
tag.text(tr("Public; Semi permanent message saving"));
else if(channel.properties.channel_conversation_history_length == 0)
tag.text(tr("Public; Permanent message saving"));
else
tag.append(formatMessage(tr("Public; Saving last {} messages"), channel.properties.channel_conversation_history_length));
}
}
const codec_names = [
tr("Speex Narrowband"),
tr("Speex Wideband"),
tr("Speex Ultra-Wideband"),
tr("CELT Mono"),
tr("Opus Voice"),
tr("Opus Music")
];
/* current clients */
{
const tag = container.find(".current-clients .value").empty();
function apply_general(container: JQuery, channel: ChannelEntry) {
/* channel type */
{
const tag = container.find(".channel-type .value").empty();
if(channel.properties.channel_flag_permanent)
tag.text(tr("Permanent"));
else if(channel.properties.channel_flag_semi_permanent)
tag.text(tr("Semi permanent"));
else
//TODO: Channel delete delay!
tag.text(tr("Temporary"));
}
/* chat mode */
{
const tag = container.find(".chat-mode .value").empty();
if(channel.properties.channel_flag_conversation_private || channel.properties.channel_flag_password) {
tag.text(tr("Private"));
} else {
if(channel.properties.channel_conversation_history_length == -1)
tag.text(tr("Public; Semi permanent message saving"));
else if(channel.properties.channel_conversation_history_length == 0)
tag.text(tr("Public; Permanent message saving"));
else
tag.append(MessageHelper.formatMessage(tr("Public; Saving last {} messages"), channel.properties.channel_conversation_history_length));
if(channel.flag_subscribed) {
const current = channel.clients().length;
let channel_limit = tr("Unlimited");
if(!channel.properties.channel_flag_maxclients_unlimited)
channel_limit = "" + channel.properties.channel_maxclients;
else if(!channel.properties.channel_flag_maxfamilyclients_unlimited) {
if(channel.properties.channel_maxfamilyclients >= 0)
channel_limit = "" + channel.properties.channel_maxfamilyclients;
}
tag.text(current + " / " + channel_limit);
} else {
tag.text(tr("Channel not subscribed"));
}
}
/* current clients */
{
const tag = container.find(".current-clients .value").empty();
/* audio codec */
{
const tag = container.find(".audio-codec .value").empty();
tag.text((codec_names[channel.properties.channel_codec] || tr("Unknown")) + " (" + channel.properties.channel_codec_quality + ")")
}
if(channel.flag_subscribed) {
const current = channel.clients().length;
let channel_limit = tr("Unlimited");
if(!channel.properties.channel_flag_maxclients_unlimited)
channel_limit = "" + channel.properties.channel_maxclients;
else if(!channel.properties.channel_flag_maxfamilyclients_unlimited) {
if(channel.properties.channel_maxfamilyclients >= 0)
channel_limit = "" + channel.properties.channel_maxfamilyclients;
}
/* audio encrypted */
{
const tag = container.find(".audio-encrypted .value").empty();
const mode = channel.channelTree.server.properties.virtualserver_codec_encryption_mode;
let appendix;
if(mode == 1)
appendix = tr("Overridden by the server with Unencrypted!");
else if(mode == 2)
appendix = tr("Overridden by the server with Encrypted!");
tag.text(current + " / " + channel_limit);
} else {
tag.text(tr("Channel not subscribed"));
}
}
tag.html((channel.properties.channel_codec_is_unencrypted ? tr("Unencrypted") : tr("Encrypted")) + (appendix ? "<br>" + appendix : ""))
}
/* audio codec */
{
const tag = container.find(".audio-codec .value").empty();
tag.text((codec_names[channel.properties.channel_codec] || tr("Unknown")) + " (" + channel.properties.channel_codec_quality + ")")
}
/* flag password */
{
const tag = container.find(".flag-password .value").empty();
if(channel.properties.channel_flag_password)
tag.text(tr("Yes"));
else
tag.text(tr("No"));
}
/* audio encrypted */
{
const tag = container.find(".audio-encrypted .value").empty();
const mode = channel.channelTree.server.properties.virtualserver_codec_encryption_mode;
let appendix;
if(mode == 1)
appendix = tr("Overridden by the server with Unencrypted!");
else if(mode == 2)
appendix = tr("Overridden by the server with Encrypted!");
tag.html((channel.properties.channel_codec_is_unencrypted ? tr("Unencrypted") : tr("Encrypted")) + (appendix ? "<br>" + appendix : ""))
}
/* flag password */
{
const tag = container.find(".flag-password .value").empty();
if(channel.properties.channel_flag_password)
tag.text(tr("Yes"));
else
tag.text(tr("No"));
}
/* topic */
{
const container_tag = container.find(".topic");
const tag = container_tag.find(".value").empty();
if(channel.properties.channel_topic) {
container_tag.show();
tag.text(channel.properties.channel_topic);
} else {
container_tag.hide();
}
/* topic */
{
const container_tag = container.find(".topic");
const tag = container_tag.find(".value").empty();
if(channel.properties.channel_topic) {
container_tag.show();
tag.text(channel.properties.channel_topic);
} else {
container_tag.hide();
}
}
}

View File

@ -1,512 +1,519 @@
namespace Modals {
type InfoUpdateCallback = (info: ClientConnectionInfo) => any;
export function openClientInfo(client: ClientEntry) {
let modal: Modal;
let update_callbacks: InfoUpdateCallback[] = [];
import {ClientConnectionInfo, ClientEntry} from "tc-shared/ui/client";
import PermissionType from "tc-shared/permission/PermissionType";
import {createInfoModal, createModal, Modal} from "tc-shared/ui/elements/Modal";
import {copy_to_clipboard} from "tc-shared/utils/helpers";
import * as i18nc from "tc-shared/i18n/country";
import * as tooltip from "tc-shared/ui/elements/Tooltip";
import {format_number, network} from "tc-shared/ui/frames/chat";
modal = createModal({
header: tr("Profile Information: ") + client.clientNickName(),
body: () => {
const template = $("#tmpl_client_info").renderTag();
type InfoUpdateCallback = (info: ClientConnectionInfo) => any;
export function openClientInfo(client: ClientEntry) {
let modal: Modal;
let update_callbacks: InfoUpdateCallback[] = [];
/* the tab functionality */
{
const container_tabs = template.find(".container-categories");
container_tabs.find(".categories .entry").on('click', event => {
const entry = $(event.target);
modal = createModal({
header: tr("Profile Information: ") + client.clientNickName(),
body: () => {
const template = $("#tmpl_client_info").renderTag();
container_tabs.find(".bodies > .body").addClass("hidden");
container_tabs.find(".categories > .selected").removeClass("selected");
/* the tab functionality */
{
const container_tabs = template.find(".container-categories");
container_tabs.find(".categories .entry").on('click', event => {
const entry = $(event.target);
entry.addClass("selected");
container_tabs.find(".bodies > .body." + entry.attr("container")).removeClass("hidden");
});
container_tabs.find(".bodies > .body").addClass("hidden");
container_tabs.find(".categories > .selected").removeClass("selected");
container_tabs.find(".entry").first().trigger('click');
}
entry.addClass("selected");
container_tabs.find(".bodies > .body." + entry.attr("container")).removeClass("hidden");
});
apply_static_info(client, template, modal, update_callbacks);
apply_client_status(client, template, modal, update_callbacks);
apply_basic_info(client, template.find(".container-basic"), modal, update_callbacks);
apply_groups(client, template.find(".container-groups"), modal, update_callbacks);
apply_packets(client, template.find(".container-packets"), modal, update_callbacks);
container_tabs.find(".entry").first().trigger('click');
}
tooltip(template);
return template.children();
},
footer: null,
apply_static_info(client, template, modal, update_callbacks);
apply_client_status(client, template, modal, update_callbacks);
apply_basic_info(client, template.find(".container-basic"), modal, update_callbacks);
apply_groups(client, template.find(".container-groups"), modal, update_callbacks);
apply_packets(client, template.find(".container-packets"), modal, update_callbacks);
width: '60em'
tooltip.initialize(template);
return template.children();
},
footer: null,
width: '60em'
});
const updater = setInterval(() => {
client.request_connection_info().then(info => update_callbacks.forEach(e => e(info)));
}, 1000);
modal.htmlTag.find(".modal-body").addClass("modal-client-info");
modal.open();
modal.close_listener.push(() => clearInterval(updater));
}
const TIME_SECOND = 1000;
const TIME_MINUTE = 60 * TIME_SECOND;
const TIME_HOUR = 60 * TIME_MINUTE;
const TIME_DAY = 24 * TIME_HOUR;
const TIME_WEEK = 7 * TIME_DAY;
function format_time(time: number, default_value: string) {
let result = "";
if(time > TIME_WEEK) {
const amount = Math.floor(time / TIME_WEEK);
result += " " + amount + " " + (amount > 1 ? tr("Weeks") : tr("Week"));
time -= amount * TIME_WEEK;
}
if(time > TIME_DAY) {
const amount = Math.floor(time / TIME_DAY);
result += " " + amount + " " + (amount > 1 ? tr("Days") : tr("Day"));
time -= amount * TIME_DAY;
}
if(time > TIME_HOUR) {
const amount = Math.floor(time / TIME_HOUR);
result += " " + amount + " " + (amount > 1 ? tr("Hours") : tr("Hour"));
time -= amount * TIME_HOUR;
}
if(time > TIME_MINUTE) {
const amount = Math.floor(time / TIME_MINUTE);
result += " " + amount + " " + (amount > 1 ? tr("Minutes") : tr("Minute"));
time -= amount * TIME_MINUTE;
}
if(time > TIME_SECOND) {
const amount = Math.floor(time / TIME_SECOND);
result += " " + amount + " " + (amount > 1 ? tr("Seconds") : tr("Second"));
time -= amount * TIME_SECOND;
}
return result.length > 0 ? result.substring(1) : default_value;
}
function apply_static_info(client: ClientEntry, tag: JQuery, modal: Modal, callbacks: InfoUpdateCallback[]) {
tag.find(".container-avatar").append(
client.channelTree.client.fileManager.avatars.generate_chat_tag({database_id: client.properties.client_database_id, id: client.clientId()}, client.properties.client_unique_identifier)
);
tag.find(".container-name").append(
client.createChatTag()
);
tag.find(".client-description").text(
client.properties.client_description
);
}
function apply_client_status(client: ClientEntry, tag: JQuery, modal: Modal, callbacks: InfoUpdateCallback[]) {
tag.find(".status-output-disabled").toggle(!client.properties.client_output_hardware);
tag.find(".status-input-disabled").toggle(!client.properties.client_input_hardware);
tag.find(".status-output-muted").toggle(client.properties.client_output_muted);
tag.find(".status-input-muted").toggle(client.properties.client_input_muted);
tag.find(".status-away").toggle(client.properties.client_away);
if(client.properties.client_away_message) {
tag.find(".container-away-message").show().find("a").text(client.properties.client_away_message);
} else {
tag.find(".container-away-message").hide();
}
}
declare const moment;
function apply_basic_info(client: ClientEntry, tag: JQuery, modal: Modal, callbacks: InfoUpdateCallback[]) {
/* Unique ID */
{
const container = tag.find(".property-unique-id");
container.find(".value a").text(client.clientUid());
container.find(".value-dbid").text(client.properties.client_database_id);
container.find(".button-copy").on('click', event => {
copy_to_clipboard(client.clientUid());
createInfoModal(tr("Unique ID copied"), tr("The unique id has been copied to your clipboard!")).open();
});
}
/* TeaForo */
{
const container = tag.find(".property-teaforo .value").empty();
if(client.properties.client_teaforo_id) {
container.children().remove();
let text = client.properties.client_teaforo_name;
if((client.properties.client_teaforo_flags & 0x01) > 0)
text += " (" + tr("Banned") + ")";
if((client.properties.client_teaforo_flags & 0x02) > 0)
text += " (" + tr("Stuff") + ")";
if((client.properties.client_teaforo_flags & 0x04) > 0)
text += " (" + tr("Premium") + ")";
$.spawn("a")
.attr("href", "https://forum.teaspeak.de/index.php?members/" + client.properties.client_teaforo_id)
.attr("target", "_blank")
.text(text)
.appendTo(container);
} else {
container.append($.spawn("a").text(tr("Not connected")));
}
}
/* Version */
{
const container = tag.find(".property-version");
let version_full = client.properties.client_version;
let version = version_full.substring(0, version_full.indexOf(" "));
container.find(".value").empty().append(
$.spawn("a").attr("title", version_full).text(version),
$.spawn("a").addClass("a-on").text("on"),
$.spawn("a").text(client.properties.client_platform)
);
const container_timestamp = container.find(".container-tooltip");
let timestamp = -1;
version_full.replace(/\[build: ?([0-9]+)]/gmi, (group, ts) => {
timestamp = parseInt(ts);
return "";
});
if(timestamp > 0) {
container_timestamp.find(".value-timestamp").text(moment(timestamp * 1000).format('MMMM Do YYYY, h:mm:ss a'));
container_timestamp.show();
} else {
container_timestamp.hide();
}
}
/* Country */
{
const container = tag.find(".property-country");
container.find(".value").empty().append(
$.spawn("div").addClass("country flag-" + client.properties.client_country.toLowerCase()),
$.spawn("a").text(i18nc.country_name(client.properties.client_country, tr("Unknown")))
);
}
/* IP Address */
{
const container = tag.find(".property-ip");
const value = container.find(".value a");
value.text(tr("loading..."));
container.find(".button-copy").on('click', event => {
copy_to_clipboard(value.text());
createInfoModal(tr("Client IP copied"), tr("The client IP has been copied to your clipboard!")).open();
});
const updater = setInterval(() => {
client.request_connection_info().then(info => update_callbacks.forEach(e => e(info)));
}, 1000);
modal.htmlTag.find(".modal-body").addClass("modal-client-info");
modal.open();
modal.close_listener.push(() => clearInterval(updater));
callbacks.push(info => {
value.text(info.connection_client_ip ? (info.connection_client_ip + ":" + info.connection_client_port) : tr("Hidden"));
});
}
const TIME_SECOND = 1000;
const TIME_MINUTE = 60 * TIME_SECOND;
const TIME_HOUR = 60 * TIME_MINUTE;
const TIME_DAY = 24 * TIME_HOUR;
const TIME_WEEK = 7 * TIME_DAY;
/* first connected */
{
const container = tag.find(".property-first-connected");
function format_time(time: number, default_value: string) {
let result = "";
if(time > TIME_WEEK) {
const amount = Math.floor(time / TIME_WEEK);
result += " " + amount + " " + (amount > 1 ? tr("Weeks") : tr("Week"));
time -= amount * TIME_WEEK;
}
if(time > TIME_DAY) {
const amount = Math.floor(time / TIME_DAY);
result += " " + amount + " " + (amount > 1 ? tr("Days") : tr("Day"));
time -= amount * TIME_DAY;
}
if(time > TIME_HOUR) {
const amount = Math.floor(time / TIME_HOUR);
result += " " + amount + " " + (amount > 1 ? tr("Hours") : tr("Hour"));
time -= amount * TIME_HOUR;
}
if(time > TIME_MINUTE) {
const amount = Math.floor(time / TIME_MINUTE);
result += " " + amount + " " + (amount > 1 ? tr("Minutes") : tr("Minute"));
time -= amount * TIME_MINUTE;
}
if(time > TIME_SECOND) {
const amount = Math.floor(time / TIME_SECOND);
result += " " + amount + " " + (amount > 1 ? tr("Seconds") : tr("Second"));
time -= amount * TIME_SECOND;
}
return result.length > 0 ? result.substring(1) : default_value;
container.find(".value a").text(tr("loading..."));
client.updateClientVariables().then(() => {
container.find(".value a").text(moment(client.properties.client_created * 1000).format('MMMM Do YYYY, h:mm:ss a'));
}).catch(error => {
container.find(".value a").text(tr("error"));
});
}
function apply_static_info(client: ClientEntry, tag: JQuery, modal: Modal, callbacks: InfoUpdateCallback[]) {
tag.find(".container-avatar").append(
client.channelTree.client.fileManager.avatars.generate_chat_tag({database_id: client.properties.client_database_id, id: client.clientId()}, client.properties.client_unique_identifier)
);
/* connect count */
{
const container = tag.find(".property-connect-count");
tag.find(".container-name").append(
client.createChatTag()
);
tag.find(".client-description").text(
client.properties.client_description
);
container.find(".value a").text(tr("loading..."));
client.updateClientVariables().then(() => {
container.find(".value a").text(client.properties.client_totalconnections);
}).catch(error => {
container.find(".value a").text(tr("error"));
});
}
function apply_client_status(client: ClientEntry, tag: JQuery, modal: Modal, callbacks: InfoUpdateCallback[]) {
tag.find(".status-output-disabled").toggle(!client.properties.client_output_hardware);
tag.find(".status-input-disabled").toggle(!client.properties.client_input_hardware);
/* Online since */
{
const container = tag.find(".property-online-since");
tag.find(".status-output-muted").toggle(client.properties.client_output_muted);
tag.find(".status-input-muted").toggle(client.properties.client_input_muted);
tag.find(".status-away").toggle(client.properties.client_away);
if(client.properties.client_away_message) {
tag.find(".container-away-message").show().find("a").text(client.properties.client_away_message);
} else {
tag.find(".container-away-message").hide();
}
}
function apply_basic_info(client: ClientEntry, tag: JQuery, modal: Modal, callbacks: InfoUpdateCallback[]) {
/* Unique ID */
{
const container = tag.find(".property-unique-id");
container.find(".value a").text(client.clientUid());
container.find(".value-dbid").text(client.properties.client_database_id);
container.find(".button-copy").on('click', event => {
copy_to_clipboard(client.clientUid());
createInfoModal(tr("Unique ID copied"), tr("The unique id has been copied to your clipboard!")).open();
});
}
/* TeaForo */
{
const container = tag.find(".property-teaforo .value").empty();
if(client.properties.client_teaforo_id) {
container.children().remove();
let text = client.properties.client_teaforo_name;
if((client.properties.client_teaforo_flags & 0x01) > 0)
text += " (" + tr("Banned") + ")";
if((client.properties.client_teaforo_flags & 0x02) > 0)
text += " (" + tr("Stuff") + ")";
if((client.properties.client_teaforo_flags & 0x04) > 0)
text += " (" + tr("Premium") + ")";
$.spawn("a")
.attr("href", "https://forum.teaspeak.de/index.php?members/" + client.properties.client_teaforo_id)
.attr("target", "_blank")
.text(text)
.appendTo(container);
} else {
container.append($.spawn("a").text(tr("Not connected")));
}
}
/* Version */
{
const container = tag.find(".property-version");
let version_full = client.properties.client_version;
let version = version_full.substring(0, version_full.indexOf(" "));
container.find(".value").empty().append(
$.spawn("a").attr("title", version_full).text(version),
$.spawn("a").addClass("a-on").text("on"),
$.spawn("a").text(client.properties.client_platform)
);
const container_timestamp = container.find(".container-tooltip");
let timestamp = -1;
version_full.replace(/\[build: ?([0-9]+)]/gmi, (group, ts) => {
timestamp = parseInt(ts);
return "";
});
if(timestamp > 0) {
container_timestamp.find(".value-timestamp").text(moment(timestamp * 1000).format('MMMM Do YYYY, h:mm:ss a'));
container_timestamp.show();
} else {
container_timestamp.hide();
}
}
/* Country */
{
const container = tag.find(".property-country");
container.find(".value").empty().append(
$.spawn("div").addClass("country flag-" + client.properties.client_country.toLowerCase()),
$.spawn("a").text(i18n.country_name(client.properties.client_country, tr("Unknown")))
);
}
/* IP Address */
{
const container = tag.find(".property-ip");
const value = container.find(".value a");
value.text(tr("loading..."));
container.find(".button-copy").on('click', event => {
copy_to_clipboard(value.text());
createInfoModal(tr("Client IP copied"), tr("The client IP has been copied to your clipboard!")).open();
});
callbacks.push(info => {
value.text(info.connection_client_ip ? (info.connection_client_ip + ":" + info.connection_client_port) : tr("Hidden"));
});
}
/* first connected */
{
const container = tag.find(".property-first-connected");
container.find(".value a").text(tr("loading..."));
client.updateClientVariables().then(() => {
container.find(".value a").text(moment(client.properties.client_created * 1000).format('MMMM Do YYYY, h:mm:ss a'));
}).catch(error => {
container.find(".value a").text(tr("error"));
});
}
/* connect count */
{
const container = tag.find(".property-connect-count");
container.find(".value a").text(tr("loading..."));
client.updateClientVariables().then(() => {
container.find(".value a").text(client.properties.client_totalconnections);
}).catch(error => {
container.find(".value a").text(tr("error"));
});
}
/* Online since */
{
const container = tag.find(".property-online-since");
const node = container.find(".value a")[0];
if(node) {
const update = () => {
node.innerText = format_time(client.calculateOnlineTime() * 1000, tr("0 Seconds"));
};
callbacks.push(update); /* keep it in sync with all other updates. Else it looks wired */
update();
}
}
/* Idle time */
{
const container = tag.find(".property-idle-time");
const node = container.find(".value a")[0];
if(node) {
callbacks.push(info => {
node.innerText = format_time(info.connection_idle_time, tr("Currently active"));
});
node.innerText = tr("loading...");
}
}
/* ping */
{
const container = tag.find(".property-ping");
const node = container.find(".value a")[0];
if(node) {
callbacks.push(info => {
if(info.connection_ping >= 0)
node.innerText = info.connection_ping.toFixed(0) + "ms ± " + info.connection_ping_deviation.toFixed(2) + "ms";
else if(info.connection_ping == -1 && info.connection_ping_deviation == -1)
node.innerText = tr("Not calculated");
else
node.innerText = tr("loading...");
});
node.innerText = tr("loading...");
}
}
}
function apply_groups(client: ClientEntry, tag: JQuery, modal: Modal, callbacks: InfoUpdateCallback[]) {
/* server groups */
{
const container_entries = tag.find(".entries");
const container_empty = tag.find(".container-default-groups");
const update_groups = () => {
container_entries.empty();
container_empty.show();
for(const group_id of client.assignedServerGroupIds()) {
if(group_id == client.channelTree.server.properties.virtualserver_default_server_group)
continue;
const group = client.channelTree.client.groups.serverGroup(group_id);
if(!group) continue; //This shall never happen!
container_empty.hide();
container_entries.append($.spawn("div").addClass("entry").append(
client.channelTree.client.fileManager.icons.generateTag(group.properties.iconid),
$.spawn("a").addClass("name").text(group.name + " (" + group.id + ")"),
$.spawn("div").addClass("button-delete").append(
$.spawn("div").addClass("icon_em client-delete").attr("title", tr("Delete group")).on('click', event => {
client.channelTree.client.serverConnection.send_command("servergroupdelclient", {
sgid: group.id,
cldbid: client.properties.client_database_id
}).then(result => update_groups());
})
).toggleClass("visible",
client.channelTree.client.permissions.neededPermission(PermissionType.I_SERVER_GROUP_MEMBER_REMOVE_POWER).granted(group.requiredMemberRemovePower) ||
client.clientId() == client.channelTree.client.getClientId() && client.channelTree.client.permissions.neededPermission(PermissionType.I_SERVER_GROUP_SELF_REMOVE_POWER).granted(group.requiredMemberRemovePower)
)
))
}
const node = container.find(".value a")[0];
if(node) {
const update = () => {
node.innerText = format_time(client.calculateOnlineTime() * 1000, tr("0 Seconds"));
};
tag.find(".button-group-add").on('click', () => client.open_assignment_modal());
update_groups();
callbacks.push(update); /* keep it in sync with all other updates. Else it looks wired */
update();
}
}
function apply_packets(client: ClientEntry, tag: JQuery, modal: Modal, callbacks: InfoUpdateCallback[]) {
/* Packet Loss */
{
const container = tag.find(".statistic-packet-loss");
const node_downstream = container.find(".downstream .value")[0];
const node_upstream = container.find(".upstream .value")[0];
if(node_downstream) {
callbacks.push(info => {
node_downstream.innerText = info.connection_server2client_packetloss_control < 0 ? tr("Not calculated") : (info.connection_server2client_packetloss_control || 0).toFixed();
});
node_downstream.innerText = tr("loading...");
}
if(node_upstream) {
callbacks.push(info => {
node_upstream.innerText = info.connection_client2server_packetloss_total < 0 ? tr("Not calculated") : (info.connection_client2server_packetloss_total || 0).toFixed();
});
node_upstream.innerText = tr("loading...");
}
}
/* Packets transmitted */
{
const container = tag.find(".statistic-transmitted-packets");
const node_downstream = container.find(".downstream .value")[0];
const node_upstream = container.find(".upstream .value")[0];
if(node_downstream) {
callbacks.push(info => {
let packets = 0;
packets += info.connection_packets_received_speech > 0 ? info.connection_packets_received_speech : 0;
packets += info.connection_packets_received_control > 0 ? info.connection_packets_received_control : 0;
packets += info.connection_packets_received_keepalive > 0 ? info.connection_packets_received_keepalive : 0;
if(packets == 0 && info.connection_packets_received_keepalive == -1)
node_downstream.innerText = tr("Not calculated");
else
node_downstream.innerText = MessageHelper.format_number(packets, {unit: "Packets"});
});
node_downstream.innerText = tr("loading...");
}
if(node_upstream) {
callbacks.push(info => {
let packets = 0;
packets += info.connection_packets_sent_speech > 0 ? info.connection_packets_sent_speech : 0;
packets += info.connection_packets_sent_control > 0 ? info.connection_packets_sent_control : 0;
packets += info.connection_packets_sent_keepalive > 0 ? info.connection_packets_sent_keepalive : 0;
if(packets == 0 && info.connection_packets_sent_keepalive == -1)
node_upstream.innerText = tr("Not calculated");
else
node_upstream.innerText = MessageHelper.format_number(packets, {unit: "Packets"});
});
node_upstream.innerText = tr("loading...");
}
}
/* Bytes transmitted */
{
const container = tag.find(".statistic-transmitted-bytes");
const node_downstream = container.find(".downstream .value")[0];
const node_upstream = container.find(".upstream .value")[0];
if(node_downstream) {
callbacks.push(info => {
let bytes = 0;
bytes += info.connection_bytes_received_speech > 0 ? info.connection_bytes_received_speech : 0;
bytes += info.connection_bytes_received_control > 0 ? info.connection_bytes_received_control : 0;
bytes += info.connection_bytes_received_keepalive > 0 ? info.connection_bytes_received_keepalive : 0;
if(bytes == 0 && info.connection_bytes_received_keepalive == -1)
node_downstream.innerText = tr("Not calculated");
else
node_downstream.innerText = MessageHelper.network.format_bytes(bytes);
});
node_downstream.innerText = tr("loading...");
}
if(node_upstream) {
callbacks.push(info => {
let bytes = 0;
bytes += info.connection_bytes_sent_speech > 0 ? info.connection_bytes_sent_speech : 0;
bytes += info.connection_bytes_sent_control > 0 ? info.connection_bytes_sent_control : 0;
bytes += info.connection_bytes_sent_keepalive > 0 ? info.connection_bytes_sent_keepalive : 0;
if(bytes == 0 && info.connection_bytes_sent_keepalive == -1)
node_upstream.innerText = tr("Not calculated");
else
node_upstream.innerText = MessageHelper.network.format_bytes(bytes);
});
node_upstream.innerText = tr("loading...");
}
}
/* Bandwidth second */
{
const container = tag.find(".statistic-bandwidth-second");
const node_downstream = container.find(".downstream .value")[0];
const node_upstream = container.find(".upstream .value")[0];
if(node_downstream) {
callbacks.push(info => {
let bytes = 0;
bytes += info.connection_bandwidth_received_last_second_speech > 0 ? info.connection_bandwidth_received_last_second_speech : 0;
bytes += info.connection_bandwidth_received_last_second_control > 0 ? info.connection_bandwidth_received_last_second_control : 0;
bytes += info.connection_bandwidth_received_last_second_keepalive > 0 ? info.connection_bandwidth_received_last_second_keepalive : 0;
if(bytes == 0 && info.connection_bandwidth_received_last_second_keepalive == -1)
node_downstream.innerText = tr("Not calculated");
else
node_downstream.innerText = MessageHelper.network.format_bytes(bytes, {time: "s"});
/* Idle time */
{
const container = tag.find(".property-idle-time");
const node = container.find(".value a")[0];
if(node) {
callbacks.push(info => {
node.innerText = format_time(info.connection_idle_time, tr("Currently active"));
});
node_downstream.innerText = tr("loading...");
}
node.innerText = tr("loading...");
}
}
if(node_upstream) {
callbacks.push(info => {
let bytes = 0;
bytes += info.connection_bandwidth_sent_last_second_speech > 0 ? info.connection_bandwidth_sent_last_second_speech : 0;
bytes += info.connection_bandwidth_sent_last_second_control > 0 ? info.connection_bandwidth_sent_last_second_control : 0;
bytes += info.connection_bandwidth_sent_last_second_keepalive > 0 ? info.connection_bandwidth_sent_last_second_keepalive : 0;
if(bytes == 0 && info.connection_bandwidth_sent_last_second_keepalive == -1)
node_upstream.innerText = tr("Not calculated");
else
node_upstream.innerText = MessageHelper.network.format_bytes(bytes, {time: "s"});
});
node_upstream.innerText = tr("loading...");
/* ping */
{
const container = tag.find(".property-ping");
const node = container.find(".value a")[0];
if(node) {
callbacks.push(info => {
if(info.connection_ping >= 0)
node.innerText = info.connection_ping.toFixed(0) + "ms ± " + info.connection_ping_deviation.toFixed(2) + "ms";
else if(info.connection_ping == -1 && info.connection_ping_deviation == -1)
node.innerText = tr("Not calculated");
else
node.innerText = tr("loading...");
});
node.innerText = tr("loading...");
}
}
}
function apply_groups(client: ClientEntry, tag: JQuery, modal: Modal, callbacks: InfoUpdateCallback[]) {
/* server groups */
{
const container_entries = tag.find(".entries");
const container_empty = tag.find(".container-default-groups");
const update_groups = () => {
container_entries.empty();
container_empty.show();
for(const group_id of client.assignedServerGroupIds()) {
if(group_id == client.channelTree.server.properties.virtualserver_default_server_group)
continue;
const group = client.channelTree.client.groups.serverGroup(group_id);
if(!group) continue; //This shall never happen!
container_empty.hide();
container_entries.append($.spawn("div").addClass("entry").append(
client.channelTree.client.fileManager.icons.generateTag(group.properties.iconid),
$.spawn("a").addClass("name").text(group.name + " (" + group.id + ")"),
$.spawn("div").addClass("button-delete").append(
$.spawn("div").addClass("icon_em client-delete").attr("title", tr("Delete group")).on('click', event => {
client.channelTree.client.serverConnection.send_command("servergroupdelclient", {
sgid: group.id,
cldbid: client.properties.client_database_id
}).then(result => update_groups());
})
).toggleClass("visible",
client.channelTree.client.permissions.neededPermission(PermissionType.I_SERVER_GROUP_MEMBER_REMOVE_POWER).granted(group.requiredMemberRemovePower) ||
client.clientId() == client.channelTree.client.getClientId() && client.channelTree.client.permissions.neededPermission(PermissionType.I_SERVER_GROUP_SELF_REMOVE_POWER).granted(group.requiredMemberRemovePower)
)
))
}
};
tag.find(".button-group-add").on('click', () => client.open_assignment_modal());
update_groups();
}
}
function apply_packets(client: ClientEntry, tag: JQuery, modal: Modal, callbacks: InfoUpdateCallback[]) {
/* Packet Loss */
{
const container = tag.find(".statistic-packet-loss");
const node_downstream = container.find(".downstream .value")[0];
const node_upstream = container.find(".upstream .value")[0];
if(node_downstream) {
callbacks.push(info => {
node_downstream.innerText = info.connection_server2client_packetloss_control < 0 ? tr("Not calculated") : (info.connection_server2client_packetloss_control || 0).toFixed();
});
node_downstream.innerText = tr("loading...");
}
/* Bandwidth minute */
{
const container = tag.find(".statistic-bandwidth-minute");
const node_downstream = container.find(".downstream .value")[0];
const node_upstream = container.find(".upstream .value")[0];
if(node_upstream) {
callbacks.push(info => {
node_upstream.innerText = info.connection_client2server_packetloss_total < 0 ? tr("Not calculated") : (info.connection_client2server_packetloss_total || 0).toFixed();
});
node_upstream.innerText = tr("loading...");
}
}
if(node_downstream) {
callbacks.push(info => {
let bytes = 0;
bytes += info.connection_bandwidth_received_last_minute_speech > 0 ? info.connection_bandwidth_received_last_minute_speech : 0;
bytes += info.connection_bandwidth_received_last_minute_control > 0 ? info.connection_bandwidth_received_last_minute_control : 0;
bytes += info.connection_bandwidth_received_last_minute_keepalive > 0 ? info.connection_bandwidth_received_last_minute_keepalive : 0;
if(bytes == 0 && info.connection_bandwidth_received_last_minute_keepalive == -1)
node_downstream.innerText = tr("Not calculated");
else
node_downstream.innerText = MessageHelper.network.format_bytes(bytes, {time: "s"});
});
node_downstream.innerText = tr("loading...");
}
/* Packets transmitted */
{
const container = tag.find(".statistic-transmitted-packets");
const node_downstream = container.find(".downstream .value")[0];
const node_upstream = container.find(".upstream .value")[0];
if(node_upstream) {
callbacks.push(info => {
let bytes = 0;
bytes += info.connection_bandwidth_sent_last_minute_speech > 0 ? info.connection_bandwidth_sent_last_minute_speech : 0;
bytes += info.connection_bandwidth_sent_last_minute_control > 0 ? info.connection_bandwidth_sent_last_minute_control : 0;
bytes += info.connection_bandwidth_sent_last_minute_keepalive > 0 ? info.connection_bandwidth_sent_last_minute_keepalive : 0;
if(bytes == 0 && info.connection_bandwidth_sent_last_minute_keepalive == -1)
node_upstream.innerText = tr("Not calculated");
else
node_upstream.innerText = MessageHelper.network.format_bytes(bytes, {time: "s"});
});
node_upstream.innerText = tr("loading...");
}
if(node_downstream) {
callbacks.push(info => {
let packets = 0;
packets += info.connection_packets_received_speech > 0 ? info.connection_packets_received_speech : 0;
packets += info.connection_packets_received_control > 0 ? info.connection_packets_received_control : 0;
packets += info.connection_packets_received_keepalive > 0 ? info.connection_packets_received_keepalive : 0;
if(packets == 0 && info.connection_packets_received_keepalive == -1)
node_downstream.innerText = tr("Not calculated");
else
node_downstream.innerText = format_number(packets, {unit: "Packets"});
});
node_downstream.innerText = tr("loading...");
}
/* quota */
{
const container = tag.find(".statistic-quota");
const node_downstream = container.find(".downstream .value")[0];
const node_upstream = container.find(".upstream .value")[0];
if(node_upstream) {
callbacks.push(info => {
let packets = 0;
packets += info.connection_packets_sent_speech > 0 ? info.connection_packets_sent_speech : 0;
packets += info.connection_packets_sent_control > 0 ? info.connection_packets_sent_control : 0;
packets += info.connection_packets_sent_keepalive > 0 ? info.connection_packets_sent_keepalive : 0;
if(packets == 0 && info.connection_packets_sent_keepalive == -1)
node_upstream.innerText = tr("Not calculated");
else
node_upstream.innerText = format_number(packets, {unit: "Packets"});
});
node_upstream.innerText = tr("loading...");
}
}
if(node_downstream) {
client.updateClientVariables().then(info => {
//TODO: Test for own client info and if so then show the max quota (needed permission)
node_downstream.innerText = MessageHelper.network.format_bytes(client.properties.client_month_bytes_downloaded, {exact: false});
});
node_downstream.innerText = tr("loading...");
}
/* Bytes transmitted */
{
const container = tag.find(".statistic-transmitted-bytes");
const node_downstream = container.find(".downstream .value")[0];
const node_upstream = container.find(".upstream .value")[0];
if(node_upstream) {
client.updateClientVariables().then(info => {
//TODO: Test for own client info and if so then show the max quota (needed permission)
node_upstream.innerText = MessageHelper.network.format_bytes(client.properties.client_month_bytes_uploaded, {exact: false});
});
node_upstream.innerText = tr("loading...");
}
if(node_downstream) {
callbacks.push(info => {
let bytes = 0;
bytes += info.connection_bytes_received_speech > 0 ? info.connection_bytes_received_speech : 0;
bytes += info.connection_bytes_received_control > 0 ? info.connection_bytes_received_control : 0;
bytes += info.connection_bytes_received_keepalive > 0 ? info.connection_bytes_received_keepalive : 0;
if(bytes == 0 && info.connection_bytes_received_keepalive == -1)
node_downstream.innerText = tr("Not calculated");
else
node_downstream.innerText = network.format_bytes(bytes);
});
node_downstream.innerText = tr("loading...");
}
if(node_upstream) {
callbacks.push(info => {
let bytes = 0;
bytes += info.connection_bytes_sent_speech > 0 ? info.connection_bytes_sent_speech : 0;
bytes += info.connection_bytes_sent_control > 0 ? info.connection_bytes_sent_control : 0;
bytes += info.connection_bytes_sent_keepalive > 0 ? info.connection_bytes_sent_keepalive : 0;
if(bytes == 0 && info.connection_bytes_sent_keepalive == -1)
node_upstream.innerText = tr("Not calculated");
else
node_upstream.innerText = network.format_bytes(bytes);
});
node_upstream.innerText = tr("loading...");
}
}
/* Bandwidth second */
{
const container = tag.find(".statistic-bandwidth-second");
const node_downstream = container.find(".downstream .value")[0];
const node_upstream = container.find(".upstream .value")[0];
if(node_downstream) {
callbacks.push(info => {
let bytes = 0;
bytes += info.connection_bandwidth_received_last_second_speech > 0 ? info.connection_bandwidth_received_last_second_speech : 0;
bytes += info.connection_bandwidth_received_last_second_control > 0 ? info.connection_bandwidth_received_last_second_control : 0;
bytes += info.connection_bandwidth_received_last_second_keepalive > 0 ? info.connection_bandwidth_received_last_second_keepalive : 0;
if(bytes == 0 && info.connection_bandwidth_received_last_second_keepalive == -1)
node_downstream.innerText = tr("Not calculated");
else
node_downstream.innerText = network.format_bytes(bytes, {time: "s"});
});
node_downstream.innerText = tr("loading...");
}
if(node_upstream) {
callbacks.push(info => {
let bytes = 0;
bytes += info.connection_bandwidth_sent_last_second_speech > 0 ? info.connection_bandwidth_sent_last_second_speech : 0;
bytes += info.connection_bandwidth_sent_last_second_control > 0 ? info.connection_bandwidth_sent_last_second_control : 0;
bytes += info.connection_bandwidth_sent_last_second_keepalive > 0 ? info.connection_bandwidth_sent_last_second_keepalive : 0;
if(bytes == 0 && info.connection_bandwidth_sent_last_second_keepalive == -1)
node_upstream.innerText = tr("Not calculated");
else
node_upstream.innerText = network.format_bytes(bytes, {time: "s"});
});
node_upstream.innerText = tr("loading...");
}
}
/* Bandwidth minute */
{
const container = tag.find(".statistic-bandwidth-minute");
const node_downstream = container.find(".downstream .value")[0];
const node_upstream = container.find(".upstream .value")[0];
if(node_downstream) {
callbacks.push(info => {
let bytes = 0;
bytes += info.connection_bandwidth_received_last_minute_speech > 0 ? info.connection_bandwidth_received_last_minute_speech : 0;
bytes += info.connection_bandwidth_received_last_minute_control > 0 ? info.connection_bandwidth_received_last_minute_control : 0;
bytes += info.connection_bandwidth_received_last_minute_keepalive > 0 ? info.connection_bandwidth_received_last_minute_keepalive : 0;
if(bytes == 0 && info.connection_bandwidth_received_last_minute_keepalive == -1)
node_downstream.innerText = tr("Not calculated");
else
node_downstream.innerText = network.format_bytes(bytes, {time: "s"});
});
node_downstream.innerText = tr("loading...");
}
if(node_upstream) {
callbacks.push(info => {
let bytes = 0;
bytes += info.connection_bandwidth_sent_last_minute_speech > 0 ? info.connection_bandwidth_sent_last_minute_speech : 0;
bytes += info.connection_bandwidth_sent_last_minute_control > 0 ? info.connection_bandwidth_sent_last_minute_control : 0;
bytes += info.connection_bandwidth_sent_last_minute_keepalive > 0 ? info.connection_bandwidth_sent_last_minute_keepalive : 0;
if(bytes == 0 && info.connection_bandwidth_sent_last_minute_keepalive == -1)
node_upstream.innerText = tr("Not calculated");
else
node_upstream.innerText = network.format_bytes(bytes, {time: "s"});
});
node_upstream.innerText = tr("loading...");
}
}
/* quota */
{
const container = tag.find(".statistic-quota");
const node_downstream = container.find(".downstream .value")[0];
const node_upstream = container.find(".upstream .value")[0];
if(node_downstream) {
client.updateClientVariables().then(info => {
//TODO: Test for own client info and if so then show the max quota (needed permission)
node_downstream.innerText = network.format_bytes(client.properties.client_month_bytes_downloaded, {exact: false});
});
node_downstream.innerText = tr("loading...");
}
if(node_upstream) {
client.updateClientVariables().then(info => {
//TODO: Test for own client info and if so then show the max quota (needed permission)
node_upstream.innerText = network.format_bytes(client.properties.client_month_bytes_uploaded, {exact: false});
});
node_upstream.innerText = tr("loading...");
}
}
}

View File

@ -1,7 +1,17 @@
/// <reference path="../../ui/elements/modal.ts" />
import {Settings, settings} from "tc-shared/settings";
import {LogCategory} from "tc-shared/log";
import * as log from "tc-shared/log";
import * as loader from "tc-loader";
import {createModal} from "tc-shared/ui/elements/Modal";
import {ConnectionProfile, default_profile, find_profile, profiles} from "tc-shared/profiles/ConnectionProfile";
import {KeyCode} from "tc-shared/PPTListener";
import {IconManager} from "tc-shared/FileManager";
import * as i18nc from "tc-shared/i18n/country";
import {spawnSettingsModal} from "tc-shared/ui/modal/ModalSettings";
import {server_connections} from "tc-shared/ui/frames/connection_handlers";
//FIXME: Move this shit out of this file!
namespace connection_log {
export namespace connection_log {
//TODO: Save password data
export type ConnectionData = {
name: string;
@ -91,168 +101,150 @@ namespace connection_log {
});
}
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}) {
let selected_profile: profiles.ConnectionProfile;
declare const native_client;
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: ConnectionProfile, enforce: boolean}) {
let selected_profile: ConnectionProfile;
const random_id = (() => {
const array = new Uint32Array(10);
window.crypto.getRandomValues(array);
return array.join("");
})();
const random_id = (() => {
const array = new Uint32Array(10);
window.crypto.getRandomValues(array);
return array.join("");
})();
const modal = createModal({
header: tr("Connect to a server"),
body: $("#tmpl_connect").renderTag({
client: native_client,
forum_path: settings.static("forum_path"),
password_id: random_id,
multi_tab: !settings.static_global(Settings.KEY_DISABLE_MULTI_SESSION),
default_connect_new_tab: typeof(options.default_connect_new_tab) === "boolean" && options.default_connect_new_tab
}),
footer: () => undefined,
min_width: "28em"
const modal = createModal({
header: tr("Connect to a server"),
body: $("#tmpl_connect").renderTag({
client: native_client,
forum_path: settings.static("forum_path"),
password_id: random_id,
multi_tab: !settings.static_global(Settings.KEY_DISABLE_MULTI_SESSION),
default_connect_new_tab: typeof(options.default_connect_new_tab) === "boolean" && options.default_connect_new_tab
}),
footer: () => undefined,
min_width: "28em"
});
modal.htmlTag.find(".modal-body").addClass("modal-connect");
/* server list toggle */
{
const container_last_servers = modal.htmlTag.find(".container-last-servers");
const button = modal.htmlTag.find(".button-toggle-last-servers");
const set_show = shown => {
container_last_servers.toggleClass('shown', shown);
button.find(".arrow").toggleClass('down', shown).toggleClass('up', !shown);
settings.changeGlobal("connect_show_last_servers", shown);
};
button.on('click', event => {
set_show(!container_last_servers.hasClass("shown"));
});
set_show(settings.static_global("connect_show_last_servers", false));
}
modal.htmlTag.find(".modal-body").addClass("modal-connect");
const apply = (header, body, footer) => {
const container_last_server_body = modal.htmlTag.find(".container-last-servers .table .body");
const container_empty = container_last_server_body.find(".body-empty");
let current_connect_data: connection_log.ConnectionEntry;
/* server list toggle */
{
const container_last_servers = modal.htmlTag.find(".container-last-servers");
const button = modal.htmlTag.find(".button-toggle-last-servers");
const set_show = shown => {
container_last_servers.toggleClass('shown', shown);
button.find(".arrow").toggleClass('down', shown).toggleClass('up', !shown);
settings.changeGlobal("connect_show_last_servers", shown);
};
button.on('click', event => {
set_show(!container_last_servers.hasClass("shown"));
});
set_show(settings.static_global("connect_show_last_servers", false));
}
const button_connect = footer.find(".button-connect");
const button_connect_tab = footer.find(".button-connect-new-tab");
const button_manage = body.find(".button-manage-profiles");
const apply = (header, body, footer) => {
const container_last_server_body = modal.htmlTag.find(".container-last-servers .table .body");
const container_empty = container_last_server_body.find(".body-empty");
let current_connect_data: connection_log.ConnectionEntry;
const input_profile = body.find(".container-select-profile select");
const input_address = body.find(".container-address input");
const input_nickname = body.find(".container-nickname input");
const input_password = body.find(".container-password input");
const button_connect = footer.find(".button-connect");
const button_connect_tab = footer.find(".button-connect-new-tab");
const button_manage = body.find(".button-manage-profiles");
const input_profile = body.find(".container-select-profile select");
const input_address = body.find(".container-address input");
const input_nickname = body.find(".container-nickname input");
const input_password = body.find(".container-password input");
let updateFields = (reset_current_data: boolean) => {
if(reset_current_data) {
current_connect_data = undefined;
container_last_server_body.find(".selected").removeClass("selected");
}
let address = input_address.val().toString();
settings.changeGlobal(Settings.KEY_CONNECT_ADDRESS, address);
let flag_address = !!address.match(Regex.IP_V4) || !!address.match(Regex.IP_V6) || !!address.match(Regex.DOMAIN);
let nickname = input_nickname.val().toString();
if(nickname)
settings.changeGlobal(Settings.KEY_CONNECT_USERNAME, nickname);
else
nickname = input_nickname.attr("placeholder") || "";
let flag_nickname = nickname.length >= 3 && nickname.length <= 32;
input_address.attr('pattern', flag_address ? null : '^[a]{1000}$').toggleClass('is-invalid', !flag_address);
input_nickname.attr('pattern', flag_nickname ? null : '^[a]{1000}$').toggleClass('is-invalid', !flag_nickname);
const flag_disabled = !flag_nickname || !flag_address || !selected_profile || !selected_profile.valid();
button_connect.prop("disabled", flag_disabled);
button_connect_tab.prop("disabled", flag_disabled);
};
input_address.val(defaultHost.enforce ? defaultHost.url : settings.static_global(Settings.KEY_CONNECT_ADDRESS, defaultHost.url));
input_address
.on("keyup", () => updateFields(true))
.on('keydown', event => {
if(event.keyCode == KeyCode.KEY_ENTER && !event.shiftKey)
button_connect.trigger('click');
});
button_manage.on('click', event => {
const modal = Modals.spawnSettingsModal("identity-profiles");
modal.close_listener.push(() => {
input_profile.trigger('change');
});
return true;
});
/* Connect Profiles */
{
for(const profile of profiles.profiles()) {
input_profile.append(
$.spawn("option").text(profile.profile_name).val(profile.id)
);
}
input_profile.on('change', event => {
selected_profile = profiles.find_profile(input_profile.val() as string) || profiles.default_profile();
{
settings.changeGlobal(Settings.KEY_CONNECT_USERNAME, undefined);
input_nickname
.attr('placeholder', selected_profile.connect_username() || "Another TeaSpeak user")
.val("");
}
settings.changeGlobal(Settings.KEY_CONNECT_PROFILE, selected_profile.id);
input_profile.toggleClass("is-invalid", !selected_profile || !selected_profile.valid());
updateFields(true);
});
input_profile.val(connect_profile && connect_profile.profile ?
connect_profile.profile.id :
settings.static_global(Settings.KEY_CONNECT_PROFILE, "default")
).trigger('change');
let updateFields = (reset_current_data: boolean) => {
if(reset_current_data) {
current_connect_data = undefined;
container_last_server_body.find(".selected").removeClass("selected");
}
const last_nickname = settings.static_global(Settings.KEY_CONNECT_USERNAME, undefined);
if(last_nickname) /* restore */
settings.changeGlobal(Settings.KEY_CONNECT_USERNAME, last_nickname);
let address = input_address.val().toString();
settings.changeGlobal(Settings.KEY_CONNECT_ADDRESS, address);
let flag_address = !!address.match(Regex.IP_V4) || !!address.match(Regex.IP_V6) || !!address.match(Regex.DOMAIN);
input_nickname.val(last_nickname);
input_nickname.on("keyup", () => updateFields(true));
setTimeout(() => updateFields(false), 100);
let nickname = input_nickname.val().toString();
if(nickname)
settings.changeGlobal(Settings.KEY_CONNECT_USERNAME, nickname);
else
nickname = input_nickname.attr("placeholder") || "";
let flag_nickname = nickname.length >= 3 && nickname.length <= 32;
const server_address = () => {
let address = input_address.val().toString();
if(address.match(Regex.IP_V6) && !address.startsWith("["))
return "[" + address + "]";
return address;
};
button_connect.on('click', event => {
modal.close();
input_address.attr('pattern', flag_address ? null : '^[a]{1000}$').toggleClass('is-invalid', !flag_address);
input_nickname.attr('pattern', flag_nickname ? null : '^[a]{1000}$').toggleClass('is-invalid', !flag_nickname);
const connection = server_connections.active_connection_handler();
if(connection) {
connection.startConnection(
current_connect_data ? current_connect_data.address.hostname + ":" + current_connect_data.address.port : server_address(),
selected_profile,
true,
{
nickname: input_nickname.val().toString() || input_nickname.attr("placeholder"),
password: (current_connect_data && current_connect_data.password_hash) ? {password: current_connect_data.password_hash, hashed: true} : {password: input_password.val().toString(), hashed: false}
}
);
} else {
button_connect_tab.trigger('click');
}
const flag_disabled = !flag_nickname || !flag_address || !selected_profile || !selected_profile.valid();
button_connect.prop("disabled", flag_disabled);
button_connect_tab.prop("disabled", flag_disabled);
};
input_address.val(defaultHost.enforce ? defaultHost.url : settings.static_global(Settings.KEY_CONNECT_ADDRESS, defaultHost.url));
input_address
.on("keyup", () => updateFields(true))
.on('keydown', event => {
if(event.keyCode == KeyCode.KEY_ENTER && !event.shiftKey)
button_connect.trigger('click');
});
button_connect_tab.on('click', event => {
modal.close();
button_manage.on('click', event => {
const modal = spawnSettingsModal("identity-profiles");
modal.close_listener.push(() => {
input_profile.trigger('change');
});
return true;
});
const connection = server_connections.spawn_server_connection_handler();
server_connections.set_active_connection_handler(connection);
/* Connect Profiles */
{
for(const profile of profiles()) {
input_profile.append(
$.spawn("option").text(profile.profile_name).val(profile.id)
);
}
input_profile.on('change', event => {
selected_profile = find_profile(input_profile.val() as string) || default_profile();
{
settings.changeGlobal(Settings.KEY_CONNECT_USERNAME, undefined);
input_nickname
.attr('placeholder', selected_profile.connect_username() || "Another TeaSpeak user")
.val("");
}
settings.changeGlobal(Settings.KEY_CONNECT_PROFILE, selected_profile.id);
input_profile.toggleClass("is-invalid", !selected_profile || !selected_profile.valid());
updateFields(true);
});
input_profile.val(connect_profile && connect_profile.profile ?
connect_profile.profile.id :
settings.static_global(Settings.KEY_CONNECT_PROFILE, "default")
).trigger('change');
}
const last_nickname = settings.static_global(Settings.KEY_CONNECT_USERNAME, undefined);
if(last_nickname) /* restore */
settings.changeGlobal(Settings.KEY_CONNECT_USERNAME, last_nickname);
input_nickname.val(last_nickname);
input_nickname.on("keyup", () => updateFields(true));
setTimeout(() => updateFields(false), 100);
const server_address = () => {
let address = input_address.val().toString();
if(address.match(Regex.IP_V6) && !address.startsWith("["))
return "[" + address + "]";
return address;
};
button_connect.on('click', event => {
modal.close();
const connection = server_connections.active_connection_handler();
if(connection) {
connection.startConnection(
current_connect_data ? current_connect_data.address.hostname + ":" + current_connect_data.address.port : server_address(),
current_connect_data ? current_connect_data.address.hostname + ":" + current_connect_data.address.port : server_address(),
selected_profile,
true,
{
@ -260,72 +252,89 @@ namespace Modals {
password: (current_connect_data && current_connect_data.password_hash) ? {password: current_connect_data.password_hash, hashed: true} : {password: input_password.val().toString(), hashed: false}
}
);
});
/* connect history show */
{
for(const entry of connection_log.history().slice(0, 10)) {
$.spawn("div").addClass("row").append(
$.spawn("div").addClass("column delete").append($.spawn("div").addClass("icon_em client-delete")).on('click', event => {
event.preventDefault();
const row = $(event.target).parents('.row');
row.hide(250, () => {
row.detach();
});
connection_log.delete_entry(entry.address);
container_empty.toggle(container_last_server_body.children().length > 1);
})
).append(
$.spawn("div").addClass("column name").append([
IconManager.generate_tag(IconManager.load_cached_icon(entry.icon_id)),
$.spawn("a").text(entry.name)
])
).append(
$.spawn("div").addClass("column address").text(entry.address.hostname + (entry.address.port != 9987 ? (":" + entry.address.port) : ""))
).append(
$.spawn("div").addClass("column password").text(entry.flag_password ? tr("Yes") : tr("No"))
).append(
$.spawn("div").addClass("column country-name").append([
$.spawn("div").addClass("country flag-" + entry.country.toLowerCase()),
$.spawn("a").text(i18n.country_name(entry.country, tr("Global")))
])
).append(
$.spawn("div").addClass("column clients").text(entry.clients_online + "/" + entry.clients_total)
).append(
$.spawn("div").addClass("column connections").text(entry.total_connection + "")
).on('click', event => {
if(event.isDefaultPrevented())
return;
event.preventDefault();
current_connect_data = entry;
container_last_server_body.find(".selected").removeClass("selected");
$(event.target).parent('.row').addClass('selected');
input_address.val(entry.address.hostname + (entry.address.port != 9987 ? (":" + entry.address.port) : ""));
input_password.val(entry.flag_password && entry.password_hash ? "WolverinDEV Yeahr!" : "").trigger('change');
}).on('dblclick', event => {
current_connect_data = entry;
button_connect.trigger('click');
}).appendTo(container_last_server_body);
container_empty.toggle(false);
}
} else {
button_connect_tab.trigger('click');
}
};
apply(modal.htmlTag, modal.htmlTag, modal.htmlTag);
});
button_connect_tab.on('click', event => {
modal.close();
modal.open();
return;
}
const connection = server_connections.spawn_server_connection_handler();
server_connections.set_active_connection_handler(connection);
connection.startConnection(
current_connect_data ? current_connect_data.address.hostname + ":" + current_connect_data.address.port : server_address(),
selected_profile,
true,
{
nickname: input_nickname.val().toString() || input_nickname.attr("placeholder"),
password: (current_connect_data && current_connect_data.password_hash) ? {password: current_connect_data.password_hash, hashed: true} : {password: input_password.val().toString(), hashed: false}
}
);
});
export const Regex = {
//DOMAIN<:port>
DOMAIN: /^(localhost|((([a-zA-Z0-9_-]{0,63}\.){0,253})?[a-zA-Z0-9_-]{0,63}\.[a-zA-Z]{2,64}))(|:(6553[0-5]|655[0-2][0-9]|65[0-4][0-9]{2}|6[0-4][0-9]{3}|[0-5]?[0-9]{1,46}))$/,
//IP<:port>
IP_V4: /(^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?))(|:(6553[0-5]|655[0-2][0-9]|65[0-4][0-9]{2}|6[0-4][0-9]{3}|[0-5]?[0-9]{1,4}))$/,
IP_V6: /(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))/,
IP: /^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$|^(([a-zA-Z]|[a-zA-Z][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*([A-Za-z]|[A-Za-z][A-Za-z0-9\-]*[A-Za-z0-9])$|^\s*((([0-9A-Fa-f]{1,4}:){7}([0-9A-Fa-f]{1,4}|:))|(([0-9A-Fa-f]{1,4}:){6}(:[0-9A-Fa-f]{1,4}|((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){5}(((:[0-9A-Fa-f]{1,4}){1,2})|:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){4}(((:[0-9A-Fa-f]{1,4}){1,3})|((:[0-9A-Fa-f]{1,4})?:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){3}(((:[0-9A-Fa-f]{1,4}){1,4})|((:[0-9A-Fa-f]{1,4}){0,2}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){2}(((:[0-9A-Fa-f]{1,4}){1,5})|((:[0-9A-Fa-f]{1,4}){0,3}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){1}(((:[0-9A-Fa-f]{1,4}){1,6})|((:[0-9A-Fa-f]{1,4}){0,4}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(:(((:[0-9A-Fa-f]{1,4}){1,7})|((:[0-9A-Fa-f]{1,4}){0,5}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:)))(%.+)?\s*$/,
/* connect history show */
{
for(const entry of connection_log.history().slice(0, 10)) {
$.spawn("div").addClass("row").append(
$.spawn("div").addClass("column delete").append($.spawn("div").addClass("icon_em client-delete")).on('click', event => {
event.preventDefault();
const row = $(event.target).parents('.row');
row.hide(250, () => {
row.detach();
});
connection_log.delete_entry(entry.address);
container_empty.toggle(container_last_server_body.children().length > 1);
})
).append(
$.spawn("div").addClass("column name").append([
IconManager.generate_tag(IconManager.load_cached_icon(entry.icon_id)),
$.spawn("a").text(entry.name)
])
).append(
$.spawn("div").addClass("column address").text(entry.address.hostname + (entry.address.port != 9987 ? (":" + entry.address.port) : ""))
).append(
$.spawn("div").addClass("column password").text(entry.flag_password ? tr("Yes") : tr("No"))
).append(
$.spawn("div").addClass("column country-name").append([
$.spawn("div").addClass("country flag-" + entry.country.toLowerCase()),
$.spawn("a").text(i18nc.country_name(entry.country, tr("Global")))
])
).append(
$.spawn("div").addClass("column clients").text(entry.clients_online + "/" + entry.clients_total)
).append(
$.spawn("div").addClass("column connections").text(entry.total_connection + "")
).on('click', event => {
if(event.isDefaultPrevented())
return;
event.preventDefault();
current_connect_data = entry;
container_last_server_body.find(".selected").removeClass("selected");
$(event.target).parent('.row').addClass('selected');
input_address.val(entry.address.hostname + (entry.address.port != 9987 ? (":" + entry.address.port) : ""));
input_password.val(entry.flag_password && entry.password_hash ? "WolverinDEV Yeahr!" : "").trigger('change');
}).on('dblclick', event => {
current_connect_data = entry;
button_connect.trigger('click');
}).appendTo(container_last_server_body);
container_empty.toggle(false);
}
}
};
}
apply(modal.htmlTag, modal.htmlTag, modal.htmlTag);
modal.open();
return;
}
export const Regex = {
//DOMAIN<:port>
DOMAIN: /^(localhost|((([a-zA-Z0-9_-]{0,63}\.){0,253})?[a-zA-Z0-9_-]{0,63}\.[a-zA-Z]{2,64}))(|:(6553[0-5]|655[0-2][0-9]|65[0-4][0-9]{2}|6[0-4][0-9]{3}|[0-5]?[0-9]{1,46}))$/,
//IP<:port>
IP_V4: /(^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?))(|:(6553[0-5]|655[0-2][0-9]|65[0-4][0-9]{2}|6[0-4][0-9]{3}|[0-5]?[0-9]{1,4}))$/,
IP_V6: /(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))/,
IP: /^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$|^(([a-zA-Z]|[a-zA-Z][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*([A-Za-z]|[A-Za-z][A-Za-z0-9\-]*[A-Za-z0-9])$|^\s*((([0-9A-Fa-f]{1,4}:){7}([0-9A-Fa-f]{1,4}|:))|(([0-9A-Fa-f]{1,4}:){6}(:[0-9A-Fa-f]{1,4}|((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){5}(((:[0-9A-Fa-f]{1,4}){1,2})|:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){4}(((:[0-9A-Fa-f]{1,4}){1,3})|((:[0-9A-Fa-f]{1,4})?:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){3}(((:[0-9A-Fa-f]{1,4}){1,4})|((:[0-9A-Fa-f]{1,4}){0,2}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){2}(((:[0-9A-Fa-f]{1,4}){1,5})|((:[0-9A-Fa-f]{1,4}){0,3}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){1}(((:[0-9A-Fa-f]{1,4}){1,6})|((:[0-9A-Fa-f]{1,4}){0,4}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(:(((:[0-9A-Fa-f]{1,4}){1,7})|((:[0-9A-Fa-f]{1,4}){0,5}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:)))(%.+)?\s*$/,
};

File diff suppressed because it is too large Load Diff

Some files were not shown because too many files have changed in this diff Show More