Changes are listed bellow:

- Fixed the control bar microphone and speaker buttons
    - Improved the default identity generation (no web worker required now)
    - Improved voice connection error handling (especially for firefox)
    - Adding a max reconnect limit for voice connection
    - Don't show the newcomer guide when directly connection to a server
    - Fixed default profile initialisation
canary
WolverinDEV 2020-09-16 19:30:28 +02:00
parent 4a4cbdf38e
commit 6f56150e0b
24 changed files with 514 additions and 304 deletions

View File

@ -1,4 +1,12 @@
# Changelog:
* **16.09.20**
- Fixed the control bar microphone and speaker buttons
- Improved the default identity generation (no web worker required now)
- Improved voice connection error handling (especially for firefox)
- Adding a max reconnect limit for voice connection
- Don't show the newcomer guide when directly connection to a server
- Fixed default profile initialisation
* **07.09.20**
- Fixed the web client for safari

138
package-lock.json generated
View File

@ -1167,6 +1167,39 @@
"fastq": "^1.6.0"
}
},
"@peculiar/asn1-schema": {
"version": "2.0.17",
"resolved": "https://registry.npmjs.org/@peculiar/asn1-schema/-/asn1-schema-2.0.17.tgz",
"integrity": "sha512-7rJD8bR1r6NFE4skDxXsLsFEO3zM2TfjX9wdq5SERoBNEuxGkAJ3uIH84sIMxvDgJtb3cMfLsv8iNpGN0nAWdw==",
"requires": {
"@types/asn1js": "^0.0.1",
"asn1js": "^2.0.26",
"pvtsutils": "^1.0.11",
"tslib": "^2.0.1"
},
"dependencies": {
"tslib": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.0.1.tgz",
"integrity": "sha512-SgIkNheinmEBgx1IUNirK0TUD4X9yjjBRTqqjggWCU3pUEqIk3/Uwl3yRixYKT6WjQuGiwDv4NomL3wqRCj+CQ=="
}
}
},
"@peculiar/json-schema": {
"version": "1.1.12",
"resolved": "https://registry.npmjs.org/@peculiar/json-schema/-/json-schema-1.1.12.tgz",
"integrity": "sha512-coUfuoMeIB7B8/NMekxaDzLhaYmp0HZNPEjYRm9goRou8UZIC3z21s0sL9AWoCw4EG876QyO3kYrc61WNF9B/w==",
"requires": {
"tslib": "^2.0.0"
},
"dependencies": {
"tslib": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.0.1.tgz",
"integrity": "sha512-SgIkNheinmEBgx1IUNirK0TUD4X9yjjBRTqqjggWCU3pUEqIk3/Uwl3yRixYKT6WjQuGiwDv4NomL3wqRCj+CQ=="
}
}
},
"@protobufjs/aspromise": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz",
@ -1258,6 +1291,14 @@
"integrity": "sha512-/+CRPXpBDpo2RK9C68N3b2cOvO0Cf5B9aPijHsoDQTHivnGSObdOF2BRQOYjojWTDy6nQvMjmqRXIxH55VjxxA==",
"dev": true
},
"@types/asn1js": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/@types/asn1js/-/asn1js-0.0.1.tgz",
"integrity": "sha1-74uflwjLFjKhw6nNJ3F8qr55O8I=",
"requires": {
"@types/pvutils": "*"
}
},
"@types/clean-css": {
"version": "4.2.1",
"resolved": "https://registry.npmjs.org/@types/clean-css/-/clean-css-4.2.1.tgz",
@ -1439,6 +1480,11 @@
"integrity": "sha512-KfRL3PuHmqQLOG+2tGpRO26Ctg+Cq1E01D2DMriKEATHgWLfeNDmq9e29Q9WIky0dQ3NPkd1mzYH8Lm936Z9qw==",
"dev": true
},
"@types/pvutils": {
"version": "0.0.2",
"resolved": "https://registry.npmjs.org/@types/pvutils/-/pvutils-0.0.2.tgz",
"integrity": "sha512-CgQAm7pjyeF3Gnv78ty4RBVIfluB+Td+2DR8iPaU0prF18pkzptHHP+DoKPfpsJYknKsVZyVsJEu5AuGgAqQ5w=="
},
"@types/react": {
"version": "16.9.26",
"resolved": "https://registry.npmjs.org/@types/react/-/react-16.9.26.tgz",
@ -2212,6 +2258,11 @@
"integrity": "sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug==",
"dev": true
},
"asmcrypto.js": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/asmcrypto.js/-/asmcrypto.js-2.3.2.tgz",
"integrity": "sha512-3FgFARf7RupsZETQ1nHnhLUUvpcttcCq1iZCaVAbJZbCZ5VNRrNyvpDyHTOb0KC3llFcsyOT/a99NZcCbeiEsA=="
},
"asn1": {
"version": "0.2.4",
"resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz",
@ -2232,6 +2283,14 @@
"minimalistic-assert": "^1.0.0"
}
},
"asn1js": {
"version": "2.0.26",
"resolved": "https://registry.npmjs.org/asn1js/-/asn1js-2.0.26.tgz",
"integrity": "sha512-yG89F0j9B4B0MKIcFyWWxnpZPLaNTjCj4tkE3fjbAoo0qmpGw0PYYqSbX/4ebnd9Icn8ZgK4K1fvDyEtW1JYtQ==",
"requires": {
"pvutils": "^1.0.17"
}
},
"assert": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/assert/-/assert-1.5.0.tgz",
@ -2577,8 +2636,7 @@
"bn.js": {
"version": "4.11.8",
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.8.tgz",
"integrity": "sha512-ItfYfPLkWHUjckQCk8xC+LwxgK8NYcXywGigJgSwOP8Y2iyWT4f2vsZnoOXTTbo+o5yXmIUJ4gn5538SO5S3gA==",
"dev": true
"integrity": "sha512-ItfYfPLkWHUjckQCk8xC+LwxgK8NYcXywGigJgSwOP8Y2iyWT4f2vsZnoOXTTbo+o5yXmIUJ4gn5538SO5S3gA=="
},
"body-parser": {
"version": "1.19.0",
@ -2757,8 +2815,7 @@
"brorand": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz",
"integrity": "sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8=",
"dev": true
"integrity": "sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8="
},
"browserify-aes": {
"version": "1.2.0",
@ -4007,7 +4064,6 @@
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/des.js/-/des.js-1.0.1.tgz",
"integrity": "sha512-Q0I4pfFrv2VPd34/vfLrFOoRmlYj3OV50i7fskps1jZWK1kApMWWT9G6RRUeYedLcBDIhnSDaUvJMb3AhUlaEA==",
"dev": true,
"requires": {
"inherits": "^2.0.1",
"minimalistic-assert": "^1.0.0"
@ -4208,7 +4264,6 @@
"version": "6.5.3",
"resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.3.tgz",
"integrity": "sha512-IMqzv5wNQf+E6aHeIqATs0tOLeOTwj1QKbRcS3jBbYkl5oLAserA8yJTT7/VyHUYG91PRmPyeQDObKLPpeS4dw==",
"dev": true,
"requires": {
"bn.js": "^4.4.0",
"brorand": "^1.0.1",
@ -7046,7 +7101,6 @@
"version": "1.1.7",
"resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz",
"integrity": "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==",
"dev": true,
"requires": {
"inherits": "^2.0.3",
"minimalistic-assert": "^1.0.1"
@ -7067,7 +7121,6 @@
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz",
"integrity": "sha1-0nRXAQJabHdabFRXk+1QL8DGSaE=",
"dev": true,
"requires": {
"hash.js": "^1.0.3",
"minimalistic-assert": "^1.0.0",
@ -8953,14 +9006,12 @@
"minimalistic-assert": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz",
"integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==",
"dev": true
"integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A=="
},
"minimalistic-crypto-utils": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz",
"integrity": "sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo=",
"dev": true
"integrity": "sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo="
},
"minimatch": {
"version": "3.0.4",
@ -10288,6 +10339,26 @@
"escape-goat": "^2.0.0"
}
},
"pvtsutils": {
"version": "1.0.12",
"resolved": "https://registry.npmjs.org/pvtsutils/-/pvtsutils-1.0.12.tgz",
"integrity": "sha512-fudCcWFUE7WPHMRVdlEDdeaeLf+8hvZFvfJJ+p8GZlwrrdoiVfv7WZaPt6k7k/NZjMxR8yUbbH51hpwlSmLHiQ==",
"requires": {
"tslib": "^2.0.1"
},
"dependencies": {
"tslib": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.0.1.tgz",
"integrity": "sha512-SgIkNheinmEBgx1IUNirK0TUD4X9yjjBRTqqjggWCU3pUEqIk3/Uwl3yRixYKT6WjQuGiwDv4NomL3wqRCj+CQ=="
}
}
},
"pvutils": {
"version": "1.0.17",
"resolved": "https://registry.npmjs.org/pvutils/-/pvutils-1.0.17.tgz",
"integrity": "sha512-wLHYUQxWaXVQvKnwIDWFVKDJku9XDCvyhhxoq8dc5MFdIlRenyPI9eSfEtcvgHgD7FlvCyGAlWgOzRnZD99GZQ=="
},
"qs": {
"version": "6.5.2",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz",
@ -13937,6 +14008,49 @@
}
}
},
"webcrypto-core": {
"version": "1.1.8",
"resolved": "https://registry.npmjs.org/webcrypto-core/-/webcrypto-core-1.1.8.tgz",
"integrity": "sha512-hKnFXsqh0VloojNeTfrwFoRM4MnaWzH6vtXcaFcGjPEu+8HmBdQZnps3/2ikOFqS8bJN1RYr6mI2P/FJzyZnXg==",
"requires": {
"@peculiar/asn1-schema": "^2.0.12",
"@peculiar/json-schema": "^1.1.12",
"asn1js": "^2.0.26",
"pvtsutils": "^1.0.11",
"tslib": "^2.0.1"
},
"dependencies": {
"tslib": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.0.1.tgz",
"integrity": "sha512-SgIkNheinmEBgx1IUNirK0TUD4X9yjjBRTqqjggWCU3pUEqIk3/Uwl3yRixYKT6WjQuGiwDv4NomL3wqRCj+CQ=="
}
}
},
"webcrypto-liner": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/webcrypto-liner/-/webcrypto-liner-1.2.3.tgz",
"integrity": "sha512-e2P3hMkBVlMo1gMQnkEym0uJh+WVskQbvC3lT278ux3/OnxX7KAVaqROi1nlV9FRQBBvdadjuaWiFSLyESh4mA==",
"requires": {
"@peculiar/asn1-schema": "^2.0.3",
"@peculiar/json-schema": "^1.1.10",
"asmcrypto.js": "^2.3.2",
"asn1js": "^2.0.26",
"core-js": "^3.6.5",
"des.js": "^1.0.1",
"elliptic": "^6.5.2",
"pvtsutils": "^1.0.10",
"tslib": "^1.13.0",
"webcrypto-core": "^1.1.2"
},
"dependencies": {
"tslib": {
"version": "1.13.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.13.0.tgz",
"integrity": "sha512-i/6DQjL8Xf3be4K/E6Wgpekn5Qasl1usyw++dAA35Ue5orEn65VIxOA+YvNNl9HV3qv70T7CNwjODHZrLwvd1Q=="
}
}
},
"webpack": {
"version": "4.42.1",
"resolved": "https://registry.npmjs.org/webpack/-/webpack-4.42.1.tgz",

View File

@ -107,6 +107,7 @@
"resize-observer-polyfill": "^1.5.1",
"simplebar-react": "^2.2.0",
"twemoji": "^13.0.0",
"webcrypto-liner": "^1.2.3",
"webrtc-adapter": "^7.5.1"
}
}

