Introducing the new client service provider and removing the old obsolete statistics service

master
WolverinDEV 2021-01-12 03:52:16 +01:00
parent 474ad7af01
commit bf5d1c4771
10 changed files with 1780 additions and 553 deletions

1418
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -109,6 +109,7 @@
"remarkable": "^2.0.1", "remarkable": "^2.0.1",
"resize-observer-polyfill": "^1.5.1", "resize-observer-polyfill": "^1.5.1",
"sdp-transform": "^2.14.0", "sdp-transform": "^2.14.0",
"simple-jsonp-promise": "^1.1.0",
"simplebar-react": "^2.2.0", "simplebar-react": "^2.2.0",
"twemoji": "^13.0.0", "twemoji": "^13.0.0",
"url-knife": "^3.1.3", "url-knife": "^3.1.3",

View File

@ -0,0 +1,173 @@
import * as loader from "tc-loader";
import {Stage} from "tc-loader";
import {LogCategory, logTrace} from "tc-shared/log";
import jsonp from 'simple-jsonp-promise';
interface GeoLocationInfo {
/* The country code */
country: string,
city?: string,
region?: string,
timezone?: string
}
interface GeoLocationResolver {
name() : string;
resolve() : Promise<GeoLocationInfo>;
}
const kLocalCacheKey = "geo-info";
type GeoLocationCache = {
version: 1,
timestamp: number,
info: GeoLocationInfo,
}
class GeoLocationProvider {
private readonly resolver: GeoLocationResolver[];
private currentResolverIndex: number;
private cachedInfo: GeoLocationInfo | undefined;
private lookupPromise: Promise<GeoLocationInfo>;
constructor() {
this.resolver = [
new GeoResolverIpInfo(),
new GeoResolverIpData()
];
this.currentResolverIndex = 0;
}
loadCache() {
this.doLoadCache();
if(!this.cachedInfo) {
this.lookupPromise = this.doQueryInfo();
}
}
private doLoadCache() : GeoLocationInfo {
try {
const rawItem = localStorage.getItem(kLocalCacheKey);
if(!rawItem) {
return undefined;
}
const info: GeoLocationCache = JSON.parse(rawItem);
if(info.version !== 1) {
throw tr("invalid version number");
}
if(info.timestamp + 2 * 24 * 60 * 60 * 1000 < Date.now()) {
throw tr("cache is too old");
}
if(info.timestamp + 2 * 60 * 60 * 1000 > Date.now()) {
logTrace(LogCategory.GENERAL, tr("Geo cache is less than 2hrs old. Don't updating."));
this.lookupPromise = Promise.resolve(info.info);
} else {
this.lookupPromise = this.doQueryInfo();
}
this.cachedInfo = info.info;
} catch (error) {
logTrace(LogCategory.GENERAL, tr("Failed to load geo resolve cache: %o"), error);
}
}
async queryInfo(timeout: number) : Promise<GeoLocationInfo | undefined> {
return await new Promise<GeoLocationInfo>(resolve => {
if(!this.lookupPromise) {
resolve(this.cachedInfo);
return;
}
const timeoutId = typeof timeout === "number" ? setTimeout(() => resolve(this.cachedInfo), timeout) : -1;
this.lookupPromise.then(result => {
clearTimeout(timeoutId);
resolve(result);
});
});
}
private async doQueryInfo() : Promise<GeoLocationInfo | undefined> {
while(this.currentResolverIndex < this.resolver.length) {
const resolver = this.resolver[this.currentResolverIndex++];
try {
const info = await resolver.resolve();
logTrace(LogCategory.GENERAL, tr("Successfully resolved geo info from %s: %o"), resolver.name(), info);
localStorage.setItem(kLocalCacheKey, JSON.stringify({
version: 1,
timestamp: Date.now(),
info: info
} as GeoLocationCache));
return info;
} catch (error) {
logTrace(LogCategory.GENERAL, tr("Geo resolver %s failed: %o. Trying next one."), resolver.name(), error);
}
}
logTrace(LogCategory.GENERAL, tr("All geo resolver failed."));
return undefined;
}
}
class GeoResolverIpData implements GeoLocationResolver {
name(): string {
return "ipdata.co";
}
async resolve(): Promise<GeoLocationInfo> {
const response = await fetch("https://api.ipdata.co/?api-key=test");
const json = await response.json();
if(!("country_code" in json)) {
throw tr("missing country code");
}
return {
country: json["country_code"],
region: json["region"],
city: json["city"],
timezone: json["time_zone"]["name"]
}
}
}
class GeoResolverIpInfo implements GeoLocationResolver {
name(): string {
return "ipinfo.io";
}
async resolve(): Promise<GeoLocationInfo> {
const response = await jsonp("http://ipinfo.io");
if(!("country" in response)) {
throw tr("missing country");
}
return {
country: response["country"],
city: response["city"],
region: response["region"],
timezone: response["timezone"]
}
}
}
export let geoLocationProvider: GeoLocationProvider;
/* The client services depend on this */
loader.register_task(Stage.JAVASCRIPT_INITIALIZING, {
priority: 35,
function: async () => {
geoLocationProvider = new GeoLocationProvider();
geoLocationProvider.loadCache();
},
name: "geo services"
});

