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: # 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** * **07.09.20**
- Fixed the web client for safari - Fixed the web client for safari

138
package-lock.json generated
View File

@ -1167,6 +1167,39 @@
"fastq": "^1.6.0" "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": { "@protobufjs/aspromise": {
"version": "1.1.2", "version": "1.1.2",
"resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz",
@ -1258,6 +1291,14 @@
"integrity": "sha512-/+CRPXpBDpo2RK9C68N3b2cOvO0Cf5B9aPijHsoDQTHivnGSObdOF2BRQOYjojWTDy6nQvMjmqRXIxH55VjxxA==", "integrity": "sha512-/+CRPXpBDpo2RK9C68N3b2cOvO0Cf5B9aPijHsoDQTHivnGSObdOF2BRQOYjojWTDy6nQvMjmqRXIxH55VjxxA==",
"dev": true "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": { "@types/clean-css": {
"version": "4.2.1", "version": "4.2.1",
"resolved": "https://registry.npmjs.org/@types/clean-css/-/clean-css-4.2.1.tgz", "resolved": "https://registry.npmjs.org/@types/clean-css/-/clean-css-4.2.1.tgz",
@ -1439,6 +1480,11 @@
"integrity": "sha512-KfRL3PuHmqQLOG+2tGpRO26Ctg+Cq1E01D2DMriKEATHgWLfeNDmq9e29Q9WIky0dQ3NPkd1mzYH8Lm936Z9qw==", "integrity": "sha512-KfRL3PuHmqQLOG+2tGpRO26Ctg+Cq1E01D2DMriKEATHgWLfeNDmq9e29Q9WIky0dQ3NPkd1mzYH8Lm936Z9qw==",
"dev": true "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": { "@types/react": {
"version": "16.9.26", "version": "16.9.26",
"resolved": "https://registry.npmjs.org/@types/react/-/react-16.9.26.tgz", "resolved": "https://registry.npmjs.org/@types/react/-/react-16.9.26.tgz",
@ -2212,6 +2258,11 @@
"integrity": "sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug==", "integrity": "sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug==",
"dev": true "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": { "asn1": {
"version": "0.2.4", "version": "0.2.4",
"resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz", "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz",
@ -2232,6 +2283,14 @@
"minimalistic-assert": "^1.0.0" "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": { "assert": {
"version": "1.5.0", "version": "1.5.0",
"resolved": "https://registry.npmjs.org/assert/-/assert-1.5.0.tgz", "resolved": "https://registry.npmjs.org/assert/-/assert-1.5.0.tgz",
@ -2577,8 +2636,7 @@
"bn.js": { "bn.js": {
"version": "4.11.8", "version": "4.11.8",
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.8.tgz", "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.8.tgz",
"integrity": "sha512-ItfYfPLkWHUjckQCk8xC+LwxgK8NYcXywGigJgSwOP8Y2iyWT4f2vsZnoOXTTbo+o5yXmIUJ4gn5538SO5S3gA==", "integrity": "sha512-ItfYfPLkWHUjckQCk8xC+LwxgK8NYcXywGigJgSwOP8Y2iyWT4f2vsZnoOXTTbo+o5yXmIUJ4gn5538SO5S3gA=="
"dev": true
}, },
"body-parser": { "body-parser": {
"version": "1.19.0", "version": "1.19.0",
@ -2757,8 +2815,7 @@
"brorand": { "brorand": {
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz", "resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz",
"integrity": "sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8=", "integrity": "sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8="
"dev": true
}, },
"browserify-aes": { "browserify-aes": {
"version": "1.2.0", "version": "1.2.0",
@ -4007,7 +4064,6 @@
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/des.js/-/des.js-1.0.1.tgz", "resolved": "https://registry.npmjs.org/des.js/-/des.js-1.0.1.tgz",
"integrity": "sha512-Q0I4pfFrv2VPd34/vfLrFOoRmlYj3OV50i7fskps1jZWK1kApMWWT9G6RRUeYedLcBDIhnSDaUvJMb3AhUlaEA==", "integrity": "sha512-Q0I4pfFrv2VPd34/vfLrFOoRmlYj3OV50i7fskps1jZWK1kApMWWT9G6RRUeYedLcBDIhnSDaUvJMb3AhUlaEA==",
"dev": true,
"requires": { "requires": {
"inherits": "^2.0.1", "inherits": "^2.0.1",
"minimalistic-assert": "^1.0.0" "minimalistic-assert": "^1.0.0"
@ -4208,7 +4264,6 @@
"version": "6.5.3", "version": "6.5.3",
"resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.3.tgz", "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.3.tgz",
"integrity": "sha512-IMqzv5wNQf+E6aHeIqATs0tOLeOTwj1QKbRcS3jBbYkl5oLAserA8yJTT7/VyHUYG91PRmPyeQDObKLPpeS4dw==", "integrity": "sha512-IMqzv5wNQf+E6aHeIqATs0tOLeOTwj1QKbRcS3jBbYkl5oLAserA8yJTT7/VyHUYG91PRmPyeQDObKLPpeS4dw==",
"dev": true,
"requires": { "requires": {
"bn.js": "^4.4.0", "bn.js": "^4.4.0",
"brorand": "^1.0.1", "brorand": "^1.0.1",
@ -7046,7 +7101,6 @@
"version": "1.1.7", "version": "1.1.7",
"resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz", "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz",
"integrity": "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==", "integrity": "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==",
"dev": true,
"requires": { "requires": {
"inherits": "^2.0.3", "inherits": "^2.0.3",
"minimalistic-assert": "^1.0.1" "minimalistic-assert": "^1.0.1"
@ -7067,7 +7121,6 @@
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz", "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz",
"integrity": "sha1-0nRXAQJabHdabFRXk+1QL8DGSaE=", "integrity": "sha1-0nRXAQJabHdabFRXk+1QL8DGSaE=",
"dev": true,
"requires": { "requires": {
"hash.js": "^1.0.3", "hash.js": "^1.0.3",
"minimalistic-assert": "^1.0.0", "minimalistic-assert": "^1.0.0",
@ -8953,14 +9006,12 @@
"minimalistic-assert": { "minimalistic-assert": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz",
"integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==", "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A=="
"dev": true
}, },
"minimalistic-crypto-utils": { "minimalistic-crypto-utils": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz", "resolved": "https://registry.npmjs.org/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz",
"integrity": "sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo=", "integrity": "sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo="
"dev": true
}, },
"minimatch": { "minimatch": {
"version": "3.0.4", "version": "3.0.4",
@ -10288,6 +10339,26 @@
"escape-goat": "^2.0.0" "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": { "qs": {
"version": "6.5.2", "version": "6.5.2",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", "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": { "webpack": {
"version": "4.42.1", "version": "4.42.1",
"resolved": "https://registry.npmjs.org/webpack/-/webpack-4.42.1.tgz", "resolved": "https://registry.npmjs.org/webpack/-/webpack-4.42.1.tgz",

View File

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

View File

@ -781,11 +781,15 @@ export class ConnectionHandler {
if(currentInput) { if(currentInput) {
if(shouldRecord) { if(shouldRecord) {
if(this.getInputHardwareState() !== InputHardwareState.START_FAILED) { 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 { } else {
currentInput.stop().catch(error => { currentInput.stop().catch(error => {
logWarn(LogCategory.AUDIO, tr("Failed to stop the microphone input recorder: %o"), 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 { reconnect_properties(profile?: ConnectionProfile) : ConnectParameters {
const name = (this.getClient() ? this.getClient().clientNickName() : "") || const name = (this.getClient() ? this.getClient().clientNickName() : "") ||
(this.serverConnection && this.serverConnection.handshake_handler() ? this.serverConnection.handshake_handler().parameters.nickname : "") || (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"; "Another TeaSpeak user";
const channel = (this.getClient() && this.getClient().currentChannel() ? this.getClient().currentChannel().channelId : 0) || 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 : ""); (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.client_status.input_muted = muted;
this.sound.play(muted ? Sound.MICROPHONE_MUTED : Sound.MICROPHONE_ACTIVATED); this.sound.play(muted ? Sound.MICROPHONE_MUTED : Sound.MICROPHONE_ACTIVATED);
this.update_voice_status(); this.update_voice_status();
this.event_registry.fire("notify_state_updated", { state: "microphone" });
} }
toggleMicrophone() { this.setMicrophoneMuted(!this.isMicrophoneMuted()); } toggleMicrophone() { this.setMicrophoneMuted(!this.isMicrophoneMuted()); }
@ -1058,6 +1063,7 @@ export class ConnectionHandler {
if(this.client_status.output_muted === muted) return; 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 */ 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.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 */ if(!muted) this.sound.play(Sound.SOUND_ACTIVATED); /* play the sound *after* we're setting we've unmuted the sound */
this.update_voice_status(); this.update_voice_status();
} }

View File

@ -2,7 +2,7 @@ import * as log from "./log";
import {LogCategory} from "./log"; import {LogCategory} from "./log";
import {guid} from "./crypto/uid"; import {guid} from "./crypto/uid";
import {createErrorModal, createInfoModal, createInputModal} from "./ui/elements/Modal"; 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 {server_connections} from "./ui/frames/connection_handlers";
import {spawnConnectModal} from "./ui/modal/ModalConnect"; import {spawnConnectModal} from "./ui/modal/ModalConnect";
import * as top_menu from "./ui/frames/MenuBar"; 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"; import {ConnectionHandler} from "./ConnectionHandler";
export const boorkmak_connect = (mark: Bookmark, new_tab?: boolean) => { 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()) { if(profile.valid()) {
const connection = (typeof(new_tab) !== "boolean" || !new_tab) ? server_connections.active_connection() : server_connections.spawn_server_connection(); const connection = (typeof(new_tab) !== "boolean" || !new_tab) ? server_connections.active_connection() : server_connections.spawn_server_connection();
server_connections.set_active_connection(connection); server_connections.set_active_connection(connection);
@ -19,7 +19,7 @@ export const boorkmak_connect = (mark: Bookmark, new_tab?: boolean) => {
profile, profile,
true, 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 ? {
password: mark.server_properties.server_password_hash, password: mark.server_properties.server_password_hash,
hashed: true hashed: true

View File

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

View File

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

View File

@ -1,13 +1,12 @@
import * as loader from "tc-loader"; import * as loader from "tc-loader";
import {settings, Settings} from "tc-shared/settings"; import {settings, Settings} from "tc-shared/settings";
import * as profiles from "tc-shared/profiles/ConnectionProfile";
import * as log from "tc-shared/log"; import * as log from "tc-shared/log";
import {LogCategory} from "tc-shared/log"; import {LogCategory} from "tc-shared/log";
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 {tra} 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 {createInfoModal} from "tc-shared/ui/elements/Modal";
import * as stats from "./stats"; import * as stats from "./stats";
import * as fidentity from "./profiles/identities/TeaForumIdentity"; 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 {copy_to_clipboard} from "tc-shared/utils/helpers";
import {checkForUpdatedApp} from "tc-shared/update"; import {checkForUpdatedApp} from "tc-shared/update";
import {setupJSRender} from "tc-shared/ui/jsrender"; import {setupJSRender} from "tc-shared/ui/jsrender";
import ContextMenuEvent = JQuery.ContextMenuEvent;
import "svg-sprites/client-icons"; import "svg-sprites/client-icons";
/* required import for init */ /* required import for init */
@ -44,8 +42,10 @@ import "./connection/CommandHandler";
import "./connection/ConnectionBase"; import "./connection/ConnectionBase";
import {ConnectRequestData} from "tc-shared/ipc/ConnectHandler"; import {ConnectRequestData} from "tc-shared/ipc/ConnectHandler";
import "./video-viewer/Controller"; import "./video-viewer/Controller";
import "./profiles/ConnectionProfile";
import "./update/UpdaterWeb"; import "./update/UpdaterWeb";
import ContextMenuEvent = JQuery.ContextMenuEvent;
import {defaultConnectProfile, findConnectProfile} from "tc-shared/profiles/ConnectionProfile";
async function initialize() { async function initialize() {
try { try {
@ -109,8 +109,6 @@ async function initialize_app() {
}); });
sound.set_master_volume(settings.global(Settings.KEY_SOUND_MASTER_SOUNDS) / 100); sound.set_master_volume(settings.global(Settings.KEY_SOUND_MASTER_SOUNDS) / 100);
await profiles.load();
try { try {
await ppt.initialize(); await ppt.initialize();
} catch(error) { } 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) { export function handle_connect_request(properties: ConnectRequestData, connection: ConnectionHandler) {
const profile_uuid = properties.profile || (profiles.default_profile() || {id: 'default'}).id; const profile_uuid = properties.profile || (defaultConnectProfile() || { id: 'default' }).id;
const profile = profiles.find_profile(profile_uuid) || profiles.default_profile(); const profile = findConnectProfile(profile_uuid) || defaultConnectProfile();
const username = properties.username || profile.connect_username(); const username = properties.username || profile.connectUsername();
const password = properties.password ? properties.password.value : ""; const password = properties.password ? properties.password.value : "";
const password_hashed = properties.password ? properties.password.hashed : false; const password_hashed = properties.password ? properties.password.hashed : false;
debugger;
if(profile && profile.valid()) { if(profile && profile.valid()) {
connection.startConnection(properties.address, profile, true, { connection.startConnection(properties.address, profile, true, {
nickname: username, nickname: username,
@ -192,21 +148,6 @@ export function handle_connect_request(properties: ConnectRequestData, connectio
} }
function main() { 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 */ /* initialize font */
{ {
const font = settings.static_global(Settings.KEY_FONT_SIZE, 14); //parseInt(getComputedStyle(document.body).fontSize) 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]); server_connections.set_active_connection(server_connections.all_connections()[0]);
checkForUpdatedApp(); 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 () => { (window as any).test_download = async () => {
const connection = server_connections.active_connection(); const connection = server_connections.active_connection();
const download = connection.fileManager.initializeFileDownload({ const download = connection.fileManager.initializeFileDownload({
@ -387,10 +302,12 @@ function main() {
//setTimeout(() => spawnPermissionEditorModal(server_connections.active_connection()), 3000); //setTimeout(() => spawnPermissionEditorModal(server_connections.active_connection()), 3000);
//setTimeout(() => spawnGroupCreate(server_connections.active_connection(), "server"), 3000); //setTimeout(() => spawnGroupCreate(server_connections.active_connection(), "server"), 3000);
if(server_connections.active_connection().getServerConnection().getConnectionState() === ConnectionState.UNCONNECTED) {
if(settings.static_global(Settings.KEY_USER_IS_NEW)) { if(settings.static_global(Settings.KEY_USER_IS_NEW)) {
const modal = openModalNewcomer(); const modal = openModalNewcomer();
modal.close_listener.push(() => settings.changeGlobal(Settings.KEY_USER_IS_NEW, false)); modal.close_listener.push(() => settings.changeGlobal(Settings.KEY_USER_IS_NEW, false));
} }
}
//spawnVideoPopout(server_connections.active_connection(), "https://www.youtube.com/watch?v=9683D18fyvs"); //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 {HandshakeIdentityHandler} from "../connection/HandshakeHandler";
import {createErrorModal} from "../ui/elements/Modal"; import {createErrorModal} from "../ui/elements/Modal";
import {formatMessage} from "../ui/frames/chat"; 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 { export class ConnectionProfile {
id: string; id: string;
profile_name: string; profileName: string;
default_username: string; defaultUsername: string;
default_password: string; defaultPassword: string;
selected_identity_type: string = "unset"; selectedIdentityType: string = "unset";
identities: { [key: string]: Identity } = {}; identities: { [key: string]: Identity } = {};
constructor(id: string) { constructor(id: string) {
this.id = id; this.id = id;
} }
connect_username(): string { connectUsername(): string {
if (this.default_username && this.default_username !== "Another TeaSpeak user") if (this.defaultUsername && this.defaultUsername !== "Another TeaSpeak user")
return this.default_username; return this.defaultUsername;
let selected = this.selected_identity(); let selected = this.selectedIdentity();
let name = selected ? selected.fallback_name() : undefined; let name = selected ? selected.fallback_name() : undefined;
return name || "Another TeaSpeak user"; return name || "Another TeaSpeak user";
} }
selected_identity(current_type?: IdentitifyType): Identity { selectedIdentity(current_type?: IdentitifyType): Identity {
if (!current_type) if (!current_type)
current_type = this.selected_type(); current_type = this.selectedType();
if (current_type === undefined) if (current_type === undefined)
return undefined; return undefined;
@ -46,55 +49,58 @@ export class ConnectionProfile {
return undefined; return undefined;
} }
selected_type?(): IdentitifyType { selectedType(): IdentitifyType | undefined {
return this.selected_identity_type ? IdentitifyType[this.selected_identity_type.toUpperCase()] : 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; this.identities[IdentitifyType[type].toLowerCase()] = identity;
} }
spawn_identity_handshake_handler?(connection: AbstractServerConnection): HandshakeIdentityHandler { spawnIdentityHandshakeHandler(connection: AbstractServerConnection): HandshakeIdentityHandler | undefined {
const identity = this.selected_identity(); const identity = this.selectedIdentity();
if (!identity) if (!identity)
return undefined; return undefined;
return identity.spawn_identity_handshake_handler(connection); return identity.spawn_identity_handshake_handler(connection);
} }
encode?(): string { encode(): string {
const identity_data = {}; const identity_data = {};
for (const key in this.identities) for (const key in this.identities) {
if (this.identities[key]) if (this.identities[key]) {
identity_data[key] = this.identities[key].encode(); identity_data[key] = this.identities[key].encode();
}
}
return JSON.stringify({ return JSON.stringify({
version: 1, version: 1,
username: this.default_username, username: this.defaultUsername,
password: this.default_password, password: this.defaultPassword,
profile_name: this.profile_name, profile_name: this.profileName,
identity_type: this.selected_identity_type, identity_type: this.selectedIdentityType,
identity_data: identity_data, identity_data: identity_data,
id: this.id id: this.id
}); });
} }
valid(): boolean { valid(): boolean {
const identity = this.selected_identity(); const identity = this.selectedIdentity();
return !!identity && identity.valid(); return !!identity && identity.valid();
} }
} }
async function decode_profile(data): Promise<ConnectionProfile | string> { async function decodeProfile(payload: string): Promise<ConnectionProfile | string> {
data = JSON.parse(data); const data = JSON.parse(payload);
if (data.version !== 1) if (data.version !== 1) {
return "invalid version"; return "invalid version";
}
const result: ConnectionProfile = new ConnectionProfile(data.id); const result: ConnectionProfile = new ConnectionProfile(data.id);
result.default_username = data.username; result.defaultUsername = data.username;
result.default_password = data.password; result.defaultPassword = data.password;
result.profile_name = data.profile_name; result.profileName = data.profile_name;
result.selected_identity_type = (data.identity_type || "").toLowerCase(); result.selectedIdentityType = (data.identity_type || "").toLowerCase();
if (data.identity_data) { if (data.identity_data) {
for (const key of Object.keys(data.identity_data)) { for (const key of Object.keys(data.identity_data)) {
@ -117,17 +123,16 @@ interface ProfilesData {
profiles: string[]; profiles: string[];
} }
let available_profiles: ConnectionProfile[] = []; let availableProfiles_: ConnectionProfile[] = [];
export async function load() { async function loadConnectProfiles() {
available_profiles = []; availableProfiles_ = [];
const profiles_json = localStorage.getItem("profiles"); const profiles_json = localStorage.getItem("profiles");
let profiles_data: ProfilesData = (() => { let profiles_data: ProfilesData = (() => {
try { try {
return profiles_json ? JSON.parse(profiles_json) : {version: 0} as any; return profiles_json ? JSON.parse(profiles_json) : {version: 0} as any;
} catch (error) { } catch (error) {
debugger;
console.error(tr("Invalid profile json! Resetting profiles :( (%o)"), profiles_json); 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(); 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) { if (profiles_data.version == 1) {
for (const profile_data of profiles_data.profiles) { 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") { if (typeof profile === "string") {
console.error(tr("Failed to load profile. Reason: %s, Profile data: %s"), profile, profiles_data); console.error(tr("Failed to load profile. Reason: %s, Profile data: %s"), profile, profiles_data);
} else { } 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"); const profile = createConnectProfile(tr("Default Profile"), "default");
profile.default_password = ""; profile.defaultPassword = "";
profile.default_username = ""; profile.defaultUsername = "";
profile.profile_name = "Default Profile"; profile.profileName = "Default Profile";
/* generate default identity */ /* generate default identity */
try { try {
const identity = await TeaSpeakIdentity.generate_new(); const identity = await TeaSpeakIdentity.generateNew();
let active = true; const begin = Date.now();
setTimeout(() => {
active = false; const newLevel = await identity.improveLevelJavascript(8, () => Date.now() - begin < 1000);
}, 1000); /* await identity.improveLevelNative(8, 1, () => doImprove); */
await identity.improve_level(8, 1, () => active); logDebug(LogCategory.IDENTITIES, tr("Improved the identity level to %d within %s milliseconds"), newLevel, Date.now() - begin);
profile.set_identity(IdentitifyType.TEAMSPEAK, identity);
profile.selected_identity_type = IdentitifyType[IdentitifyType.TEAMSPEAK]; profile.setIdentity(IdentitifyType.TEAMSPEAK, identity);
profile.selectedIdentityType = IdentitifyType[IdentitifyType.TEAMSPEAK];
} catch (error) { } 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(); 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) */ { /* forum identity (works only when connected to the forum) */
const profile = create_new_profile("TeaSpeak Forum", "teaforo"); const profile = createConnectProfile(tr("TeaSpeak Forum Profile"), "teaforo");
profile.default_password = ""; profile.defaultPassword = "";
profile.default_username = ""; profile.defaultUsername = "";
profile.profile_name = "TeaSpeak Forum profile";
profile.set_identity(IdentitifyType.TEAFORO, TeaForumIdentity.identity()); profile.setIdentity(IdentitifyType.TEAFORO, TeaForumIdentity.identity());
profile.selected_identity_type = IdentitifyType[IdentitifyType.TEAFORO]; profile.selectedIdentityType = IdentitifyType[IdentitifyType.TEAFORO];
} }
save(); 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()); const profile = new ConnectionProfile(id || guid());
profile.profile_name = name; profile.profileName = name;
profile.default_username = ""; profile.defaultUsername = "";
available_profiles.push(profile); availableProfiles_.push(profile);
return profile; return profile;
} }
@ -199,8 +206,9 @@ let _requires_save = false;
export function save() { export function save() {
const profiles: string[] = []; const profiles: string[] = [];
for (const profile of available_profiles) for (const profile of availableProfiles_) {
profiles.push(profile.encode()); profiles.push(profile.encode());
}
const data = JSON.stringify({ const data = JSON.stringify({
version: 1, version: 1,
@ -217,34 +225,36 @@ export function requires_save(): boolean {
return _requires_save; return _requires_save;
} }
export function profiles(): ConnectionProfile[] { export function availableConnectProfiles(): ConnectionProfile[] {
return available_profiles; return availableProfiles_;
} }
export function find_profile(id: string): ConnectionProfile | undefined { export function findConnectProfile(id: string): ConnectionProfile | undefined {
for (const profile of profiles()) for (const profile of availableConnectProfiles()) {
if (profile.id == id) if (profile.id == id) {
return profile; return profile;
}
}
return undefined; return undefined;
} }
export function find_profile_by_name(name: string): ConnectionProfile | undefined { export function find_profile_by_name(name: string): ConnectionProfile | undefined {
name = name.toLowerCase(); name = name.toLowerCase();
for (const profile of profiles()) for (const profile of availableConnectProfiles())
if ((profile.profile_name || "").toLowerCase() == name) if ((profile.profileName || "").toLowerCase() == name)
return profile; return profile;
return undefined; return undefined;
} }
export function default_profile(): ConnectionProfile { export function defaultConnectProfile(): ConnectionProfile {
return find_profile("default"); return findConnectProfile("default");
} }
export function set_default_profile(profile: ConnectionProfile) { export function set_default_profile(profile: ConnectionProfile) {
const old_default = default_profile(); const old_default = defaultConnectProfile();
if (old_default && old_default != profile) { if (old_default && old_default != profile) {
old_default.id = guid(); old_default.id = guid();
} }
@ -253,10 +263,19 @@ export function set_default_profile(profile: ConnectionProfile) {
} }
export function delete_profile(profile: ConnectionProfile) { export function delete_profile(profile: ConnectionProfile) {
available_profiles.remove(profile); availableProfiles_.remove(profile);
} }
window.addEventListener("beforeunload", event => { window.addEventListener("beforeunload", event => {
if(requires_save()) if(requires_save()) {
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 { handle_command(command: ServerCommand): boolean {
if($.isFunction(this[command.command])) if(typeof this[command.command] === "function") {
this[command.command](command.arguments); this[command.command](command.arguments);
else if(command.command == "error") { } else if(command.command == "error") {
return false; return false;
} else { } else {
console.warn(tr("Received unknown command while handshaking (%o)"), command); console.warn(tr("Received unknown command while handshaking (%o)"), command);

View File

@ -1,5 +1,5 @@
import * as log from "../../log"; import * as log from "../../log";
import {LogCategory} from "../../log"; import {LogCategory, logDebug, logTrace} from "../../log";
import * as asn1 from "../../crypto/asn1"; import * as asn1 from "../../crypto/asn1";
import * as sha from "../../crypto/sha"; 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 { export class TeaSpeakHandshakeHandler extends AbstractHandshakeIdentityHandler {
identity: TeaSpeakIdentity; identity: TeaSpeakIdentity;
@ -234,7 +235,7 @@ export class TeaSpeakHandshakeHandler extends AbstractHandshakeIdentityHandler {
this.connection.send_command("handshakebegin", { this.connection.send_command("handshakebegin", {
intention: 0, intention: 0,
authentication_method: this.identity.type(), authentication_method: this.identity.type(),
publicKey: this.identity.public_key publicKey: this.identity.publicKey
}).catch(error => { }).catch(error => {
log.error(LogCategory.IDENTITIES, tr("Failed to initialize TeamSpeak based handshake. Error: %o"), 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 { export class TeaSpeakIdentity implements Identity {
static async generate_new() : Promise<TeaSpeakIdentity> { static async generateNew() : Promise<TeaSpeakIdentity> {
let key: CryptoKeyPair; let key: CryptoKeyPair;
try { try {
key = await crypto.subtle.generateKey({name:'ECDH', namedCurve: 'P-256'}, true, ["deriveKey"]); 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 */ private_key: string; /* base64 representation of the private key */
_name: string; _name: string;
public_key: string; /* only set when initialized */ publicKey: string; /* only set when initialized */
private _initialized: boolean; private _initialized: boolean;
private _crypto_key: CryptoKey; private _crypto_key: CryptoKey;
@ -569,21 +570,25 @@ export class TeaSpeakIdentity implements Identity {
} }
async level() : Promise<number> { async level() : Promise<number> {
if(!this._initialized || !this.public_key) if(!this._initialized || !this.publicKey)
throw "not initialized"; 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; let level = 0;
while(level < hash.byteLength && hash[level] == 0) while(level < buffer.byteLength && buffer[level] === 0)
level++; level++;
if(level >= hash.byteLength) { if(level >= buffer.byteLength) {
level = 256; level = 256;
} else { } else {
let byte = hash[level]; let byte = buffer[level];
level <<= 3; level <<= 3;
while((byte & 0x1) == 0) { while((byte & 0x1) === 0) {
level++; level++;
byte >>= 1; byte >>= 1;
} }
@ -597,7 +602,7 @@ export class TeaSpeakIdentity implements Identity {
* @param {string} b * @param {string} b
* @description b must be smaller (in bytes) then a * @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_result: number[] = [];
const char_a = [...a].reverse().map(e => e.charCodeAt(0)); const char_a = [...a].reverse().map(e => e.charCodeAt(0));
const char_b = [...b].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; let active = true;
setTimeout(() => active = false, time); 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> { 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.public_key) if(!this._initialized || !this.publicKey) {
throw "not initialized"; throw "not initialized";
if(target == -1) /* get the highest level possible */ }
/* get the highest level possible */
if(target == -1) {
target = 0; target = 0;
else if(target <= await this.level()) } else if(target <= await this.level()) {
return true; return true;
}
const workers: IdentityPOWWorker[] = []; const workers: IdentityPOWWorker[] = [];
const iterations = 100000; const iterations = 100000;
let current_hash; let current_hash;
const next_hash = () => { const next_hash = () => {
if(!current_hash) if(!current_hash) {
return (current_hash = this.hash_number); return (current_hash = this.hash_number);
}
if(current_hash.length < iterations.toString().length) { 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 { } else {
current_hash = this.string_add(current_hash, iterations.toString()); current_hash = TeaSpeakIdentity.string_add(current_hash, iterations.toString());
} }
return current_hash; return current_hash;
}; };
@ -661,7 +671,7 @@ export class TeaSpeakIdentity implements Identity {
for (let index = 0; index < threads; index++) { for (let index = 0; index < threads; index++) {
const worker = new IdentityPOWWorker(); const worker = new IdentityPOWWorker();
workers.push(worker); workers.push(worker);
initialize_promise.push(worker.initialize(this.public_key)); initialize_promise.push(worker.initialize(this.publicKey));
} }
try { try {
@ -778,6 +788,63 @@ export class TeaSpeakIdentity implements Identity {
throw "this should never be reached"; 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() { private async initialize() {
if(!this.private_key) if(!this.private_key)
throw "Invalid private key"; throw "Invalid private key";
@ -806,8 +873,8 @@ export class TeaSpeakIdentity implements Identity {
} }
try { try {
this.public_key = await CryptoHelper.export_ecc_key(this._crypto_key, true); this.publicKey = await CryptoHelper.export_ecc_key(this._crypto_key, true);
this._unique_id = base64_encode_ab(await sha.sha1(this.public_key)); this._unique_id = base64_encode_ab(await sha.sha1(this.publicKey));
} catch(error) { } catch(error) {
log.error(LogCategory.IDENTITIES, error); log.error(LogCategory.IDENTITIES, error);
throw "failed to calculate unique id"; throw "failed to calculate unique id";

View File

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

View File

@ -3,7 +3,7 @@ import * as log from "../../log";
import {LogCategory} from "../../log"; import {LogCategory} from "../../log";
import * as loader from "tc-loader"; import * as loader from "tc-loader";
import {createModal} from "../../ui/elements/Modal"; 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 {KeyCode} from "../../PPTListener";
import * as i18nc from "../../i18n/country"; import * as i18nc from "../../i18n/country";
import {spawnSettingsModal} from "../../ui/modal/ModalSettings"; import {spawnSettingsModal} from "../../ui/modal/ModalSettings";
@ -208,18 +208,18 @@ export function spawnConnectModal(options: {
/* Connect Profiles */ /* Connect Profiles */
{ {
for (const profile of profiles()) { for (const profile of availableConnectProfiles()) {
input_profile.append( 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 => { 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); settings.changeGlobal(Settings.KEY_CONNECT_USERNAME, undefined);
input_nickname input_nickname
.attr('placeholder', selected_profile.connect_username() || "Another TeaSpeak user") .attr('placeholder', selected_profile.connectUsername() || "Another TeaSpeak user")
.val(""); .val("");
} }

View File

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

View File

@ -701,7 +701,7 @@ export namespace modal_settings {
error: text error: text
}); });
event_registry.on("create-profile", event => { event_registry.on("create-profile", event => {
const profile = profiles.create_new_profile(event.name); const profile = profiles.createConnectProfile(event.name);
profiles.mark_need_save(); profiles.mark_need_save();
event_registry.fire_async("create-profile-result", { event_registry.fire_async("create-profile-result", {
status: "success", status: "success",
@ -711,7 +711,7 @@ export namespace modal_settings {
}); });
event_registry.on("delete-profile", event => { event_registry.on("delete-profile", event => {
const profile = profiles.find_profile(event.profile_id); const profile = profiles.findConnectProfile(event.profile_id);
if (!profile) { if (!profile) {
log.warn(LogCategory.CLIENT, tr("Received profile event with unknown profile id (event: %s, id: %s)"), event.type, event.profile_id); 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")); 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 build_profile_info = (profile: ConnectionProfile) => {
const forum_data = profile.selected_identity(IdentitifyType.TEAFORO) as TeaForumIdentity; const forum_data = profile.selectedIdentity(IdentitifyType.TEAFORO) as TeaForumIdentity;
const teamspeak_data = profile.selected_identity(IdentitifyType.TEAMSPEAK) as TeaSpeakIdentity; const teamspeak_data = profile.selectedIdentity(IdentitifyType.TEAMSPEAK) as TeaSpeakIdentity;
const nickname_data = profile.selected_identity(IdentitifyType.NICKNAME) as NameIdentity; const nickname_data = profile.selectedIdentity(IdentitifyType.NICKNAME) as NameIdentity;
return { return {
id: profile.id, id: profile.id,
name: profile.profile_name, name: profile.profileName,
nickname: profile.default_username, nickname: profile.defaultUsername,
identity_type: profile.selected_identity_type as any, identity_type: profile.selectedIdentityType as any,
identity_forum: !forum_data ? undefined : { identity_forum: !forum_data ? undefined : {
valid: forum_data.valid(), valid: forum_data.valid(),
fallback_name: forum_data.fallback_name() fallback_name: forum_data.fallback_name()
@ -749,12 +749,12 @@ export namespace modal_settings {
event_registry.on("query-profile-list", event => { event_registry.on("query-profile-list", event => {
event_registry.fire_async("query-profile-list-result", { event_registry.fire_async("query-profile-list-result", {
status: "success", 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 => { event_registry.on("query-profile", event => {
const profile = profiles.find_profile(event.profile_id); const profile = profiles.findConnectProfile(event.profile_id);
if (!profile) { if (!profile) {
log.warn(LogCategory.CLIENT, tr("Received profile event with unknown profile id (event: %s, id: %s)"), event.type, event.profile_id); 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")); 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 => { event_registry.on("set-default-profile", event => {
const profile = profiles.find_profile(event.profile_id); const profile = profiles.findConnectProfile(event.profile_id);
if (!profile) { if (!profile) {
log.warn(LogCategory.CLIENT, tr("Received profile event with unknown profile id (event: %s, id: %s)"), event.type, event.profile_id); 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")); 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 => { event_registry.on("set-profile-name", event => {
const profile = profiles.find_profile(event.profile_id); const profile = profiles.findConnectProfile(event.profile_id);
if (!profile) { if (!profile) {
log.warn(LogCategory.CLIENT, tr("Received profile event with unknown profile id (event: %s, id: %s)"), event.type, event.profile_id); 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")); send_error("set-profile-name-result", event.profile_id, tr("Unknown profile"));
return; return;
} }
profile.profile_name = event.name; profile.profileName = event.name;
profiles.mark_need_save(); profiles.mark_need_save();
event_registry.fire_async("set-profile-name-result", { event_registry.fire_async("set-profile-name-result", {
name: event.name, name: event.name,
@ -802,14 +802,14 @@ export namespace modal_settings {
}); });
event_registry.on("set-default-name", event => { event_registry.on("set-default-name", event => {
const profile = profiles.find_profile(event.profile_id); const profile = profiles.findConnectProfile(event.profile_id);
if (!profile) { if (!profile) {
log.warn(LogCategory.CLIENT, tr("Received profile event with unknown profile id (event: %s, id: %s)"), event.type, event.profile_id); 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")); send_error("set-default-name-result", event.profile_id, tr("Unknown profile"));
return; return;
} }
profile.default_username = event.name; profile.defaultUsername = event.name;
profiles.mark_need_save(); profiles.mark_need_save();
event_registry.fire_async("set-default-name-result", { event_registry.fire_async("set-default-name-result", {
name: event.name, name: event.name,
@ -819,16 +819,16 @@ export namespace modal_settings {
}); });
event_registry.on("set-identity-name-name", event => { 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) { if (!profile) {
log.warn(LogCategory.CLIENT, tr("Received profile event with unknown profile id (event: %s, id: %s)"), event.type, event.profile_id); 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")); send_error("set-identity-name-name-result", event.profile_id, tr("Unknown profile"));
return; return;
} }
let identity = profile.selected_identity(IdentitifyType.NICKNAME) as NameIdentity; let identity = profile.selectedIdentity(IdentitifyType.NICKNAME) as NameIdentity;
if (!identity) if (!identity)
profile.set_identity(IdentitifyType.NICKNAME, identity = new NameIdentity()); profile.setIdentity(IdentitifyType.NICKNAME, identity = new NameIdentity());
identity.set_name(event.name); identity.set_name(event.name);
profiles.mark_need_save(); profiles.mark_need_save();
@ -840,7 +840,7 @@ export namespace modal_settings {
}); });
event_registry.on("query-profile-validity", event => { event_registry.on("query-profile-validity", event => {
const profile = profiles.find_profile(event.profile_id); const profile = profiles.findConnectProfile(event.profile_id);
if (!profile) { if (!profile) {
log.warn(LogCategory.CLIENT, tr("Received profile event with unknown profile id (event: %s, id: %s)"), event.type, event.profile_id); 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")); 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 => { event_registry.on("query-identity-teamspeak", event => {
const profile = profiles.find_profile(event.profile_id); const profile = profiles.findConnectProfile(event.profile_id);
if (!profile) { if (!profile) {
log.warn(LogCategory.CLIENT, tr("Received profile event with unknown profile id (event: %s, id: %s)"), event.type, event.profile_id); 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")); send_error("query-identity-teamspeak-result", event.profile_id, tr("Unknown profile"));
return; return;
} }
const ts = profile.selected_identity(IdentitifyType.TEAMSPEAK) as TeaSpeakIdentity; const ts = profile.selectedIdentity(IdentitifyType.TEAMSPEAK) as TeaSpeakIdentity;
if (!ts) { if (!ts) {
event_registry.fire_async("query-identity-teamspeak-result", { event_registry.fire_async("query-identity-teamspeak-result", {
status: "error", status: "error",
@ -884,26 +884,31 @@ export namespace modal_settings {
}); });
event_registry.on("select-identity-type", event => { 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) { if (!profile) {
log.warn(LogCategory.CLIENT, tr("Received profile event with unknown profile id (event: %s, id: %s)"), event.type, event.profile_id); log.warn(LogCategory.CLIENT, tr("Received profile event with unknown profile id (event: %s, id: %s)"), event.type, event.profile_id);
return; return;
} }
profile.selected_identity_type = event.identity_type; profile.selectedIdentityType = event.identity_type;
profiles.mark_need_save(); profiles.mark_need_save();
}); });
event_registry.on("generate-identity-teamspeak", event => { event_registry.on("generate-identity-teamspeak", event => {
const profile = profiles.find_profile(event.profile_id); const profile = profiles.findConnectProfile(event.profile_id);
if (!profile) { if (!profile) {
log.warn(LogCategory.CLIENT, tr("Received profile event with unknown profile id (event: %s, id: %s)"), event.type, event.profile_id); 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")); send_error("generate-identity-teamspeak-result", event.profile_id, tr("Unknown profile"));
return; return;
} }
TeaSpeakIdentity.generate_new().then(identity => { TeaSpeakIdentity.generateNew().then(identity => {
profile.set_identity(IdentitifyType.TEAMSPEAK, identity); profile.setIdentity(IdentitifyType.TEAMSPEAK, identity);
profiles.mark_need_save(); profiles.mark_need_save();
identity.level().then(level => { identity.level().then(level => {
@ -924,14 +929,14 @@ export namespace modal_settings {
}); });
event_registry.on("import-identity-teamspeak", event => { event_registry.on("import-identity-teamspeak", event => {
const profile = profiles.find_profile(event.profile_id); const profile = profiles.findConnectProfile(event.profile_id);
if (!profile) { if (!profile) {
log.warn(LogCategory.CLIENT, tr("Received profile event with unknown profile id (event: %s, id: %s)"), event.type, event.profile_id); log.warn(LogCategory.CLIENT, tr("Received profile event with unknown profile id (event: %s, id: %s)"), event.type, event.profile_id);
return; return;
} }
spawnTeamSpeakIdentityImport(identity => { spawnTeamSpeakIdentityImport(identity => {
profile.set_identity(IdentitifyType.TEAMSPEAK, identity); profile.setIdentity(IdentitifyType.TEAMSPEAK, identity);
profiles.mark_need_save(); profiles.mark_need_save();
identity.level().catch(error => { identity.level().catch(error => {
@ -948,16 +953,16 @@ export namespace modal_settings {
}); });
event_registry.on("improve-identity-teamspeak-level", event => { 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) { if (!profile) {
log.warn(LogCategory.CLIENT, tr("Received profile event with unknown profile id (event: %s, id: %s)"), event.type, event.profile_id); log.warn(LogCategory.CLIENT, tr("Received profile event with unknown profile id (event: %s, id: %s)"), event.type, event.profile_id);
return; return;
} }
const identity = profile.selected_identity(IdentitifyType.TEAMSPEAK) as TeaSpeakIdentity; const identity = profile.selectedIdentity(IdentitifyType.TEAMSPEAK) as TeaSpeakIdentity;
if (!identity) return; if (!identity) return;
spawnTeamSpeakIdentityImprove(identity, profile.profile_name).close_listener.push(() => { spawnTeamSpeakIdentityImprove(identity, profile.profileName).close_listener.push(() => {
profiles.mark_need_save(); profiles.mark_need_save();
identity.level().then(level => { identity.level().then(level => {
@ -972,13 +977,13 @@ export namespace modal_settings {
}); });
event_registry.on("export-identity-teamspeak", event => { event_registry.on("export-identity-teamspeak", event => {
const profile = profiles.find_profile(event.profile_id); const profile = profiles.findConnectProfile(event.profile_id);
if (!profile) { if (!profile) {
log.warn(LogCategory.CLIENT, tr("Received profile event with unknown profile id (event: %s, id: %s)"), event.type, event.profile_id); log.warn(LogCategory.CLIENT, tr("Received profile event with unknown profile id (event: %s, id: %s)"), event.type, event.profile_id);
return; return;
} }
const identity = profile.selected_identity(IdentitifyType.TEAMSPEAK) as TeaSpeakIdentity; const identity = profile.selectedIdentity(IdentitifyType.TEAMSPEAK) as TeaSpeakIdentity;
if (!identity) return; if (!identity) return;
identity.export_ts(true).then(data => { 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"}); events.fire("notify_voice_connection_state", {state: "unsupported-server"});
break; 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" | "connected"
| "disconnected" | "disconnected"
| "unsupported-client" | "unsupported-client"
| "unsupported-server"; | "unsupported-server"
| "failed";
export type TestState = export type TestState =
{ state: "initializing" | "running" | "stopped" | "microphone-invalid" | "unsupported" } { state: "initializing" | "running" | "stopped" | "microphone-invalid" | "unsupported" }
| { state: "start-failed", error: string }; | { state: "start-failed", error: string };
@ -29,7 +30,8 @@ export interface EchoTestEvents {
phase: "testing" | "troubleshooting" phase: "testing" | "troubleshooting"
}, },
notify_voice_connection_state: { notify_voice_connection_state: {
state: VoiceConnectionState state: VoiceConnectionState,
message?: string
}, },
notify_test_state: { notify_test_state: {
state: TestState state: TestState

View File

@ -105,7 +105,7 @@
pointer-events: none; pointer-events: none;
opacity: 0; opacity: 0;
background-color: #19191bcc; background-color: #19191b;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@ -120,7 +120,12 @@
&.shown { &.shown {
pointer-events: all; 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"); events.fire("query_voice_connection_state");
return "loading"; 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) { switch (state) {
case "failed":
error = true;
inner = <a key={state}>
<Translatable>Voice connection establishment has been failed:</Translatable><br />
{message}
</a>;
break;
case "disconnected": case "disconnected":
inner = <a key={state}><Translatable>Voice connection has been disconnected.</Translatable></a>; inner = <a key={state}><Translatable>Voice connection has been disconnected.</Translatable></a>;
break; break;
@ -57,7 +69,7 @@ const VoiceStateOverlay = () => {
} }
return ( return (
<div className={cssStyle.overlay + " " + (shown ? cssStyle.shown : "")}> <div className={cssStyle.overlay + " " + (shown ? cssStyle.shown : "") + " " + (error ? cssStyle.error : "")}>
{inner} {inner}
</div> </div>
); );

View File

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

View File

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

View File

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

View File

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

View File

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