View File

@ -781,11 +781,15 @@ export class ConnectionHandler {
if(currentInput) {
if(shouldRecord) {
if(this.getInputHardwareState() !== InputHardwareState.START_FAILED) {
this.startVoiceRecorder(Date.now() - this._last_record_error_popup > 10 * 1000).then(() => {});
this.startVoiceRecorder(Date.now() - this._last_record_error_popup > 10 * 1000).then(() => {
this.event_registry.fire("notify_state_updated", { state: "microphone" });
});
}
} else {
currentInput.stop().catch(error => {
logWarn(LogCategory.AUDIO, tr("Failed to stop the microphone input recorder: %o"), error);
}).then(() => {
this.event_registry.fire("notify_state_updated", { state: "microphone" });
});
}
}
@ -888,7 +892,7 @@ export class ConnectionHandler {
reconnect_properties(profile?: ConnectionProfile) : ConnectParameters {
const name = (this.getClient() ? this.getClient().clientNickName() : "") ||
(this.serverConnection && this.serverConnection.handshake_handler() ? this.serverConnection.handshake_handler().parameters.nickname : "") ||
StaticSettings.instance.static(Settings.KEY_CONNECT_USERNAME, profile ? profile.default_username : undefined) ||
StaticSettings.instance.static(Settings.KEY_CONNECT_USERNAME, profile ? profile.defaultUsername : 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 : "");
@ -1048,6 +1052,7 @@ export class ConnectionHandler {
this.client_status.input_muted = muted;
this.sound.play(muted ? Sound.MICROPHONE_MUTED : Sound.MICROPHONE_ACTIVATED);
this.update_voice_status();
this.event_registry.fire("notify_state_updated", { state: "microphone" });
}
toggleMicrophone() { this.setMicrophoneMuted(!this.isMicrophoneMuted()); }
@ -1058,6 +1063,7 @@ export class ConnectionHandler {
if(this.client_status.output_muted === muted) return;
if(muted) this.sound.play(Sound.SOUND_MUTED); /* play the sound *before* we're setting the muted state */
this.client_status.output_muted = muted;
this.event_registry.fire("notify_state_updated", { state: "speaker" });
if(!muted) this.sound.play(Sound.SOUND_ACTIVATED); /* play the sound *after* we're setting we've unmuted the sound */
this.update_voice_status();
}

View File

@ -2,7 +2,7 @@ import * as log from "./log";
import {LogCategory} from "./log";
import {guid} from "./crypto/uid";
import {createErrorModal, createInfoModal, createInputModal} from "./ui/elements/Modal";
import {default_profile, find_profile} from "./profiles/ConnectionProfile";
import {defaultConnectProfile, findConnectProfile} from "./profiles/ConnectionProfile";
import {server_connections} from "./ui/frames/connection_handlers";
import {spawnConnectModal} from "./ui/modal/ModalConnect";
import * as top_menu from "./ui/frames/MenuBar";
@ -10,7 +10,7 @@ import {control_bar_instance} from "./ui/frames/control-bar";
import {ConnectionHandler} from "./ConnectionHandler";
export const boorkmak_connect = (mark: Bookmark, new_tab?: boolean) => {
const profile = find_profile(mark.connect_profile) || default_profile();
const profile = findConnectProfile(mark.connect_profile) || defaultConnectProfile();
if(profile.valid()) {
const connection = (typeof(new_tab) !== "boolean" || !new_tab) ? server_connections.active_connection() : server_connections.spawn_server_connection();
server_connections.set_active_connection(connection);
@ -19,7 +19,7 @@ export const boorkmak_connect = (mark: Bookmark, new_tab?: boolean) => {
profile,
true,
{
nickname: mark.nickname === "Another TeaSpeak user" || !mark.nickname ? profile.connect_username() : mark.nickname,
nickname: mark.nickname === "Another TeaSpeak user" || !mark.nickname ? profile.connectUsername() : mark.nickname,
password: mark.server_properties.server_password_hash ? {
password: mark.server_properties.server_password_hash,
hashed: true

View File

@ -32,17 +32,18 @@ export class HandshakeHandler {
}
initialize() {
this.handshake_handler = this.profile.spawn_identity_handshake_handler(this.connection);
this.handshake_handler = this.profile.spawnIdentityHandshakeHandler(this.connection);
if(!this.handshake_handler) {
this.handshake_failed("failed to create identity handler");
return;
}
this.handshake_handler.register_callback((flag, message) => {
if(flag)
if(flag) {
this.handshake_finished();
else
} else {
this.handshake_failed(message);
}
});
}
@ -55,7 +56,7 @@ export class HandshakeHandler {
}
on_teamspeak() {
const type = this.profile.selected_type();
const type = this.profile.selectedType();
if(type == IdentitifyType.TEAMSPEAK)
this.handshake_finished();
else {
@ -124,8 +125,8 @@ export class HandshakeHandler {
data.client_platform = (os_mapping[os.platform()] || os.platform());
}
if(this.profile.selected_type() === IdentitifyType.TEAMSPEAK)
data["client_key_offset"] = (this.profile.selected_identity() as TeaSpeakIdentity).hash_number;
if(this.profile.selectedType() === IdentitifyType.TEAMSPEAK)
data["client_key_offset"] = (this.profile.selectedIdentity() as TeaSpeakIdentity).hash_number;
this.connection.send_command("clientinit", data).catch(error => {
if(error instanceof CommandResult) {

View File

@ -11,7 +11,9 @@ export enum VoiceConnectionStatus {
Connecting,
Connected,
Disconnecting,
Disconnected
Disconnected,
Failed
}
export interface VoiceConnectionEvents {
@ -55,6 +57,7 @@ export abstract class AbstractVoiceConnection {
}
abstract getConnectionState() : VoiceConnectionStatus;
abstract getFailedMessage() : string;
abstract encodingSupported(codec: number) : boolean;
abstract decodingSupported(codec: number) : boolean;

View File

@ -1,13 +1,12 @@
import * as loader from "tc-loader";
import {settings, Settings} from "tc-shared/settings";
import * as profiles from "tc-shared/profiles/ConnectionProfile";
import * as log from "tc-shared/log";
import {LogCategory} from "tc-shared/log";
import * as bipc from "./ipc/BrowserIPC";
import * as sound from "./sound/Sounds";
import * as i18n from "./i18n/localize";
import {tra} from "./i18n/localize";
import {ConnectionHandler} from "tc-shared/ConnectionHandler";
import {ConnectionHandler, ConnectionState} from "tc-shared/ConnectionHandler";
import {createInfoModal} from "tc-shared/ui/elements/Modal";
import * as stats from "./stats";
import * as fidentity from "./profiles/identities/TeaForumIdentity";
@ -32,7 +31,6 @@ import {MenuEntryType, spawn_context_menu} from "tc-shared/ui/elements/ContextMe
import {copy_to_clipboard} from "tc-shared/utils/helpers";
import {checkForUpdatedApp} from "tc-shared/update";
import {setupJSRender} from "tc-shared/ui/jsrender";
import ContextMenuEvent = JQuery.ContextMenuEvent;
import "svg-sprites/client-icons";
/* required import for init */
@ -44,8 +42,10 @@ import "./connection/CommandHandler";
import "./connection/ConnectionBase";
import {ConnectRequestData} from "tc-shared/ipc/ConnectHandler";
import "./video-viewer/Controller";
import "./profiles/ConnectionProfile";
import "./update/UpdaterWeb";
import ContextMenuEvent = JQuery.ContextMenuEvent;
import {defaultConnectProfile, findConnectProfile} from "tc-shared/profiles/ConnectionProfile";
async function initialize() {
try {
@ -109,8 +109,6 @@ async function initialize_app() {
});
sound.set_master_volume(settings.global(Settings.KEY_SOUND_MASTER_SOUNDS) / 100);
await profiles.load();
try {
await ppt.initialize();
} catch(error) {
@ -120,57 +118,15 @@ async function initialize_app() {
}
}
/*
class TestProxy extends bipc.MethodProxy {
constructor(params: bipc.MethodProxyConnectParameters) {
super(bipc.get_handler(), params.channel_id && params.client_id ? params : undefined);
if(!this.is_slave()) {
this.register_method(this.add_slave);
}
if(!this.is_master()) {
this.register_method(this.say_hello);
this.register_method(this.add_master);
}
}
setup() {
super.setup();
}
protected on_connected() {
log.info(LogCategory.IPC, "Test proxy connected");
}
protected on_disconnected() {
log.info(LogCategory.IPC, "Test proxy disconnected");
}
private async say_hello() : Promise<void> {
log.info(LogCategory.IPC, "Hello World");
}
private async add_slave(a: number, b: number) : Promise<number> {
return a + b;
}
private async add_master(a: number, b: number) : Promise<number> {
return a * b;
}
}
interface Window {
proxy_instance: TestProxy & {url: () => string};
}
*/
export function handle_connect_request(properties: ConnectRequestData, connection: ConnectionHandler) {
const profile_uuid = properties.profile || (profiles.default_profile() || {id: 'default'}).id;
const profile = profiles.find_profile(profile_uuid) || profiles.default_profile();
const username = properties.username || profile.connect_username();
const profile_uuid = properties.profile || (defaultConnectProfile() || { id: 'default' }).id;
const profile = findConnectProfile(profile_uuid) || defaultConnectProfile();
const username = properties.username || profile.connectUsername();
const password = properties.password ? properties.password.value : "";
const password_hashed = properties.password ? properties.password.hashed : false;
debugger;
if(profile && profile.valid()) {
connection.startConnection(properties.address, profile, true, {
nickname: username,
@ -192,21 +148,6 @@ export function handle_connect_request(properties: ConnectRequestData, connectio
}
function main() {
/*
window.proxy_instance = new TestProxy({
client_id: settings.static_global<string>("proxy_client_id", undefined),
channel_id: settings.static_global<string>("proxy_channel_id", undefined)
}) as any;
if(window.proxy_instance.is_master()) {
window.proxy_instance.setup();
window.proxy_instance.url = () => {
const data = window.proxy_instance.generate_connect_parameters();
return "proxy_channel_id=" + data.channel_id + "&proxy_client_id=" + data.client_id;
};
}
*/
//http://localhost:63343/Web-Client/index.php?_ijt=omcpmt8b9hnjlfguh8ajgrgolr&default_connect_url=true&default_connect_type=teamspeak&default_connect_url=localhost%3A9987&disableUnloadDialog=1&loader_ignore_age=1
/* initialize font */
{
const font = settings.static_global(Settings.KEY_FONT_SIZE, 14); //parseInt(getComputedStyle(document.body).fontSize)
@ -268,32 +209,6 @@ function main() {
server_connections.set_active_connection(server_connections.all_connections()[0]);
checkForUpdatedApp();
/*
(window as any).test_upload = (message?: string) => {
message = message || "Hello World";
const connection = server_connections.active_connection();
connection.fileManager.upload_file({
size: message.length,
overwrite: true,
channel: connection.getClient().currentChannel(),
name: '/HelloWorld.txt',
path: ''
}).then(key => {
const upload = new RequestFileUpload(key);
const buffer = new Uint8Array(message.length);
{
for(let index = 0; index < message.length; index++)
buffer[index] = message.charCodeAt(index);
}
upload.put_data(buffer).catch(error => {
console.error(error);
});
})
};
*/
(window as any).test_download = async () => {
const connection = server_connections.active_connection();
const download = connection.fileManager.initializeFileDownload({
@ -387,9 +302,11 @@ function main() {
//setTimeout(() => spawnPermissionEditorModal(server_connections.active_connection()), 3000);
//setTimeout(() => spawnGroupCreate(server_connections.active_connection(), "server"), 3000);
if(settings.static_global(Settings.KEY_USER_IS_NEW)) {
const modal = openModalNewcomer();
modal.close_listener.push(() => settings.changeGlobal(Settings.KEY_USER_IS_NEW, false));
if(server_connections.active_connection().getServerConnection().getConnectionState() === ConnectionState.UNCONNECTED) {
if(settings.static_global(Settings.KEY_USER_IS_NEW)) {
const modal = openModalNewcomer();
modal.close_listener.push(() => settings.changeGlobal(Settings.KEY_USER_IS_NEW, false));
}
}
//spawnVideoPopout(server_connections.active_connection(), "https://www.youtube.com/watch?v=9683D18fyvs");

View File

@ -6,33 +6,36 @@ import {AbstractServerConnection} from "../connection/ConnectionBase";
import {HandshakeIdentityHandler} from "../connection/HandshakeHandler";
import {createErrorModal} from "../ui/elements/Modal";
import {formatMessage} from "../ui/frames/chat";
import * as loader from "tc-loader";
import {Stage} from "tc-loader";
import {LogCategory, logDebug, logError} from "tc-shared/log";
export class ConnectionProfile {
id: string;
profile_name: string;
default_username: string;
default_password: string;
profileName: string;
defaultUsername: string;
defaultPassword: string;
selected_identity_type: string = "unset";
selectedIdentityType: string = "unset";
identities: { [key: string]: Identity } = {};
constructor(id: string) {
this.id = id;
}
connect_username(): string {
if (this.default_username && this.default_username !== "Another TeaSpeak user")
return this.default_username;
connectUsername(): string {
if (this.defaultUsername && this.defaultUsername !== "Another TeaSpeak user")
return this.defaultUsername;
let selected = this.selected_identity();
let selected = this.selectedIdentity();
let name = selected ? selected.fallback_name() : undefined;
return name || "Another TeaSpeak user";
}
selected_identity(current_type?: IdentitifyType): Identity {
selectedIdentity(current_type?: IdentitifyType): Identity {
if (!current_type)
current_type = this.selected_type();
current_type = this.selectedType();
if (current_type === undefined)
return undefined;
@ -46,55 +49,58 @@ export class ConnectionProfile {
return undefined;
}
selected_type?(): IdentitifyType {
return this.selected_identity_type ? IdentitifyType[this.selected_identity_type.toUpperCase()] : undefined;
selectedType(): IdentitifyType | undefined {
return this.selectedIdentityType ? IdentitifyType[this.selectedIdentityType.toUpperCase()] : undefined;
}
set_identity(type: IdentitifyType, identity: Identity) {
setIdentity(type: IdentitifyType, identity: Identity) {
this.identities[IdentitifyType[type].toLowerCase()] = identity;
}
spawn_identity_handshake_handler?(connection: AbstractServerConnection): HandshakeIdentityHandler {
const identity = this.selected_identity();
spawnIdentityHandshakeHandler(connection: AbstractServerConnection): HandshakeIdentityHandler | undefined {
const identity = this.selectedIdentity();
if (!identity)
return undefined;
return identity.spawn_identity_handshake_handler(connection);
}
encode?(): string {
encode(): string {
const identity_data = {};
for (const key in this.identities)
if (this.identities[key])
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,
username: this.defaultUsername,
password: this.defaultPassword,
profile_name: this.profileName,
identity_type: this.selectedIdentityType,
identity_data: identity_data,
id: this.id
});
}
valid(): boolean {
const identity = this.selected_identity();
const identity = this.selectedIdentity();
return !!identity && identity.valid();
}
}
async function decode_profile(data): Promise<ConnectionProfile | string> {
data = JSON.parse(data);
if (data.version !== 1)
async function decodeProfile(payload: string): Promise<ConnectionProfile | string> {
const data = JSON.parse(payload);
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();
result.defaultUsername = data.username;
result.defaultPassword = data.password;
result.profileName = data.profile_name;
result.selectedIdentityType = (data.identity_type || "").toLowerCase();
if (data.identity_data) {
for (const key of Object.keys(data.identity_data)) {
@ -117,20 +123,19 @@ interface ProfilesData {
profiles: string[];
}
let available_profiles: ConnectionProfile[] = [];
let availableProfiles_: ConnectionProfile[] = [];
export async function load() {
available_profiles = [];
async function loadConnectProfiles() {
availableProfiles_ = [];
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};
return { version: 0 };
}
})();
@ -142,56 +147,58 @@ export async function load() {
}
if (profiles_data.version == 1) {
for (const profile_data of profiles_data.profiles) {
const profile = await decode_profile(profile_data);
const profile = await decodeProfile(profile_data);
if (typeof profile === "string") {
console.error(tr("Failed to load profile. Reason: %s, Profile data: %s"), profile, profiles_data);
} else {
available_profiles.push(profile as ConnectionProfile);
availableProfiles_.push(profile as ConnectionProfile);
}
}
}
if (!find_profile("default")) { //Create a default profile and teaforo profile
const defaultProfile = findConnectProfile("default");
if (!defaultProfile) { //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";
const profile = createConnectProfile(tr("Default Profile"), "default");
profile.defaultPassword = "";
profile.defaultUsername = "";
profile.profileName = "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];
const identity = await TeaSpeakIdentity.generateNew();
const begin = Date.now();
const newLevel = await identity.improveLevelJavascript(8, () => Date.now() - begin < 1000);
/* await identity.improveLevelNative(8, 1, () => doImprove); */
logDebug(LogCategory.IDENTITIES, tr("Improved the identity level to %d within %s milliseconds"), newLevel, Date.now() - begin);
profile.setIdentity(IdentitifyType.TEAMSPEAK, identity);
profile.selectedIdentityType = IdentitifyType[IdentitifyType.TEAMSPEAK];
} catch (error) {
logError(LogCategory.GENERAL, tr("Failed to generate the default identity: %o"), 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";
const profile = createConnectProfile(tr("TeaSpeak Forum Profile"), "teaforo");
profile.defaultPassword = "";
profile.defaultUsername = "";
profile.set_identity(IdentitifyType.TEAFORO, TeaForumIdentity.identity());
profile.selected_identity_type = IdentitifyType[IdentitifyType.TEAFORO];
profile.setIdentity(IdentitifyType.TEAFORO, TeaForumIdentity.identity());
profile.selectedIdentityType = IdentitifyType[IdentitifyType.TEAFORO];
}
save();
}
}
export function create_new_profile(name: string, id?: string): ConnectionProfile {
export function createConnectProfile(name: string, id?: string): ConnectionProfile {
const profile = new ConnectionProfile(id || guid());
profile.profile_name = name;
profile.default_username = "";
available_profiles.push(profile);
profile.profileName = name;
profile.defaultUsername = "";
availableProfiles_.push(profile);
return profile;
}
@ -199,8 +206,9 @@ let _requires_save = false;
export function save() {
const profiles: string[] = [];
for (const profile of available_profiles)
for (const profile of availableProfiles_) {
profiles.push(profile.encode());
}
const data = JSON.stringify({
version: 1,
@ -217,34 +225,36 @@ export function requires_save(): boolean {
return _requires_save;
}
export function profiles(): ConnectionProfile[] {
return available_profiles;
export function availableConnectProfiles(): ConnectionProfile[] {
return availableProfiles_;
}
export function find_profile(id: string): ConnectionProfile | undefined {
for (const profile of profiles())
if (profile.id == id)
export function findConnectProfile(id: string): ConnectionProfile | undefined {
for (const profile of availableConnectProfiles()) {
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)
for (const profile of availableConnectProfiles())
if ((profile.profileName || "").toLowerCase() == name)
return profile;
return undefined;
}
export function default_profile(): ConnectionProfile {
return find_profile("default");
export function defaultConnectProfile(): ConnectionProfile {
return findConnectProfile("default");
}
export function set_default_profile(profile: ConnectionProfile) {
const old_default = default_profile();
const old_default = defaultConnectProfile();
if (old_default && old_default != profile) {
old_default.id = guid();
}
@ -253,10 +263,19 @@ export function set_default_profile(profile: ConnectionProfile) {
}
export function delete_profile(profile: ConnectionProfile) {
available_profiles.remove(profile);
availableProfiles_.remove(profile);
}
window.addEventListener("beforeunload", event => {
if(requires_save())
if(requires_save()) {
save();
});
}
});
loader.register_task(Stage.JAVASCRIPT_INITIALIZING, {
name: "Identity setup",
function: async () => {
await loadConnectProfiles();
},
priority: 30
})

View File

@ -81,9 +81,9 @@ export class HandshakeCommandHandler<T extends AbstractHandshakeIdentityHandler>
handle_command(command: ServerCommand): boolean {
if($.isFunction(this[command.command]))
if(typeof this[command.command] === "function") {
this[command.command](command.arguments);
else if(command.command == "error") {
} else if(command.command == "error") {
return false;
} else {
console.warn(tr("Received unknown command while handshaking (%o)"), command);

View File

@ -1,5 +1,5 @@
import * as log from "../../log";
import {LogCategory} from "../../log";
import {LogCategory, logDebug, logTrace} from "../../log";
import * as asn1 from "../../crypto/asn1";
import * as sha from "../../crypto/sha";
@ -217,6 +217,7 @@ export namespace CryptoHelper {
};
}
}
import arraybuffer_to_string = CryptoHelper.arraybuffer_to_string;
export class TeaSpeakHandshakeHandler extends AbstractHandshakeIdentityHandler {
identity: TeaSpeakIdentity;
@ -234,7 +235,7 @@ export class TeaSpeakHandshakeHandler extends AbstractHandshakeIdentityHandler {
this.connection.send_command("handshakebegin", {
intention: 0,
authentication_method: this.identity.type(),
publicKey: this.identity.public_key
publicKey: this.identity.publicKey
}).catch(error => {
log.error(LogCategory.IDENTITIES, tr("Failed to initialize TeamSpeak based handshake. Error: %o"), error);
@ -433,7 +434,7 @@ class IdentityPOWWorker {
}
export class TeaSpeakIdentity implements Identity {
static async generate_new() : Promise<TeaSpeakIdentity> {
static async generateNew() : Promise<TeaSpeakIdentity> {
let key: CryptoKeyPair;
try {
key = await crypto.subtle.generateKey({name:'ECDH', namedCurve: 'P-256'}, true, ["deriveKey"]);
@ -501,7 +502,7 @@ export class TeaSpeakIdentity implements Identity {
private_key: string; /* base64 representation of the private key */
_name: string;
public_key: string; /* only set when initialized */
publicKey: string; /* only set when initialized */
private _initialized: boolean;
private _crypto_key: CryptoKey;
@ -569,21 +570,25 @@ export class TeaSpeakIdentity implements Identity {
}
async level() : Promise<number> {
if(!this._initialized || !this.public_key)
if(!this._initialized || !this.publicKey)
throw "not initialized";
const hash = new Uint8Array(await sha.sha1(this.public_key + this.hash_number));
const hash = new Uint8Array(await sha.sha1(this.publicKey + this.hash_number));
return TeaSpeakIdentity.calculateLevel(hash);
}
private static calculateLevel(buffer: Uint8Array) : number {
let level = 0;
while(level < hash.byteLength && hash[level] == 0)
while(level < buffer.byteLength && buffer[level] === 0)
level++;
if(level >= hash.byteLength) {
if(level >= buffer.byteLength) {
level = 256;
} else {
let byte = hash[level];
let byte = buffer[level];
level <<= 3;
while((byte & 0x1) == 0) {
while((byte & 0x1) === 0) {
level++;
byte >>= 1;
}
@ -597,7 +602,7 @@ export class TeaSpeakIdentity implements Identity {
* @param {string} b
* @description b must be smaller (in bytes) then a
*/
private string_add(a: string, b: string) {
private static string_add(a: string, b: string) {
const char_result: number[] = [];
const char_a = [...a].reverse().map(e => e.charCodeAt(0));
const char_b = [...b].reverse().map(e => e.charCodeAt(0));
@ -628,29 +633,34 @@ export class TeaSpeakIdentity implements Identity {
let active = true;
setTimeout(() => active = false, time);
return await this.improve_level(-1, threads, () => active);
return await this.improveLevelNative(-1, threads, () => active);
}
async improve_level(target: number, threads: number, active_callback: () => boolean, callback_level?: (current: number) => any, callback_status?: (hash_rate: number) => any) : Promise<Boolean> {
if(!this._initialized || !this.public_key)
async improveLevelNative(target: number, threads: number, active_callback: () => boolean, callback_level?: (current: number) => any, callback_status?: (hash_rate: number) => any) : Promise<Boolean> {
if(!this._initialized || !this.publicKey) {
throw "not initialized";
if(target == -1) /* get the highest level possible */
}
/* get the highest level possible */
if(target == -1) {
target = 0;
else if(target <= await this.level())
} else if(target <= await this.level()) {
return true;
}
const workers: IdentityPOWWorker[] = [];
const iterations = 100000;
let current_hash;
const next_hash = () => {
if(!current_hash)
if(!current_hash) {
return (current_hash = this.hash_number);
}
if(current_hash.length < iterations.toString().length) {
current_hash = this.string_add(iterations.toString(), current_hash);
current_hash = TeaSpeakIdentity.string_add(iterations.toString(), current_hash);
} else {
current_hash = this.string_add(current_hash, iterations.toString());
current_hash = TeaSpeakIdentity.string_add(current_hash, iterations.toString());
}
return current_hash;
};
@ -661,7 +671,7 @@ export class TeaSpeakIdentity implements Identity {
for (let index = 0; index < threads; index++) {
const worker = new IdentityPOWWorker();
workers.push(worker);
initialize_promise.push(worker.initialize(this.public_key));
initialize_promise.push(worker.initialize(this.publicKey));
}
try {
@ -778,6 +788,63 @@ export class TeaSpeakIdentity implements Identity {
throw "this should never be reached";
}
/* Improve the identity within the current thread */
async improveLevelJavascript(target: number, activeCallback: () => boolean) : Promise<number> {
const publicKey = str2ab8(this.publicKey);
const buffer = new Uint8Array(publicKey.byteLength + 20); /* Max 20 append digest (Appendix is a number in range of [0;2^64]) -> log10(2^64) */
buffer.set(new Uint8Array(publicKey));
buffer.set(new Uint8Array(str2ab8(this.hash_number)), publicKey.byteLength);
const kChar9 = '9'.charCodeAt(0);
const kChar0 = '0'.charCodeAt(0);
let numberIndex = publicKey.byteLength + this.hash_number.length;
let bufferView = buffer.subarray(0, numberIndex);
const incrementCounter = () => {
let currentIndex = numberIndex - 1;
while(currentIndex > publicKey.byteLength && buffer[currentIndex] == kChar9) {
buffer[currentIndex--] = kChar0;
}
if(currentIndex > publicKey.byteLength) {
buffer[currentIndex]++;
} else {
/* yeah a new diget */
if(numberIndex >= buffer.byteLength) {
throw "hash number got too big. use another identity";
}
buffer[numberIndex] = kChar0;
numberIndex++;
buffer[currentIndex] = '1'.charCodeAt(0);
bufferView = buffer.subarray(0, numberIndex);
}
};
let currentLevel = await this.level();
let iteration = 0;
const timeBegin = Date.now();
while(currentLevel < target) {
if((iteration++ % 1000) === 0 && !activeCallback()) {
break;
}
incrementCounter();
const newLevel = await TeaSpeakIdentity.calculateLevel(new Uint8Array(await crypto.subtle.digest("SHA-1", bufferView)));
if(newLevel > currentLevel) {
this.hash_number = arraybuffer_to_string(buffer.subarray(publicKey.byteLength, numberIndex));
logTrace(LogCategory.IDENTITIES, tr("Found a new identity level at %s. Previous level %d now %d (%d hashes/second)"),
this.hash_number, currentLevel, newLevel, iteration * 1000 / (Date.now() - timeBegin));
currentLevel = newLevel;
}
}
return currentLevel;
}
private async initialize() {
if(!this.private_key)
throw "Invalid private key";
@ -806,8 +873,8 @@ export class TeaSpeakIdentity implements Identity {
}
try {
this.public_key = await CryptoHelper.export_ecc_key(this._crypto_key, true);
this._unique_id = base64_encode_ab(await sha.sha1(this.public_key));
this.publicKey = await CryptoHelper.export_ecc_key(this._crypto_key, true);
this._unique_id = base64_encode_ab(await sha.sha1(this.publicKey));
} catch(error) {
log.error(LogCategory.IDENTITIES, error);
throw "failed to calculate unique id";

View File

@ -11,7 +11,7 @@ import {
save_bookmark
} from "../../bookmarks";
import {connection_log, Regex} from "../../ui/modal/ModalConnect";
import {profiles} from "../../profiles/ConnectionProfile";
import {availableConnectProfiles} from "../../profiles/ConnectionProfile";
import {spawnYesNo} from "../../ui/modal/ModalYesNo";
import {Settings, settings} from "../../settings";
import * as log from "../../log";
@ -203,11 +203,11 @@ export function spawnBookmarkModal() {
.text("")
.css("display", "none")
);
for (const profile of profiles()) {
for (const profile of availableConnectProfiles()) {
input_connect_profile.append(
$.spawn("option")
.attr("value", profile.id)
.text(profile.profile_name)
.text(profile.profileName)
);
}
}
@ -345,7 +345,7 @@ export function spawnBookmarkModal() {
input_connect_profile.on('change', event => {
const id = input_connect_profile.val() as string;
const profile = profiles().find(e => e.id === id);
const profile = availableConnectProfiles().find(e => e.id === id);
if (profile) {
(selected_bookmark as Bookmark).connect_profile = id;
save_bookmark(selected_bookmark);

View File

@ -3,7 +3,7 @@ import * as log from "../../log";
import {LogCategory} from "../../log";
import * as loader from "tc-loader";
import {createModal} from "../../ui/elements/Modal";
import {ConnectionProfile, default_profile, find_profile, profiles} from "../../profiles/ConnectionProfile";
import {ConnectionProfile, defaultConnectProfile, findConnectProfile, availableConnectProfiles} from "../../profiles/ConnectionProfile";
import {KeyCode} from "../../PPTListener";
import * as i18nc from "../../i18n/country";
import {spawnSettingsModal} from "../../ui/modal/ModalSettings";
@ -208,18 +208,18 @@ export function spawnConnectModal(options: {
/* Connect Profiles */
{
for (const profile of profiles()) {
for (const profile of availableConnectProfiles()) {
input_profile.append(
$.spawn("option").text(profile.profile_name).val(profile.id)
$.spawn("option").text(profile.profileName).val(profile.id)
);
}
input_profile.on('change', event => {
selected_profile = find_profile(input_profile.val() as string) || default_profile();
selected_profile = findConnectProfile(input_profile.val() as string) || defaultConnectProfile();
{
settings.changeGlobal(Settings.KEY_CONNECT_USERNAME, undefined);
input_nickname
.attr('placeholder', selected_profile.connect_username() || "Another TeaSpeak user")
.attr('placeholder', selected_profile.connectUsername() || "Another TeaSpeak user")
.val("");
}

View File

@ -52,7 +52,7 @@ export function spawnTeamSpeakIdentityImprove(identity: TeaSpeakIdentity, name:
const threads = parseInt(input_threads.val() as string);
const target_level = parseInt(input_target_level.val() as string);
if (target_level == 0) {
identity.improve_level(-1, threads, () => active, current_level => {
identity.improveLevelNative(-1, threads, () => active, current_level => {
input_current_level.val(current_level);
}, hash_rate => {
input_hash_rate.val(hash_rate);
@ -63,7 +63,7 @@ export function spawnTeamSpeakIdentityImprove(identity: TeaSpeakIdentity, name:
button_start_stop.trigger('click');
});
} else {
identity.improve_level(target_level, threads, () => active, current_level => {
identity.improveLevelNative(target_level, threads, () => active, current_level => {
input_current_level.val(current_level);
}, hash_rate => {
input_hash_rate.val(hash_rate);

View File

@ -701,7 +701,7 @@ export namespace modal_settings {
error: text
});
event_registry.on("create-profile", event => {
const profile = profiles.create_new_profile(event.name);
const profile = profiles.createConnectProfile(event.name);
profiles.mark_need_save();
event_registry.fire_async("create-profile-result", {
status: "success",
@ -711,7 +711,7 @@ export namespace modal_settings {
});
event_registry.on("delete-profile", event => {
const profile = profiles.find_profile(event.profile_id);
const profile = profiles.findConnectProfile(event.profile_id);
if (!profile) {
log.warn(LogCategory.CLIENT, tr("Received profile event with unknown profile id (event: %s, id: %s)"), event.type, event.profile_id);
send_error("delete-profile-result", event.profile_id, tr("Unknown profile"));
@ -723,15 +723,15 @@ export namespace modal_settings {
});
const build_profile_info = (profile: ConnectionProfile) => {
const forum_data = profile.selected_identity(IdentitifyType.TEAFORO) as TeaForumIdentity;
const teamspeak_data = profile.selected_identity(IdentitifyType.TEAMSPEAK) as TeaSpeakIdentity;
const nickname_data = profile.selected_identity(IdentitifyType.NICKNAME) as NameIdentity;
const forum_data = profile.selectedIdentity(IdentitifyType.TEAFORO) as TeaForumIdentity;
const teamspeak_data = profile.selectedIdentity(IdentitifyType.TEAMSPEAK) as TeaSpeakIdentity;
const nickname_data = profile.selectedIdentity(IdentitifyType.NICKNAME) as NameIdentity;
return {
id: profile.id,
name: profile.profile_name,
nickname: profile.default_username,
identity_type: profile.selected_identity_type as any,
name: profile.profileName,
nickname: profile.defaultUsername,
identity_type: profile.selectedIdentityType as any,
identity_forum: !forum_data ? undefined : {
valid: forum_data.valid(),
fallback_name: forum_data.fallback_name()
@ -749,12 +749,12 @@ export namespace modal_settings {
event_registry.on("query-profile-list", event => {
event_registry.fire_async("query-profile-list-result", {
status: "success",
profiles: profiles.profiles().map(e => build_profile_info(e))
profiles: profiles.availableConnectProfiles().map(e => build_profile_info(e))
});
});
event_registry.on("query-profile", event => {
const profile = profiles.find_profile(event.profile_id);
const profile = profiles.findConnectProfile(event.profile_id);
if (!profile) {
log.warn(LogCategory.CLIENT, tr("Received profile event with unknown profile id (event: %s, id: %s)"), event.type, event.profile_id);
send_error("query-profile-result", event.profile_id, tr("Unknown profile"));
@ -769,7 +769,7 @@ export namespace modal_settings {
});
event_registry.on("set-default-profile", event => {
const profile = profiles.find_profile(event.profile_id);
const profile = profiles.findConnectProfile(event.profile_id);
if (!profile) {
log.warn(LogCategory.CLIENT, tr("Received profile event with unknown profile id (event: %s, id: %s)"), event.type, event.profile_id);
send_error("set-default-profile-result", event.profile_id, tr("Unknown profile"));
@ -785,14 +785,14 @@ export namespace modal_settings {
});
event_registry.on("set-profile-name", event => {
const profile = profiles.find_profile(event.profile_id);
const profile = profiles.findConnectProfile(event.profile_id);
if (!profile) {
log.warn(LogCategory.CLIENT, tr("Received profile event with unknown profile id (event: %s, id: %s)"), event.type, event.profile_id);
send_error("set-profile-name-result", event.profile_id, tr("Unknown profile"));
return;
}
profile.profile_name = event.name;
profile.profileName = event.name;
profiles.mark_need_save();
event_registry.fire_async("set-profile-name-result", {
name: event.name,
@ -802,14 +802,14 @@ export namespace modal_settings {
});
event_registry.on("set-default-name", event => {
const profile = profiles.find_profile(event.profile_id);
const profile = profiles.findConnectProfile(event.profile_id);
if (!profile) {
log.warn(LogCategory.CLIENT, tr("Received profile event with unknown profile id (event: %s, id: %s)"), event.type, event.profile_id);
send_error("set-default-name-result", event.profile_id, tr("Unknown profile"));
return;
}
profile.default_username = event.name;
profile.defaultUsername = event.name;
profiles.mark_need_save();
event_registry.fire_async("set-default-name-result", {
name: event.name,
@ -819,16 +819,16 @@ export namespace modal_settings {
});
event_registry.on("set-identity-name-name", event => {
const profile = profiles.find_profile(event.profile_id);
const profile = profiles.findConnectProfile(event.profile_id);
if (!profile) {
log.warn(LogCategory.CLIENT, tr("Received profile event with unknown profile id (event: %s, id: %s)"), event.type, event.profile_id);
send_error("set-identity-name-name-result", event.profile_id, tr("Unknown profile"));
return;
}
let identity = profile.selected_identity(IdentitifyType.NICKNAME) as NameIdentity;
let identity = profile.selectedIdentity(IdentitifyType.NICKNAME) as NameIdentity;
if (!identity)
profile.set_identity(IdentitifyType.NICKNAME, identity = new NameIdentity());
profile.setIdentity(IdentitifyType.NICKNAME, identity = new NameIdentity());
identity.set_name(event.name);
profiles.mark_need_save();
@ -840,7 +840,7 @@ export namespace modal_settings {
});
event_registry.on("query-profile-validity", event => {
const profile = profiles.find_profile(event.profile_id);
const profile = profiles.findConnectProfile(event.profile_id);
if (!profile) {
log.warn(LogCategory.CLIENT, tr("Received profile event with unknown profile id (event: %s, id: %s)"), event.type, event.profile_id);
send_error("query-profile-validity-result", event.profile_id, tr("Unknown profile"));
@ -855,14 +855,14 @@ export namespace modal_settings {
});
event_registry.on("query-identity-teamspeak", event => {
const profile = profiles.find_profile(event.profile_id);
const profile = profiles.findConnectProfile(event.profile_id);
if (!profile) {
log.warn(LogCategory.CLIENT, tr("Received profile event with unknown profile id (event: %s, id: %s)"), event.type, event.profile_id);
send_error("query-identity-teamspeak-result", event.profile_id, tr("Unknown profile"));
return;
}
const ts = profile.selected_identity(IdentitifyType.TEAMSPEAK) as TeaSpeakIdentity;
const ts = profile.selectedIdentity(IdentitifyType.TEAMSPEAK) as TeaSpeakIdentity;
if (!ts) {
event_registry.fire_async("query-identity-teamspeak-result", {
status: "error",
@ -884,26 +884,31 @@ export namespace modal_settings {
});
event_registry.on("select-identity-type", event => {
const profile = profiles.find_profile(event.profile_id);
if(!event.identity_type) {
/* dummy event for UI init */
return;
}
const profile = profiles.findConnectProfile(event.profile_id);
if (!profile) {
log.warn(LogCategory.CLIENT, tr("Received profile event with unknown profile id (event: %s, id: %s)"), event.type, event.profile_id);
return;
}
profile.selected_identity_type = event.identity_type;
profile.selectedIdentityType = event.identity_type;
profiles.mark_need_save();
});
event_registry.on("generate-identity-teamspeak", event => {
const profile = profiles.find_profile(event.profile_id);
const profile = profiles.findConnectProfile(event.profile_id);
if (!profile) {
log.warn(LogCategory.CLIENT, tr("Received profile event with unknown profile id (event: %s, id: %s)"), event.type, event.profile_id);
send_error("generate-identity-teamspeak-result", event.profile_id, tr("Unknown profile"));
return;
}
TeaSpeakIdentity.generate_new().then(identity => {
profile.set_identity(IdentitifyType.TEAMSPEAK, identity);
TeaSpeakIdentity.generateNew().then(identity => {
profile.setIdentity(IdentitifyType.TEAMSPEAK, identity);
profiles.mark_need_save();
identity.level().then(level => {
@ -924,14 +929,14 @@ export namespace modal_settings {
});
event_registry.on("import-identity-teamspeak", event => {
const profile = profiles.find_profile(event.profile_id);
const profile = profiles.findConnectProfile(event.profile_id);
if (!profile) {
log.warn(LogCategory.CLIENT, tr("Received profile event with unknown profile id (event: %s, id: %s)"), event.type, event.profile_id);
return;
}
spawnTeamSpeakIdentityImport(identity => {
profile.set_identity(IdentitifyType.TEAMSPEAK, identity);
profile.setIdentity(IdentitifyType.TEAMSPEAK, identity);
profiles.mark_need_save();
identity.level().catch(error => {
@ -948,16 +953,16 @@ export namespace modal_settings {
});
event_registry.on("improve-identity-teamspeak-level", event => {
const profile = profiles.find_profile(event.profile_id);
const profile = profiles.findConnectProfile(event.profile_id);
if (!profile) {
log.warn(LogCategory.CLIENT, tr("Received profile event with unknown profile id (event: %s, id: %s)"), event.type, event.profile_id);
return;
}
const identity = profile.selected_identity(IdentitifyType.TEAMSPEAK) as TeaSpeakIdentity;
const identity = profile.selectedIdentity(IdentitifyType.TEAMSPEAK) as TeaSpeakIdentity;
if (!identity) return;
spawnTeamSpeakIdentityImprove(identity, profile.profile_name).close_listener.push(() => {
spawnTeamSpeakIdentityImprove(identity, profile.profileName).close_listener.push(() => {
profiles.mark_need_save();
identity.level().then(level => {
@ -972,13 +977,13 @@ export namespace modal_settings {
});
event_registry.on("export-identity-teamspeak", event => {
const profile = profiles.find_profile(event.profile_id);
const profile = profiles.findConnectProfile(event.profile_id);
if (!profile) {
log.warn(LogCategory.CLIENT, tr("Received profile event with unknown profile id (event: %s, id: %s)"), event.type, event.profile_id);
return;
}
const identity = profile.selected_identity(IdentitifyType.TEAMSPEAK) as TeaSpeakIdentity;
const identity = profile.selectedIdentity(IdentitifyType.TEAMSPEAK) as TeaSpeakIdentity;
if (!identity) return;
identity.export_ts(true).then(data => {

View File

@ -114,6 +114,9 @@ function initializeController(connection: ConnectionHandler, events: Registry<Ec
events.fire("notify_voice_connection_state", {state: "unsupported-server"});
break;
case VoiceConnectionStatus.Failed:
events.fire("notify_voice_connection_state", {state: "failed", message: connection.getServerConnection().getVoiceConnection().getFailedMessage() });
break;
}
};

View File

@ -3,7 +3,8 @@ export type VoiceConnectionState =
| "connected"
| "disconnected"
| "unsupported-client"
| "unsupported-server";
| "unsupported-server"
| "failed";
export type TestState =
{ state: "initializing" | "running" | "stopped" | "microphone-invalid" | "unsupported" }
| { state: "start-failed", error: string };
@ -29,7 +30,8 @@ export interface EchoTestEvents {
phase: "testing" | "troubleshooting"
},
notify_voice_connection_state: {
state: VoiceConnectionState
state: VoiceConnectionState,
message?: string
},
notify_test_state: {
state: TestState

View File

@ -105,7 +105,7 @@
pointer-events: none;
opacity: 0;
background-color: #19191bcc;
background-color: #19191b;
display: flex;
flex-direction: column;
@ -120,7 +120,12 @@
&.shown {
pointer-events: all;
opacity: 1;
opacity: .87;
}
&.error {
opacity: .92;
color: #a10000;
}
}
}

View File

@ -20,11 +20,23 @@ const VoiceStateOverlay = () => {
events.fire("query_voice_connection_state");
return "loading";
});
const [message, setMessage] = useState(undefined);
events.reactUse("notify_voice_connection_state", event => setState(event.state));
events.reactUse("notify_voice_connection_state", event => {
setState(event.state);
setMessage(event.message);
});
let inner, shown = true;
let inner, shown = true, error = false;
switch (state) {
case "failed":
error = true;
inner = <a key={state}>
<Translatable>Voice connection establishment has been failed:</Translatable><br />
{message}
</a>;
break;
case "disconnected":
inner = <a key={state}><Translatable>Voice connection has been disconnected.</Translatable></a>;
break;
@ -57,7 +69,7 @@ const VoiceStateOverlay = () => {
}
return (
<div className={cssStyle.overlay + " " + (shown ? cssStyle.shown : "")}>
<div className={cssStyle.overlay + " " + (shown ? cssStyle.shown : "") + " " + (error ? cssStyle.error : "")}>
{inner}
</div>
);

View File

@ -17,14 +17,15 @@ export class AudioLibrary {
private registeredClients: {[key: number]: AudioClient} = {};
constructor() {
this.worker = new WorkerOwner(() => {
/*
* Attention don't use () => new Worker(...).
* This confuses the worker plugin and will not emit any modules
*/
this.worker = new WorkerOwner(AudioLibrary.spawnNewWorker);
}
return new Worker("./worker/index.ts", { type: "module" });
});
private static spawnNewWorker() {
/*
* Attention don't use () => new Worker(...).
* This confuses the worker plugin and will not emit any modules
*/
return new Worker("./worker/index.ts", { type: "module" });
}
async initialize() {

View File

@ -1,4 +1,6 @@
import "webrtc-adapter";
import "webcrypto-liner";
import "./index.scss";
import "./FileTransfer";

View File

@ -45,6 +45,7 @@ export class VoiceConnection extends AbstractVoiceConnection {
private readonly serverConnectionStateListener;
private connectionType: VoiceEncodeType = VoiceEncodeType.NATIVE_ENCODE;
private connectionState: VoiceConnectionStatus;
private failedConnectionMessage: string;
private localAudioStarted = false;
private connectionLostModalOpen = false;
@ -65,6 +66,8 @@ export class VoiceConnection extends AbstractVoiceConnection {
private encoderCodec: number = 5;
private lastConnectAttempt: number = 0;
constructor(connection: ServerConnection) {
super(connection);
@ -83,6 +86,10 @@ export class VoiceConnection extends AbstractVoiceConnection {
return this.connectionState;
}
getFailedMessage(): string {
return this.failedConnectionMessage;
}
destroy() {
this.connection.events.off(this.serverConnectionStateListener);
this.dropVoiceBridge();
@ -98,6 +105,10 @@ export class VoiceConnection extends AbstractVoiceConnection {
this.events.destroy();
}
reset() {
this.dropVoiceBridge();
}
async acquireVoiceRecorder(recorder: RecorderProfile | undefined, enforce?: boolean) {
if(this.currentAudioSource === recorder && !enforce)
return;
@ -154,12 +165,14 @@ export class VoiceConnection extends AbstractVoiceConnection {
return;
}
if(this.connection.getConnectionState() !== ConnectionState.CONNECTED)
if(this.connection.getConnectionState() !== ConnectionState.CONNECTED) {
return;
}
this.lastConnectAttempt = Date.now();
this.connectAttemptCounter++;
if(this.voiceBridge) {
this.voiceBridge.callback_disconnect = undefined;
this.voiceBridge.callbackDisconnect = undefined;
this.voiceBridge.disconnect();
}
@ -172,7 +185,7 @@ export class VoiceConnection extends AbstractVoiceConnection {
request: request
}, payload)))
};
this.voiceBridge.callback_disconnect = () => {
this.voiceBridge.callbackDisconnect = () => {
this.connection.client.log.log(EventType.CONNECTION_VOICE_DROPPED, { });
if(!this.connectionLostModalOpen) {
this.connectionLostModalOpen = true;
@ -181,13 +194,14 @@ export class VoiceConnection extends AbstractVoiceConnection {
modal.open();
}
logInfo(LogCategory.WEBRTC, tr("Lost voice connection to target server. Trying to reconnect."));
this.startVoiceBridge();
this.executeVoiceBridgeReconnect();
}
this.connection.client.log.log(EventType.CONNECTION_VOICE_CONNECT, { attemptCount: this.connectAttemptCounter });
this.setConnectionState(VoiceConnectionStatus.Connecting);
this.voiceBridge.connect().then(result => {
if(result.type === "success") {
this.lastConnectAttempt = 0;
this.connectAttemptCounter = 0;
this.connection.client.log.log(EventType.CONNECTION_VOICE_CONNECT_SUCCEEDED, { });
@ -204,23 +218,32 @@ export class VoiceConnection extends AbstractVoiceConnection {
} else if(result.type === "canceled") {
/* we've to do nothing here */
} else if(result.type === "failed") {
logWarn(LogCategory.VOICE, tr("Failed to setup voice bridge: %s. Reconnect: %o"), result.message, result.allowReconnect);
let doReconnect = result.allowReconnect && this.connectAttemptCounter < 5;
logWarn(LogCategory.VOICE, tr("Failed to setup voice bridge: %s. Reconnect: %o"), result.message, doReconnect);
this.connection.client.log.log(EventType.CONNECTION_VOICE_CONNECT_FAILED, {
reason: result.message,
reconnect_delay: result.allowReconnect ? 1 : 0
reconnect_delay: doReconnect ? 1 : 0
});
if(result.allowReconnect) {
this.startVoiceBridge();
if(doReconnect) {
this.executeVoiceBridgeReconnect();
} else {
this.failedConnectionMessage = result.message;
this.setConnectionState(VoiceConnectionStatus.Failed);
}
}
});
}
private executeVoiceBridgeReconnect() {
/* TODO: May some kind of incremental timeout? */
this.startVoiceBridge();
}
private dropVoiceBridge() {
if(this.voiceBridge) {
this.voiceBridge.callback_disconnect = undefined;
this.voiceBridge.callbackDisconnect = undefined;
this.voiceBridge.disconnect();
this.voiceBridge = undefined;
}
@ -296,6 +319,8 @@ export class VoiceConnection extends AbstractVoiceConnection {
if(event.newState === ConnectionState.CONNECTED) {
this.startVoiceBridge();
} else {
this.connectAttemptCounter = 0;
this.lastConnectAttempt = 0;
this.dropVoiceBridge();
}
}

View File

@ -31,7 +31,7 @@ export abstract class VoiceBridge {
callback_incoming_voice: (packet: VoicePacket) => void;
callback_incoming_whisper: (packet: VoiceWhisperPacket) => void;
callback_disconnect: () => void;
callbackDisconnect: () => void;
setMuted(flag: boolean) {
this.muted = flag;

View File

@ -1,7 +1,7 @@
import * as aplayer from "tc-backend/web/audio/player";
import * as log from "tc-shared/log";
import {LogCategory, logDebug, logError, logInfo, logTrace, logWarn} from "tc-shared/log";
import {tr} from "tc-shared/i18n/localize";
import * as log from "tc-shared/log";
import {VoiceBridge, VoiceBridgeConnectResult} from "./VoiceBridge";
export abstract class WebRTCVoiceBridge extends VoiceBridge {
@ -14,6 +14,7 @@ export abstract class WebRTCVoiceBridge extends VoiceBridge {
private whisperDataChannel: RTCDataChannel;
private cachedIceCandidates: RTCIceCandidateInit[];
private localIceCandidateCount: number;
private callbackRtcAnswer: (answer: any) => void;
@ -78,8 +79,7 @@ export abstract class WebRTCVoiceBridge extends VoiceBridge {
{
let rtcConfig: RTCConfiguration = {};
rtcConfig.iceServers = [];
rtcConfig.iceServers.push({ urls: 'stun:stun.l.google.com:19302' });
//rtcConfig.iceServers.push({ urls: "stun:stun.teaspeak.de:3478" });
rtcConfig.iceServers.push({ urls: ['stun:stun.l.google.com:19302'] });
this.rtcConnection = new RTCPeerConnection(rtcConfig);
@ -111,17 +111,23 @@ export abstract class WebRTCVoiceBridge extends VoiceBridge {
this.whisperDataChannel.binaryType = "arraybuffer";
}
/* setting a dummy connect failed handler in case the rtc peer connection changes it's state to failed */
const connectFailedPromise = new Promise((resolve, reject) => this.callbackRtcConnectFailed = reject);
const wrapWithError = <T>(promise: Promise<T>) : Promise<T> => Promise.race([ promise, connectFailedPromise ]) as any;
let offer: RTCSessionDescriptionInit;
try {
offer = await this.rtcConnection.createOffer(this.generateRtpOfferOptions());
offer = await wrapWithError(this.rtcConnection.createOffer(this.generateRtpOfferOptions()));
if(canceled.value) return;
} catch (error) {
logError(LogCategory.VOICE, tr("Failed to generate RTC offer: %o"), error);
throw tr("failed to generate local offer");
}
this.localIceCandidateCount = 0;
try {
await this.rtcConnection.setLocalDescription(offer);
await wrapWithError(this.rtcConnection.setLocalDescription(offer));
if(canceled.value) return;
} catch (error) {
logError(LogCategory.VOICE, tr("Failed to apply local description: %o"), error);
@ -140,7 +146,7 @@ export abstract class WebRTCVoiceBridge extends VoiceBridge {
sdp: offer.sdp
}
});
answer = await new Promise((resolve, reject) => {
answer = await wrapWithError(new Promise((resolve, reject) => {
const timeout = setTimeout(() => {
if(canceled.value) {
resolve();
@ -156,7 +162,7 @@ export abstract class WebRTCVoiceBridge extends VoiceBridge {
clearTimeout(timeout);
resolve(answer);
};
});
}));
if(canceled.value) return;
}
@ -165,7 +171,7 @@ export abstract class WebRTCVoiceBridge extends VoiceBridge {
}
try {
await this.rtcConnection.setRemoteDescription(new RTCSessionDescription(answer.msg));
await wrapWithError(this.rtcConnection.setRemoteDescription(new RTCSessionDescription(answer.msg)));
if(canceled.value) return;
} catch (error) {
const kParseErrorPrefix = "Failed to execute 'setRemoteDescription' on 'RTCPeerConnection': ";
@ -176,9 +182,11 @@ export abstract class WebRTCVoiceBridge extends VoiceBridge {
throw tr("failed to apply remotes description");
}
while(this.cachedIceCandidates.length > 0)
while(this.cachedIceCandidates.length > 0) {
this.registerRemoteIceCandidate(this.cachedIceCandidates.pop_front());
}
/* ATTENTION: Do not use wrapWithError from now on (this.callbackRtcConnectFailed has been changed) */
await new Promise((resolve, reject) => {
if(this.rtcConnection.connectionState === "connected") {
resolve();
@ -215,8 +223,8 @@ export abstract class WebRTCVoiceBridge extends VoiceBridge {
this.cleanupRtcResources();
this.connectionState = "unconnected";
if(this.callback_disconnect)
this.callback_disconnect();
if(this.callbackDisconnect)
this.callbackDisconnect();
}
private abortConnectionAttempt() {
@ -253,8 +261,9 @@ export abstract class WebRTCVoiceBridge extends VoiceBridge {
if(typeof this.voiceDataChannel === "undefined")
throw tr("missing main data channel");
if(this.voiceDataChannel.readyState === "open")
if(this.voiceDataChannel.readyState === "open") {
return;
}
await new Promise((resolve, reject) => {
const id = setTimeout(reject, timeout);
@ -288,7 +297,7 @@ export abstract class WebRTCVoiceBridge extends VoiceBridge {
}
private handleRtcConnectionStateChange() {
log.debug(LogCategory.WEBRTC, tr("Connection state changed to %s"), this.rtcConnection.connectionState);
log.debug(LogCategory.WEBRTC, tr("Connection state changed to %s (Local connection state: %s)"), this.rtcConnection.connectionState, this.connectionState);
switch (this.rtcConnection.connectionState) {
case "connected":
if(this.callbackRtcConnected)
@ -298,14 +307,14 @@ export abstract class WebRTCVoiceBridge extends VoiceBridge {
case "failed":
if(this.callbackRtcConnectFailed)
this.callbackRtcConnectFailed(tr("connect attempt failed"));
else if(this.callback_disconnect)
this.callback_disconnect();
else if(this.callbackDisconnect)
this.callbackDisconnect();
break;
case "disconnected":
case "closed":
if(this.callback_disconnect)
this.callback_disconnect();
if(this.callbackDisconnect)
this.callbackDisconnect();
break;
}
}
@ -319,12 +328,22 @@ export abstract class WebRTCVoiceBridge extends VoiceBridge {
}
private handleIceCandidate(event: RTCPeerConnectionIceEvent) {
if(event.candidate && event.candidate.protocol !== "tcp")
if(event.candidate && event.candidate.protocol !== "tcp") {
return;
}
if(event.candidate) {
this.localIceCandidateCount++;
log.debug(LogCategory.WEBRTC, tr("Gathered local ice candidate for stream %d: %s"), event.candidate.sdpMLineIndex, event.candidate.candidate);
this.callback_send_control_data("ice", { msg: event.candidate.toJSON() });
} else if(this.localIceCandidateCount === 0) {
logError(LogCategory.WEBRTC, tr("Failed to gather any local ice candidates... This is a fatal error."));
/* we don't allow a reconnect here since it's most the times not fixable by just trying again */
this.allowReconnect = false;
if(this.callbackRtcConnectFailed) {
this.callbackRtcConnectFailed(tr("failed to gather any local ICE candidates"));
}
} else {
log.debug(LogCategory.WEBRTC, tr("Local ICE candidate gathering finish."));
this.callback_send_control_data("ice_finish", {});