View File

@ -0,0 +1,442 @@
import * as loader from "tc-loader";
import {Stage} from "tc-loader";
import {LogCategory, logDebug, logError, logInfo, logTrace, logWarn} from "tc-shared/log";
import {Registry} from "tc-shared/events";
import {
CommandSessionInitializeAgent, CommandSessionUpdateLocale,
Message,
MessageCommand,
MessageCommandResult,
MessageNotify,
NotifyClientsOnline
} from "./messages";
import {config, tr} from "tc-shared/i18n/localize";
import {geoLocationProvider} from "tc-shared/clientservice/GeoLocation";
import translation_config = config.translation_config;
import {getBackend} from "tc-shared/backend";
const kApiVersion = 1;
const kVerbose = true;
type ConnectionState = "disconnected" | "connecting" | "connected" | "reconnect-pending";
type PendingCommand = {
resolve: (result: MessageCommandResult) => void,
timeout: number
};
interface ClientServiceConnectionEvents {
notify_state_changed: { oldState: ConnectionState, newState: ConnectionState },
notify_notify_received: { notify: MessageNotify }
}
let tokenIndex = 0;
class ClientServiceConnection {
readonly events: Registry<ClientServiceConnectionEvents>;
readonly verbose: boolean;
readonly reconnectInterval: number;
private reconnectTimeout: number;
private connectionState: ConnectionState;
private connection: WebSocket;
private pendingCommands: {[key: string]: PendingCommand} = {};
constructor(reconnectInterval: number, verbose: boolean) {
this.events = new Registry<ClientServiceConnectionEvents>();
this.reconnectInterval = reconnectInterval;
this.verbose = verbose;
}
destroy() {
this.disconnect();
this.events.destroy();
}
getState() : ConnectionState {
return this.connectionState;
}
private setState(newState: ConnectionState) {
if(this.connectionState === newState) {
return;
}
const oldState = this.connectionState;
this.connectionState = newState;
this.events.fire("notify_state_changed", { oldState, newState })
}
connect() {
this.disconnect();
this.setState("connecting");
let address;
address = "client-services.teaspeak.de:27791";
//address = "localhost:1244";
this.connection = new WebSocket(`wss://${address}/ws-api/v${kApiVersion}`);
this.connection.onclose = event => {
if(this.verbose) {
logInfo(LogCategory.STATISTICS, tr("Lost connection to statistics server (Connection closed). Reason: %s"), event.reason ? `${event.reason} (${event.code})` : event.code);
}
this.handleConnectionLost();
};
this.connection.onopen = () => {
if(this.verbose) {
logDebug(LogCategory.STATISTICS, tr("Connection established."));
}
this.setState("connected");
}
this.connection.onerror = () => {
if(this.connectionState === "connecting") {
if(this.verbose) {
logDebug(LogCategory.STATISTICS, tr("Failed to connect to target server."));
}
this.handleConnectFail();
} else {
if(this.verbose) {
logWarn(LogCategory.STATISTICS, tr("Received web socket error which indicates that the connection has been closed."));
}
this.handleConnectionLost();
}
};
this.connection.onmessage = event => {
if(typeof event.data !== "string") {
if(this.verbose) {
logWarn(LogCategory.STATISTICS, tr("Receved non text message: %o"), event.data);
}
return;
}
this.handleServerMessage(event.data);
};
}
disconnect() {
if(this.connection) {
this.connection.onclose = undefined;
this.connection.onopen = undefined;
this.connection.onmessage = undefined;
this.connection.onerror = undefined;
this.connection.close();
this.connection = undefined;
}
for(const command of Object.values(this.pendingCommands)) {
command.resolve({ type: "ConnectionClosed" });
}
this.pendingCommands = {};
clearTimeout(this.reconnectTimeout);
this.reconnectTimeout = undefined;
this.setState("disconnected");
}
cancelReconnect() {
clearTimeout(this.reconnectTimeout);
this.reconnectTimeout = undefined;
if(this.connectionState === "reconnect-pending") {
this.setState("disconnected");
}
}
async executeCommand(command: MessageCommand) : Promise<MessageCommandResult> {
if(this.connectionState !== "connected") {
return { type: "ConnectionClosed" };
}
const token = "tk-" + ++tokenIndex;
try {
this.connection.send(JSON.stringify({
type: "Command",
token: token,
command: command
} as Message));
} catch (error) {
if(this.verbose) {
logError(LogCategory.STATISTICS, tr("Failed to send command: %o"), error);
}
return { type: "GenericError", error: tr("Failed to send command") };
}
return await new Promise(resolve => {
const proxiedResolve = (result: MessageCommandResult) => {
clearTimeout(this.pendingCommands[token]?.timeout);
delete this.pendingCommands[token];
resolve(result);
};
this.pendingCommands[token] = {
resolve: proxiedResolve,
timeout: setTimeout(() => proxiedResolve({ type: "ConnectionTimeout" }), 5000)
};
});
}
private handleConnectFail() {
this.disconnect();
this.executeReconnect();
}
private handleConnectionLost() {
this.disconnect();
this.executeReconnect();
}
private executeReconnect() {
if(!this.reconnectInterval) {
return;
}
if(this.verbose) {
logInfo(LogCategory.STATISTICS, tr("Scheduling reconnect in %dms"), this.reconnectInterval);
}
this.reconnectTimeout = setTimeout(() => this.connect(), this.reconnectInterval);
this.setState("reconnect-pending");
}
private handleServerMessage(message: string) {
let data: Message;
try {
data = JSON.parse(message);
} catch (_error) {
if(this.verbose) {
logWarn(LogCategory.STATISTICS, tr("Received message which isn't parsable as JSON."));
}
return;
}
if(data.type === "Command") {
if(this.verbose) {
logWarn(LogCategory.STATISTICS, tr("Received message of type command. The server should not send these. Message: %o"), data);
}
/* Well this is odd. We should never receive such */
} else if(data.type === "CommandResult") {
if(data.token === null) {
if(this.verbose) {
logWarn(LogCategory.STATISTICS, tr("Received general error: %o"), data.result);
}
} else if(this.pendingCommands[data.token]) {
/* The entry itself will be cleaned up by the resolve callback */
this.pendingCommands[data.token].resolve(data.result);
} else if(this.verbose) {
logWarn(LogCategory.STATISTICS, tr("Received command result for unknown token: %o"), data.token);
}
} else if(data.type === "Notify") {
this.events.fire("notify_notify_received", { notify: data.notify });
} else if(this.verbose) {
logWarn(LogCategory.STATISTICS, tr("Received message with invalid type: %o"), (data as any).type);
}
}
}
export class ClientServices {
private connection: ClientServiceConnection;
private sessionInitialized: boolean;
private retryTimer: number;
private initializeAgentId: number;
private initializeLocaleId: number;
constructor() {
this.initializeAgentId = 0;
this.initializeLocaleId = 0;
this.sessionInitialized = false;
this.connection = new ClientServiceConnection(5000, kVerbose);
this.connection.events.on("notify_state_changed", event => {
if(event.newState !== "connected") {
this.sessionInitialized = false;
return;
}
logInfo(LogCategory.STATISTICS, tr("Connected successfully. Initializing session."));
this.executeCommandWithRetry({ type: "SessionInitialize", payload: { anonymize_ip: false }}, 2500).then(result => {
if(result.type !== "Success") {
if(result.type === "ConnectionClosed") {
return;
}
if(kVerbose) {
logError(LogCategory.STATISTICS, tr("Failed to initialize session. Retrying in 120 seconds. Result: %o"), result);
}
this.scheduleRetry(120 * 1000);
return;
}
this.sendInitializeAgent().then(undefined);
this.sendLocaleUpdate();
});
});
this.connection.events.on("notify_notify_received", event => {
switch (event.notify.type) {
case "NotifyClientsOnline":
this.handleNotifyClientsOnline(event.notify.payload);
break;
default:
return;
}
});
}
start() {
this.connection.connect();
}
stop() {
this.connection.disconnect();
clearTimeout(this.retryTimer);
this.initializeAgentId++;
this.initializeLocaleId++;
}
private scheduleRetry(time: number) {
this.stop();
this.retryTimer = setTimeout(() => this.connection.connect(), time);
}
/**
* Returns as soon the result indicates that something else went wrong rather than transmitting.
* @param command
* @param retryInterval
*/
private async executeCommandWithRetry(command: MessageCommand, retryInterval: number) : Promise<MessageCommandResult> {
while(true) {
const result = await this.connection.executeCommand(command);
switch (result.type) {
case "ServerInternalError":
case "CommandEnqueueError":
case "ClientSessionUninitialized":
const shouldRetry = await new Promise<boolean>(resolve => {
const timeout = setTimeout(() => {
listener();
resolve(true);
}, 2500);
const listener = this.connection.events.on("notify_state_changed", event => {
if(event.newState !== "connected") {
resolve(false);
clearTimeout(timeout);
}
})
});
if(shouldRetry) {
continue;
} else {
return result;
}
default:
return result;
}
}
}
private async sendInitializeAgent() {
const taskId = ++this.initializeAgentId;
const payload: CommandSessionInitializeAgent = {
session_type: __build.target === "web" ? 0 : 1,
architecture: null,
platform_version: null,
platform: null,
client_version: null,
ui_version: __build.version
};
if(__build.target === "client") {
const info = getBackend("native").getVersionInfo();
payload.client_version = info.version;
payload.architecture = info.os_architecture;
payload.platform = info.os_platform;
payload.platform_version = info.os_platform_version;
} else {
const os = window.detectedBrowser.os;
const osParts = os.split(" ");
if(osParts.last().match(/^[0-9]+$/)) {
payload.platform_version = osParts.last();
osParts.splice(osParts.length - 1, 1);
}
payload.platform = osParts.join(" ");
payload.architecture = window.detectedBrowser.name;
payload.client_version = window.detectedBrowser.version;
}
if(this.initializeAgentId !== taskId) {
/* We don't want to send that stuff any more */
return;
}
this.executeCommandWithRetry({ type: "SessionInitializeAgent", payload }, 2500).then(result => {
if(kVerbose) {
logTrace(LogCategory.STATISTICS, tr("Agent initialize result: %o"), result);
}
});
}
private async sendLocaleUpdate() {
const taskId = ++this.initializeLocaleId;
const payload: CommandSessionUpdateLocale = {
ip_country: null,
selected_locale: null,
local_timestamp: Date.now()
};
const geoInfo = await geoLocationProvider.queryInfo(2500);
payload.ip_country = geoInfo?.country?.toLowerCase() || null;
const trConfig = translation_config();
payload.selected_locale = trConfig?.current_translation_url || null;
if(this.initializeLocaleId !== taskId) {
return;
}
this.connection.executeCommand({ type: "SessionUpdateLocale", payload }).then(result => {
if(kVerbose) {
logTrace(LogCategory.STATISTICS, tr("Agent local update result: %o"), result);
}
});
}
private handleNotifyClientsOnline(notify: NotifyClientsOnline) {
logInfo(LogCategory.GENERAL, tr("Received user count update: %o"), notify);
}
}
export let clientServices: ClientServices;
loader.register_task(Stage.JAVASCRIPT_INITIALIZING, {
priority: 30,
function: async () => {
clientServices = new ClientServices();
clientServices.start();
(window as any).clientServices = clientServices;
},
name: "client services"
});

38
shared/js/clientservice/messages.d.ts vendored Normal file
View File

@ -0,0 +1,38 @@
/* Basic message declarations */
export type Message =
| { type: "Command"; token: string; command: MessageCommand }
| { type: "CommandResult"; token: string | null; result: MessageCommandResult }
| { type: "Notify"; notify: MessageNotify };
export type MessageCommand =
| { type: "SessionInitialize"; payload: CommandSessionInitialize }
| { type: "SessionInitializeAgent"; payload: CommandSessionInitializeAgent }
| { type: "SessionUpdateLocale"; payload: CommandSessionUpdateLocale };
export type MessageCommandResult =
| { type: "Success" }
| { type: "GenericError"; error: string }
| { type: "ConnectionTimeout" }
| { type: "ConnectionClosed" }
| { type: "ClientSessionUninitialized" }
| { type: "ServerInternalError" }
| { type: "ParameterInvalid"; parameter: string }
| { type: "CommandParseError"; error: string }
| { type: "CommandEnqueueError" }
| { type: "CommandNotFound" }
| { type: "SessionAlreadyInitialized" }
| { type: "SessionAgentAlreadyInitialized" }
| { type: "SessionNotInitialized" };
export type MessageNotify =
| { type: "NotifyClientsOnline"; payload: NotifyClientsOnline };
/* All commands */
export type CommandSessionInitialize = { anonymize_ip: boolean };
export type CommandSessionInitializeAgent = { session_type: number; platform: string | null; platform_version: string | null; architecture: string | null; client_version: string | null; ui_version: string | null };
export type CommandSessionUpdateLocale = { ip_country: string | null; selected_locale: string | null; local_timestamp: number };
/* Notifies */
export type NotifyClientsOnline = { users_online: { [key: number]: number }; unique_users_online: { [key: number]: number }; total_users_online: number; total_unique_users_online: number };

View File

@ -2,7 +2,6 @@ import * as loader from "tc-loader";
import * as bipc from "./ipc/BrowserIPC"; import * as bipc from "./ipc/BrowserIPC";
import * as sound from "./sound/Sounds"; import * as sound from "./sound/Sounds";
import * as i18n from "./i18n/localize"; import * as i18n from "./i18n/localize";
import * as stats from "./stats";
import * as fidentity from "./profiles/identities/TeaForumIdentity"; import * as fidentity from "./profiles/identities/TeaForumIdentity";
import * as aplayer from "tc-backend/audio/player"; import * as aplayer from "tc-backend/audio/player";
import * as ppt from "tc-backend/ppt"; import * as ppt from "tc-backend/ppt";
@ -46,6 +45,7 @@ import "./ui/frames/menu-bar/MainMenu";
import "./ui/modal/connect/Controller"; import "./ui/modal/connect/Controller";
import "./ui/elements/ContextDivider"; import "./ui/elements/ContextDivider";
import "./ui/elements/Tab"; import "./ui/elements/Tab";
import "./clientservice";
import {initializeKeyControl} from "./KeyControl"; import {initializeKeyControl} from "./KeyControl";
let preventWelcomeUI = false; let preventWelcomeUI = false;
@ -89,6 +89,7 @@ async function initializeApp() {
} }
} }
/* Used by the native client... We can't refactor this yet */
export function handle_connect_request(properties: ConnectRequestData, connection: ConnectionHandler) { export function handle_connect_request(properties: ConnectRequestData, connection: ConnectionHandler) {
const profile = findConnectProfile(properties.profile) || defaultConnectProfile(); const profile = findConnectProfile(properties.profile) || defaultConnectProfile();
const username = properties.username || profile.connectUsername(); const username = properties.username || profile.connectUsername();
@ -183,16 +184,6 @@ function main() {
fidentity.update_forum(); fidentity.update_forum();
initializeKeyControl(); initializeKeyControl();
stats.initialize({
verbose: true,
anonymize_ip_addresses: true,
volatile_collection_only: false
});
stats.registerUserCountListener(status => {
logInfo(LogCategory.STATISTICS, tr("Received user count update: %o"), status);
});
checkForUpdatedApp(); checkForUpdatedApp();
if(settings.getValue(Settings.KEY_USER_IS_NEW) && !preventWelcomeUI) { if(settings.getValue(Settings.KEY_USER_IS_NEW) && !preventWelcomeUI) {

View File

@ -1,239 +0,0 @@
import {LogCategory, logDebug, logInfo, logTrace, logWarn} from "./log";
import { tr } from "./i18n/localize";
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: number;
let current_config: Config;
let last_user_count_update: number;
let user_count_listener: UserCountListener[] = [];
const DEFAULT_CONFIG: Config = {
verbose: true,
reconnect_interval: 5000,
anonymize_ip_addresses: true,
volatile_collection_only: false
};
function initialize_config_object(target_object: any, source_object: any) : any {
for(const key of Object.keys(source_object)) {
if(typeof(source_object[key]) === 'object')
initialize_config_object(target_object[key] || (target_object[key] = {}), source_object[key]);
if(typeof(target_object[key]) !== 'undefined')
continue;
target_object[key] = source_object[key];
}
return target_object;
}
export function initialize(config: Config) {
current_config = initialize_config_object(config || {}, DEFAULT_CONFIG);
if(current_config.verbose)
logInfo(LogCategory.STATISTICS, tr("Initializing statistics with this config: %o"), current_config);
connection.start_connection();
}
export function registerUserCountListener(listener: UserCountListener) {
user_count_listener.push(listener);
}
export function unregisterUserCountListener(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)
logWarn(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)
logDebug(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)
logWarn(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)
logWarn(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;
}
if(current_config.verbose)
logInfo(LogCategory.STATISTICS, tr("Scheduled reconnect in %dms"), current_config.reconnect_interval);
reconnect_timer = setTimeout(() => {
if(current_config.verbose)
logInfo(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];
}
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)
logTrace(LogCategory.STATISTICS, tr("Handling message of type %s"), type);
handler[type](data);
} else if(current_config.verbose) {
logWarn(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)
logInfo(LogCategory.STATISTICS, tr("Session successfully initialized."));
connection_state = ConnectionState.CONNECTED;
}
handler["notifyinitialized"] = handle_notify_initialized;
handler["notifyusercount"] = handle_notify_user_count;
}
}

View File

@ -26,7 +26,7 @@ function initializeController(events: Registry<ModalGlobalSettingsEditorEvents>)
key: setting.key, key: setting.key,
description: setting.description, description: setting.description,
type: setting.valueType, type: setting.valueType,
defaultValue: "defaultValue" in setting ? setting.defaultValue : undefined defaultValue: "defaultValue" in setting ? (setting as any).defaultValue : undefined
}); });
} }
@ -54,7 +54,7 @@ function initializeController(events: Registry<ModalGlobalSettingsEditorEvents>)
key: setting.key, key: setting.key,
description: setting.description, description: setting.description,
type: setting.valueType, type: setting.valueType,
defaultValue: "defaultValue" in setting ? setting.defaultValue : undefined defaultValue: "defaultValue" in setting ? (setting as any).defaultValue : undefined
}, },
value: settings.getValue(setting, undefined) value: settings.getValue(setting, undefined)
}); });

View File

@ -21,6 +21,7 @@
], ],
"include": [ "include": [
"../js/proto.ts", "../js/proto.ts",
"../js/main.tsx",
"../backend.d", "../backend.d",
"../js/**/*.ts", "../js/**/*.ts",
"../../webpack/build-definitions.d.ts" "../../webpack/build-definitions.d.ts"

View File

@ -1,4 +1,3 @@
import * as log from "tc-shared/log";
import {LogCategory, logWarn} from "tc-shared/log"; import {LogCategory, logWarn} from "tc-shared/log";
import {ConnectionStatistics} from "tc-shared/connection/ConnectionBase"; import {ConnectionStatistics} from "tc-shared/connection/ConnectionBase";
import { tr } from "tc-shared/i18n/localize"; import { tr } from "tc-shared/i18n/localize";
@ -46,8 +45,9 @@ export class WrappedWebSocket {
let result = ""; let result = "";
result += this.address.secure ? "wss://" : "ws://"; result += this.address.secure ? "wss://" : "ws://";
result += this.address.host + ":" + this.address.port; result += this.address.host + ":" + this.address.port;
if(this.address.path) if(this.address.path) {
result += (this.address.path.startsWith("/") ? "" : "/") + this.address.path; result += (this.address.path.startsWith("/") ? "" : "/") + this.address.path;
}
return result return result
} }