2020-03-30 11:44:18 +00:00
import { ChannelTree } from "tc-shared/ui/view" ;
import { AbstractServerConnection } from "tc-shared/connection/ConnectionBase" ;
import { PermissionManager } from "tc-shared/permission/PermissionManager" ;
import { GroupManager } from "tc-shared/permission/GroupManager" ;
2020-04-09 13:10:14 +00:00
import { ServerSettings , Settings , settings , StaticSettings } from "tc-shared/settings" ;
2020-03-30 11:44:18 +00:00
import { Sound , SoundManager } from "tc-shared/sound/Sounds" ;
import { LocalClientEntry } from "tc-shared/ui/client" ;
2020-08-19 17:33:57 +00:00
import { ConnectionProfile } from "tc-shared/profiles/ConnectionProfile" ;
2020-03-30 11:44:18 +00:00
import { ServerAddress } from "tc-shared/ui/server" ;
import * as log from "tc-shared/log" ;
2020-08-19 17:33:57 +00:00
import { LogCategory , logError } from "tc-shared/log" ;
2020-03-30 11:44:18 +00:00
import { createErrorModal , createInfoModal , createInputModal , Modal } from "tc-shared/ui/elements/Modal" ;
import { hashPassword } from "tc-shared/utils/helpers" ;
import { HandshakeHandler } from "tc-shared/connection/HandshakeHandler" ;
import * as htmltags from "./ui/htmltags" ;
import { ChannelEntry } from "tc-shared/ui/channel" ;
import { InputStartResult , InputState } from "tc-shared/voice/RecorderBase" ;
2020-06-10 16:13:56 +00:00
import { CommandResult } from "tc-shared/connection/ServerConnectionDeclaration" ;
2020-08-19 17:33:57 +00:00
import { default_recorder , RecorderProfile } from "tc-shared/voice/RecorderProfile" ;
2020-03-30 11:44:18 +00:00
import { Frame } from "tc-shared/ui/frames/chat_frame" ;
import { Hostbanner } from "tc-shared/ui/frames/hostbanner" ;
import { server_connections } from "tc-shared/ui/frames/connection_handlers" ;
import { connection_log , Regex } from "tc-shared/ui/modal/ModalConnect" ;
import { formatMessage } from "tc-shared/ui/frames/chat" ;
import { spawnAvatarUpload } from "tc-shared/ui/modal/ModalAvatar" ;
import * as dns from "tc-backend/dns" ;
2020-04-06 14:29:40 +00:00
import * as top_menu from "tc-shared/ui/frames/MenuBar" ;
2020-04-09 13:10:14 +00:00
import { EventHandler , Registry } from "tc-shared/events" ;
2020-06-10 16:13:56 +00:00
import { FileManager } from "tc-shared/file/FileManager" ;
import { FileTransferState , TransferProvider } from "tc-shared/file/Transfer" ;
import { traj } from "tc-shared/i18n/localize" ;
import { md5 } from "tc-shared/crypto/md5" ;
2020-07-20 17:08:13 +00:00
import { guid } from "tc-shared/crypto/uid" ;
2020-07-21 22:55:28 +00:00
import { ServerEventLog } from "tc-shared/ui/frames/log/ServerEventLog" ;
import { EventType } from "tc-shared/ui/frames/log/Definitions" ;
2020-08-07 11:40:11 +00:00
import { PluginCmdRegistry } from "tc-shared/connection/PluginCmdHandler" ;
2020-08-08 13:20:32 +00:00
import { W2GPluginCmdHandler } from "tc-shared/video-viewer/W2GPlugin" ;
2020-08-10 12:41:34 +00:00
import { VoiceConnectionStatus } from "tc-shared/connection/VoiceConnection" ;
import { getServerConnectionFactory } from "tc-shared/connection/ConnectionFactory" ;
2020-08-19 17:33:57 +00:00
export enum InputHardwareState {
MISSING ,
START_FAILED ,
VALID
}
2020-03-30 11:44:18 +00:00
export enum DisconnectReason {
2019-04-04 19:47:52 +00:00
HANDLER_DESTROYED ,
REQUESTED ,
2019-04-15 13:33:51 +00:00
DNS_FAILED ,
2019-04-04 19:47:52 +00:00
CONNECT_FAILURE ,
CONNECTION_CLOSED ,
CONNECTION_FATAL_ERROR ,
CONNECTION_PING_TIMEOUT ,
CLIENT_KICKED ,
CLIENT_BANNED ,
HANDSHAKE_FAILED ,
2019-04-15 14:58:42 +00:00
HANDSHAKE_TEAMSPEAK_REQUIRED ,
HANDSHAKE_BANNED ,
2019-04-04 19:47:52 +00:00
SERVER_CLOSED ,
SERVER_REQUIRES_PASSWORD ,
2019-08-21 08:00:01 +00:00
SERVER_HOSTMESSAGE ,
2019-04-15 13:33:51 +00:00
IDENTITY_TOO_LOW ,
2019-04-04 19:47:52 +00:00
UNKNOWN
}
2020-03-30 11:44:18 +00:00
export enum ConnectionState {
2020-04-09 13:10:14 +00:00
UNCONNECTED , /* no connection is currenting running */
CONNECTING , /* we try to establish a connection to the target server */
INITIALISING , /* we're setting up the connection encryption */
2020-07-12 14:31:57 +00:00
AUTHENTICATING , /* we're authenticating our self so we get a unique ID */
2020-04-09 13:10:14 +00:00
CONNECTED , /* we're connected to the server. Server init has been done, may not everything is initialized */
DISCONNECTING /* we're curently disconnecting from the server and awaiting disconnect acknowledge */
}
export namespace ConnectionState {
2020-07-12 14:31:57 +00:00
export function socketConnected ( state : ConnectionState ) {
2020-04-09 13:10:14 +00:00
switch ( state ) {
case ConnectionState . CONNECTED :
case ConnectionState . AUTHENTICATING :
//case ConnectionState.INITIALISING: /* its not yet possible to send any data */
return true ;
default :
return false ;
}
}
2020-07-12 14:31:57 +00:00
export function fullyConnected ( state : ConnectionState ) {
return state === ConnectionState . CONNECTED ;
}
2019-04-04 19:47:52 +00:00
}
2020-03-30 11:44:18 +00:00
export enum ViewReasonId {
2019-04-04 19:47:52 +00:00
VREASON_USER_ACTION = 0 ,
VREASON_MOVED = 1 ,
VREASON_SYSTEM = 2 ,
VREASON_TIMEOUT = 3 ,
VREASON_CHANNEL_KICK = 4 ,
VREASON_SERVER_KICK = 5 ,
VREASON_BAN = 6 ,
VREASON_SERVER_STOPPED = 7 ,
VREASON_SERVER_LEFT = 8 ,
VREASON_CHANNEL_UPDATED = 9 ,
VREASON_EDITED = 10 ,
VREASON_SERVER_SHUTDOWN = 11
}
2020-04-09 13:10:14 +00:00
export interface LocalClientStatus {
2019-04-04 19:47:52 +00:00
input_muted : boolean ;
output_muted : boolean ;
channel_codec_encoding_supported : boolean ;
channel_codec_decoding_supported : boolean ;
sound_playback_supported : boolean ;
sound_record_supported ;
away : boolean | string ;
channel_subscribe_all : boolean ;
queries_visible : boolean ;
}
2020-03-30 11:44:18 +00:00
export interface ConnectParameters {
2019-06-19 13:56:51 +00:00
nickname? : string ;
channel ? : {
target : string | number ;
password? : string ;
} ;
token? : string ;
password ? : { password : string , hashed : boolean } ;
2019-10-19 15:13:40 +00:00
auto_reconnect_attempt? : boolean ;
2019-06-19 13:56:51 +00:00
}
2020-03-30 11:44:18 +00:00
export class ConnectionHandler {
2020-07-20 17:08:13 +00:00
readonly handlerId : string ;
2020-04-09 13:10:14 +00:00
private readonly event_registry : Registry < ConnectionEvents > ;
2019-04-04 19:47:52 +00:00
channelTree : ChannelTree ;
2020-04-25 18:27:45 +00:00
connection_state : ConnectionState = ConnectionState . UNCONNECTED ;
2020-03-30 11:44:18 +00:00
serverConnection : AbstractServerConnection ;
2019-04-04 19:47:52 +00:00
fileManager : FileManager ;
permissions : PermissionManager ;
groups : GroupManager ;
2020-03-30 11:44:18 +00:00
side_bar : Frame ;
2019-04-04 19:47:52 +00:00
settings : ServerSettings ;
2020-03-30 11:44:18 +00:00
sound : SoundManager ;
2019-04-04 19:47:52 +00:00
2019-08-21 08:00:01 +00:00
hostbanner : Hostbanner ;
tag_connection_handler : JQuery ;
2019-04-04 19:47:52 +00:00
private _clientId : number = 0 ;
private _local_client : LocalClientEntry ;
2019-10-19 15:13:40 +00:00
2020-07-19 15:12:41 +00:00
private _reconnect_timer : number ;
2019-04-04 19:47:52 +00:00
private _reconnect_attempt : boolean = false ;
2019-10-19 15:13:40 +00:00
2019-04-15 13:33:51 +00:00
private _connect_initialize_id : number = 1 ;
2019-04-04 19:47:52 +00:00
2020-08-07 11:40:11 +00:00
private pluginCmdRegistry : PluginCmdRegistry ;
2020-04-09 13:10:14 +00:00
private client_status : LocalClientStatus = {
2019-04-04 19:47:52 +00:00
input_muted : false ,
2020-08-19 17:33:57 +00:00
2019-04-04 19:47:52 +00:00
output_muted : false ,
away : false ,
channel_subscribe_all : true ,
queries_visible : false ,
sound_playback_supported : undefined ,
sound_record_supported : undefined ,
channel_codec_encoding_supported : undefined ,
channel_codec_decoding_supported : undefined
} ;
2020-08-19 17:33:57 +00:00
private inputHardwareState : InputHardwareState = InputHardwareState . MISSING ;
2020-07-21 22:55:28 +00:00
log : ServerEventLog ;
2019-04-04 19:47:52 +00:00
constructor ( ) {
2020-07-20 17:08:13 +00:00
this . handlerId = guid ( ) ;
2020-04-09 13:10:14 +00:00
this . event_registry = new Registry < ConnectionEvents > ( ) ;
2020-07-20 17:08:13 +00:00
this . event_registry . enableDebug ( "connection-handler" ) ;
2020-04-09 13:10:14 +00:00
2019-04-04 19:47:52 +00:00
this . settings = new ServerSettings ( ) ;
2020-08-10 12:41:34 +00:00
this . serverConnection = getServerConnectionFactory ( ) . create ( this ) ;
this . serverConnection . events . on ( "notify_connection_state_changed" , event = > this . on_connection_state_changed ( event . oldState , event . newState ) ) ;
2020-08-19 17:33:57 +00:00
this . serverConnection . getVoiceConnection ( ) . events . on ( "notify_recorder_changed" , ( ) = > {
this . setInputHardwareState ( this . getVoiceRecorder ( ) ? InputHardwareState.VALID : InputHardwareState.MISSING ) ;
this . update_voice_status ( ) ;
} ) ;
2020-08-10 12:41:34 +00:00
this . serverConnection . getVoiceConnection ( ) . events . on ( "notify_connection_status_changed" , ( ) = > this . update_voice_status ( ) ) ;
2019-04-04 19:47:52 +00:00
2020-07-12 14:31:57 +00:00
this . channelTree = new ChannelTree ( this ) ;
2019-04-04 19:47:52 +00:00
this . fileManager = new FileManager ( this ) ;
this . permissions = new PermissionManager ( this ) ;
2020-07-12 14:31:57 +00:00
2020-08-07 11:40:11 +00:00
this . pluginCmdRegistry = new PluginCmdRegistry ( this ) ;
2020-07-21 22:55:28 +00:00
this . log = new ServerEventLog ( this ) ;
2020-07-12 14:31:57 +00:00
this . side_bar = new Frame ( this ) ;
this . sound = new SoundManager ( this ) ;
this . hostbanner = new Hostbanner ( this ) ;
2019-08-21 08:00:01 +00:00
2019-04-04 19:47:52 +00:00
this . groups = new GroupManager ( this ) ;
this . _local_client = new LocalClientEntry ( this ) ;
2019-08-21 08:00:01 +00:00
/* initialize connection handler tab entry */
{
this . tag_connection_handler = $ . spawn ( "div" ) . addClass ( "connection-container" ) ;
$ . spawn ( "div" ) . addClass ( "server-icon icon client-server_green" ) . appendTo ( this . tag_connection_handler ) ;
$ . spawn ( "div" ) . addClass ( "server-name" ) . appendTo ( this . tag_connection_handler ) ;
$ . spawn ( "div" ) . addClass ( "button-close icon client-tab_close_button" ) . appendTo ( this . tag_connection_handler ) ;
this . tag_connection_handler . on ( 'click' , event = > {
if ( event . isDefaultPrevented ( ) )
return ;
2020-04-09 13:10:14 +00:00
server_connections . set_active_connection ( this ) ;
2019-08-21 08:00:01 +00:00
} ) ;
this . tag_connection_handler . find ( ".button-close" ) . on ( 'click' , event = > {
2020-04-09 13:10:14 +00:00
server_connections . destroy_server_connection ( this ) ;
2019-08-21 08:00:01 +00:00
event . preventDefault ( ) ;
} ) ;
this . tab_set_name ( tr ( "Not connected" ) ) ;
}
2020-04-09 13:10:14 +00:00
this . event_registry . register_handler ( this ) ;
2020-07-17 21:56:20 +00:00
this . events ( ) . fire ( "notify_handler_initialized" ) ;
2020-08-07 11:40:11 +00:00
this . pluginCmdRegistry . registerHandler ( new W2GPluginCmdHandler ( ) ) ;
2020-04-09 13:10:14 +00:00
}
initialize_client_state ( source? : ConnectionHandler ) {
this . client_status . input_muted = source ? source.client_status.input_muted : settings.global ( Settings . KEY_CLIENT_STATE_MICROPHONE_MUTED ) ;
this . client_status . output_muted = source ? source.client_status.output_muted : settings.global ( Settings . KEY_CLIENT_STATE_SPEAKER_MUTED ) ;
this . update_voice_status ( ) ;
this . setSubscribeToAllChannels ( source ? source.client_status.channel_subscribe_all : settings.global ( Settings . KEY_CLIENT_STATE_SUBSCRIBE_ALL_CHANNELS ) ) ;
2020-04-09 21:23:34 +00:00
this . setAway_ ( source ? source . client_status . away : ( settings . global ( Settings . KEY_CLIENT_STATE_AWAY ) ? settings . global ( Settings . KEY_CLIENT_AWAY_MESSAGE ) : false ) , false ) ;
2020-04-09 13:10:14 +00:00
this . setQueriesShown ( source ? source.client_status.queries_visible : settings.global ( Settings . KEY_CLIENT_STATE_QUERY_SHOWN ) ) ;
}
events ( ) : Registry < ConnectionEvents > {
return this . event_registry ;
2019-08-21 08:00:01 +00:00
}
tab_set_name ( name : string ) {
this . tag_connection_handler . toggleClass ( 'cutoff-name' , name . length > 30 ) ;
this . tag_connection_handler . find ( ".server-name" ) . text ( name ) ;
2019-04-04 19:47:52 +00:00
}
2020-03-30 11:44:18 +00:00
async startConnection ( addr : string , profile : ConnectionProfile , user_action : boolean , parameters : ConnectParameters ) {
2019-08-21 08:00:01 +00:00
this . cancel_reconnect ( false ) ;
2019-10-19 15:13:40 +00:00
this . _reconnect_attempt = parameters . auto_reconnect_attempt || false ;
2020-08-19 17:33:57 +00:00
this . handleDisconnect ( DisconnectReason . REQUESTED ) ;
this . tab_set_name ( tr ( "Connecting" ) ) ;
2019-04-04 19:47:52 +00:00
2019-06-19 13:56:51 +00:00
let server_address : ServerAddress = {
host : "" ,
port : - 1
} ;
{
2019-08-21 08:00:01 +00:00
let _v6_end = addr . indexOf ( ']' ) ;
2019-06-19 13:56:51 +00:00
let idx = addr . lastIndexOf ( ':' ) ;
2019-08-21 08:00:01 +00:00
if ( idx != - 1 && idx > _v6_end ) {
2019-06-19 13:56:51 +00:00
server_address . port = parseInt ( addr . substr ( idx + 1 ) ) ;
server_address . host = addr . substr ( 0 , idx ) ;
2019-04-15 13:33:51 +00:00
} else {
2019-06-19 13:56:51 +00:00
server_address . host = addr ;
server_address . port = 9987 ;
2019-04-15 13:33:51 +00:00
}
2019-06-19 13:56:51 +00:00
}
2019-08-30 21:06:39 +00:00
log . info ( LogCategory . CLIENT , tr ( "Start connection to %s:%d" ) , server_address . host , server_address . port ) ;
2020-07-21 22:55:28 +00:00
this . log . log ( EventType . CONNECTION_BEGIN , {
2019-07-09 22:52:08 +00:00
address : {
server_hostname : server_address.host ,
server_port : server_address.port
} ,
client_nickname : parameters.nickname
} ) ;
2019-06-19 13:56:51 +00:00
this . channelTree . initialiseHead ( addr , server_address ) ;
if ( parameters . password && ! parameters . password . hashed ) {
try {
2020-03-30 11:44:18 +00:00
const password = await hashPassword ( parameters . password . password ) ;
2019-06-19 13:56:51 +00:00
parameters . password = {
hashed : true ,
password : password
}
} catch ( error ) {
2019-08-30 21:06:39 +00:00
log . error ( LogCategory . CLIENT , tr ( "Failed to hash connect password: %o" ) , error ) ;
2019-06-19 13:56:51 +00:00
createErrorModal ( tr ( "Error while hashing password" ) , tr ( "Failed to hash server password!<br>" ) + error ) . open ( ) ;
}
}
2019-08-21 08:00:01 +00:00
if ( parameters . password ) {
connection_log . update_address_password ( {
hostname : server_address.host ,
port : server_address.port
} , parameters . password . password ) ;
}
2019-04-15 13:33:51 +00:00
2019-08-21 08:00:01 +00:00
const original_address = { host : server_address.host , port : server_address.port } ;
2020-04-18 19:38:12 +00:00
if ( server_address . host === "localhost" ) {
server_address . host = "127.0.0.1" ;
} else if ( dns . supported ( ) && ! server_address . host . match ( Regex . IP_V4 ) && ! server_address . host . match ( Regex . IP_V6 ) ) {
2019-04-15 13:33:51 +00:00
const id = ++ this . _connect_initialize_id ;
2020-07-21 22:55:28 +00:00
this . log . log ( EventType . CONNECTION_HOSTNAME_RESOLVE , { } ) ;
2019-06-19 13:56:51 +00:00
try {
2019-10-24 16:05:11 +00:00
const resolved = await dns . resolve_address ( server_address , { timeout : 5000 } ) || { } as any ;
2019-04-15 13:33:51 +00:00
if ( id != this . _connect_initialize_id )
return ; /* we're old */
2019-10-24 16:05:11 +00:00
server_address . host = typeof ( resolved . target_ip ) === "string" ? resolved.target_ip : server_address.host ;
server_address . port = typeof ( resolved . target_port ) === "number" ? resolved.target_port : server_address.port ;
2020-07-21 22:55:28 +00:00
this . log . log ( EventType . CONNECTION_HOSTNAME_RESOLVED , {
2019-07-09 22:52:08 +00:00
address : {
server_port : server_address.port ,
server_hostname : server_address.host
}
} ) ;
2019-06-19 13:56:51 +00:00
} catch ( error ) {
2019-04-15 13:33:51 +00:00
if ( id != this . _connect_initialize_id )
return ; /* we're old */
this . handleDisconnect ( DisconnectReason . DNS_FAILED , error ) ;
2019-06-19 13:56:51 +00:00
}
2019-04-04 19:47:52 +00:00
}
2019-06-19 13:56:51 +00:00
2020-03-30 11:44:18 +00:00
await this . serverConnection . connect ( server_address , new HandshakeHandler ( profile , parameters ) ) ;
2019-08-21 08:00:01 +00:00
setTimeout ( ( ) = > {
const connected = this . serverConnection . connected ( ) ;
if ( user_action && connected ) {
connection_log . log_connect ( {
hostname : original_address.host ,
port : original_address.port
} ) ;
}
} , 50 ) ;
2019-04-04 19:47:52 +00:00
}
2020-04-09 13:10:14 +00:00
async disconnectFromServer ( reason? : string ) {
this . cancel_reconnect ( true ) ;
2020-04-10 18:57:50 +00:00
if ( ! this . connected ) return ;
2020-08-19 17:33:57 +00:00
this . handleDisconnect ( DisconnectReason . REQUESTED ) ;
2020-04-09 13:10:14 +00:00
try {
await this . serverConnection . disconnect ( ) ;
} catch ( error ) {
log . warn ( LogCategory . CLIENT , tr ( "Failed to successfully disconnect from server: {}" ) , error ) ;
}
this . sound . play ( Sound . CONNECTION_DISCONNECTED ) ;
2020-07-21 22:55:28 +00:00
this . log . log ( EventType . DISCONNECTED , { } ) ;
2020-04-09 13:10:14 +00:00
}
2019-04-04 19:47:52 +00:00
getClient ( ) : LocalClientEntry { return this . _local_client ; }
getClientId() { return this . _clientId ; }
2020-06-12 17:33:05 +00:00
initializeLocalClient ( clientId : number , acceptedName : string ) {
this . _clientId = clientId ;
this . _local_client [ "_clientId" ] = clientId ;
2019-04-04 19:47:52 +00:00
2020-06-12 17:33:05 +00:00
this . channelTree . registerClient ( this . _local_client ) ;
this . _local_client . updateVariables ( { key : "client_nickname" , value : acceptedName } ) ;
2019-04-04 19:47:52 +00:00
}
2020-03-30 11:44:18 +00:00
getServerConnection ( ) : AbstractServerConnection { return this . serverConnection ; }
2019-04-04 19:47:52 +00:00
2020-04-09 13:10:14 +00:00
@EventHandler < ConnectionEvents > ( "notify_connection_state_changed" )
2020-08-19 17:33:57 +00:00
private handleConnectionStateChanged ( event : ConnectionEvents [ "notify_connection_state_changed" ] ) {
2020-04-25 18:27:45 +00:00
this . connection_state = event . new_state ;
2020-08-19 17:33:57 +00:00
if ( event . new_state === ConnectionState . CONNECTED ) {
log . info ( LogCategory . CLIENT , tr ( "Client connected" ) ) ;
this . log . log ( EventType . CONNECTION_CONNECTED , {
serverAddress : {
server_port : this.channelTree.server.remote_address.port ,
server_hostname : this.channelTree.server.remote_address.host
} ,
serverName : this.channelTree.server.properties.virtualserver_name ,
own_client : this.getClient ( ) . log_data ( )
} ) ;
this . sound . play ( Sound . CONNECTION_CONNECTED ) ;
2020-04-11 09:43:41 +00:00
2020-08-19 17:33:57 +00:00
this . permissions . requestPermissionList ( ) ;
if ( this . groups . serverGroups . length == 0 )
this . groups . requestGroups ( ) ;
2019-04-04 19:47:52 +00:00
2020-08-19 17:33:57 +00:00
this . settings . setServer ( this . channelTree . server . properties . virtualserver_unique_identifier ) ;
2019-04-04 19:47:52 +00:00
2020-08-19 17:33:57 +00:00
/* apply the server settings */
if ( this . client_status . channel_subscribe_all )
this . channelTree . subscribe_all_channels ( ) ;
else
this . channelTree . unsubscribe_all_channels ( ) ;
this . channelTree . toggle_server_queries ( this . client_status . queries_visible ) ;
this . sync_status_with_server ( ) ;
this . channelTree . server . updateProperties ( ) ;
/ *
No need to update the voice stuff because as soon we see ourself we ' re doing it
this . update_voice_status ( ) ;
if ( control_bar . current_connection_handler ( ) === this )
control_bar . apply_server_voice_state ( ) ;
* /
} else {
this . setInputHardwareState ( this . getVoiceRecorder ( ) ? InputHardwareState.VALID : InputHardwareState.MISSING ) ;
}
2019-04-04 19:47:52 +00:00
}
get connected ( ) : boolean {
return this . serverConnection && this . serverConnection . connected ( ) ;
}
private generate_ssl_certificate_accept ( ) : JQuery {
const properties = {
connect_default : true ,
2019-04-15 13:33:51 +00:00
connect_profile : this.serverConnection.handshake_handler ( ) . profile . id ,
connect_address : this.serverConnection.remote_address ( ) . host + ( this . serverConnection . remote_address ( ) . port !== 9987 ? ":" + this . serverConnection . remote_address ( ) . port : "" )
2019-04-04 19:47:52 +00:00
} ;
2019-09-01 15:24:06 +00:00
const build_url = ( base : string , search : string , props : any ) = > {
2019-04-04 19:47:52 +00:00
const parameters : string [ ] = [ ] ;
for ( const key of Object . keys ( props ) )
parameters . push ( key + "=" + encodeURIComponent ( props [ key ] ) ) ;
2019-09-01 15:24:06 +00:00
let callback = base + search ; /* don't use document.URL because it may contains a #! */
if ( ! search )
2019-04-04 19:47:52 +00:00
callback += "?" + parameters . join ( "&" ) ;
else
callback += "&" + parameters . join ( "&" ) ;
2019-04-15 13:33:51 +00:00
return "https://" + this . serverConnection . remote_address ( ) . host + ":" + this . serverConnection . remote_address ( ) . port + "/?forward_url=" + encodeURIComponent ( callback ) ;
2019-04-04 19:47:52 +00:00
} ;
/* generate the tag */
const tag = $ . spawn ( "a" ) . text ( tr ( "here" ) ) ;
2019-11-06 11:38:33 +00:00
let pathname = document . location . pathname ;
if ( pathname . endsWith ( ".php" ) )
pathname = pathname . substring ( 0 , pathname . lastIndexOf ( "/" ) ) ;
2020-08-19 17:33:57 +00:00
tag . attr ( 'href' , build_url ( document . location . origin + pathname , document . location . search , properties ) ) ;
2019-04-04 19:47:52 +00:00
return tag ;
}
private _certificate_modal : Modal ;
handleDisconnect ( type : DisconnectReason , data : any = { } ) {
2019-04-15 13:33:51 +00:00
this . _connect_initialize_id ++ ;
2019-08-21 08:00:01 +00:00
this . tab_set_name ( tr ( "Not connected" ) ) ;
2019-04-04 19:47:52 +00:00
let auto_reconnect = false ;
switch ( type ) {
case DisconnectReason . REQUESTED :
2019-08-21 08:00:01 +00:00
case DisconnectReason . SERVER_HOSTMESSAGE : /* already handled */
2019-04-04 19:47:52 +00:00
break ;
case DisconnectReason . HANDLER_DESTROYED :
2019-09-12 21:59:35 +00:00
if ( data ) {
2019-04-04 19:47:52 +00:00
this . sound . play ( Sound . CONNECTION_DISCONNECTED ) ;
2020-07-21 22:55:28 +00:00
this . log . log ( EventType . DISCONNECTED , { } ) ;
2019-09-12 21:59:35 +00:00
}
2019-04-04 19:47:52 +00:00
break ;
2019-04-15 13:33:51 +00:00
case DisconnectReason . DNS_FAILED :
2019-08-30 21:06:39 +00:00
log . error ( LogCategory . CLIENT , tr ( "Failed to resolve hostname: %o" ) , data ) ;
2020-07-21 22:55:28 +00:00
this . log . log ( EventType . CONNECTION_HOSTNAME_RESOLVE_ERROR , {
2019-07-09 22:52:08 +00:00
message : data as any
} ) ;
2019-04-15 13:33:51 +00:00
this . sound . play ( Sound . CONNECTION_REFUSED ) ;
break ;
2019-04-04 19:47:52 +00:00
case DisconnectReason . CONNECT_FAILURE :
if ( this . _reconnect_attempt ) {
auto_reconnect = true ;
2020-07-21 22:55:28 +00:00
this . log . log ( EventType . CONNECTION_FAILED , { serverAddress : {
server_port : this.channelTree.server.remote_address.port ,
server_hostname : this.channelTree.server.remote_address.host
} } ) ;
2019-04-04 19:47:52 +00:00
break ;
}
2020-07-23 22:29:36 +00:00
2019-11-23 22:41:51 +00:00
if ( data )
log . error ( LogCategory . CLIENT , tr ( "Could not connect to remote host! Extra data: %o" ) , data ) ;
else
log . error ( LogCategory . CLIENT , tr ( "Could not connect to remote host!" ) , data ) ;
2019-04-04 19:47:52 +00:00
2020-08-19 17:33:57 +00:00
if ( __build . target === "client" || ! dns . resolve_address_ipv4 ) {
2019-04-04 19:47:52 +00:00
createErrorModal (
tr ( "Could not connect" ) ,
tr ( "Could not connect to remote host (Connection refused)" )
) . open ( ) ;
} else {
2020-07-23 22:29:36 +00:00
const generateAddressPart = ( ) = > Math . floor ( Math . random ( ) * 256 ) ;
const addressParts = [ generateAddressPart ( ) , generateAddressPart ( ) , generateAddressPart ( ) , generateAddressPart ( ) ] ;
dns . resolve_address_ipv4 ( addressParts . join ( "-" ) + ".con-gate.work" ) . then ( async result = > {
if ( result !== addressParts . join ( "." ) )
throw "miss matching address" ;
createErrorModal (
tr ( "Could not connect" ) ,
tr ( "Could not connect to remote host (Connection refused)" )
) . open ( ) ;
} ) . catch ( ( ) = > {
const error_message_format =
"Could not connect to remote host (Connection refused)\n" +
"If you're sure that the remote host is up, than you may not allow unsigned certificates or that con-gate.work works properly.\n" +
"Click {0} to accept the remote certificate" ;
this . _certificate_modal = createErrorModal (
tr ( "Could not connect" ) ,
formatMessage ( /* @tr-ignore */ tr ( error_message_format ) , this . generate_ssl_certificate_accept ( ) )
) ;
this . _certificate_modal . close_listener . push ( ( ) = > this . _certificate_modal = undefined ) ;
this . _certificate_modal . open ( ) ;
} ) ;
2019-04-04 19:47:52 +00:00
}
2020-07-23 22:29:36 +00:00
this . log . log ( EventType . CONNECTION_FAILED , { serverAddress : {
server_hostname : this.serverConnection.remote_address ( ) . host ,
server_port : this.serverConnection.remote_address ( ) . port
} } ) ;
2019-04-04 19:47:52 +00:00
this . sound . play ( Sound . CONNECTION_REFUSED ) ;
break ;
case DisconnectReason . HANDSHAKE_FAILED :
//TODO sound
2019-08-30 21:06:39 +00:00
log . error ( LogCategory . CLIENT , tr ( "Failed to process handshake: %o" ) , data ) ;
2019-04-04 19:47:52 +00:00
createErrorModal (
tr ( "Could not connect" ) ,
tr ( "Failed to process handshake: " ) + data as string
) . open ( ) ;
break ;
2019-04-15 14:58:42 +00:00
case DisconnectReason . HANDSHAKE_TEAMSPEAK_REQUIRED :
createErrorModal (
tr ( "Target server is a TeamSpeak server" ) ,
2020-03-30 11:44:18 +00:00
formatMessage ( tr ( "The target server is a TeamSpeak 3 server!{:br:}Only TeamSpeak 3 based identities are able to connect.{:br:}Please select another profile or change the identify type." ) )
2019-04-15 14:58:42 +00:00
) . open ( ) ;
this . sound . play ( Sound . CONNECTION_DISCONNECTED ) ;
auto_reconnect = false ;
break ;
2019-04-15 13:33:51 +00:00
case DisconnectReason . IDENTITY_TOO_LOW :
createErrorModal (
tr ( "Identity level is too low" ) ,
2020-03-30 11:44:18 +00:00
formatMessage ( tr ( "You've been disconnected, because your Identity level is too low.{:br:}You need at least a level of {0}" ) , data [ "extra_message" ] )
2019-04-15 13:33:51 +00:00
) . open ( ) ;
this . sound . play ( Sound . CONNECTION_DISCONNECTED ) ;
auto_reconnect = false ;
break ;
2019-04-04 19:47:52 +00:00
case DisconnectReason . CONNECTION_CLOSED :
2019-08-30 21:06:39 +00:00
log . error ( LogCategory . CLIENT , tr ( "Lost connection to remote server!" ) ) ;
2019-10-19 15:13:40 +00:00
if ( ! this . _reconnect_attempt ) {
createErrorModal (
tr ( "Connection closed" ) ,
tr ( "The connection was closed by remote host" )
) . open ( ) ;
}
2019-04-04 19:47:52 +00:00
this . sound . play ( Sound . CONNECTION_DISCONNECTED ) ;
auto_reconnect = true ;
break ;
case DisconnectReason . CONNECTION_PING_TIMEOUT :
2019-08-30 21:06:39 +00:00
log . error ( LogCategory . CLIENT , tr ( "Connection ping timeout" ) ) ;
2019-04-04 19:47:52 +00:00
this . sound . play ( Sound . CONNECTION_DISCONNECTED_TIMEOUT ) ;
createErrorModal (
tr ( "Connection lost" ) ,
tr ( "Lost connection to remote host (Ping timeout)<br>Even possible?" )
) . open ( ) ;
break ;
case DisconnectReason . SERVER_CLOSED :
2020-07-21 22:55:28 +00:00
this . log . log ( EventType . SERVER_CLOSED , { message : data.reasonmsg } ) ;
2019-10-19 15:13:40 +00:00
2019-04-04 19:47:52 +00:00
createErrorModal (
tr ( "Server closed" ) ,
"The server is closed.<br>" + //TODO tr
"Reason: " + data . reasonmsg
) . open ( ) ;
this . sound . play ( Sound . CONNECTION_DISCONNECTED ) ;
auto_reconnect = true ;
break ;
case DisconnectReason . SERVER_REQUIRES_PASSWORD :
2020-07-21 22:55:28 +00:00
this . log . log ( EventType . SERVER_REQUIRES_PASSWORD , { } ) ;
2019-08-21 08:00:01 +00:00
2019-04-04 19:47:52 +00:00
createInputModal ( tr ( "Server password" ) , tr ( "Enter server password:" ) , password = > password . length != 0 , password = > {
if ( ! ( typeof password === "string" ) ) return ;
2019-06-19 13:56:51 +00:00
2019-08-21 08:00:01 +00:00
const profile = this . serverConnection . handshake_handler ( ) . profile ;
const cprops = this . reconnect_properties ( profile ) ;
2019-06-19 13:56:51 +00:00
cprops . password = { password : password as string , hashed : false } ;
2019-08-21 08:00:01 +00:00
connection_log . update_address_info ( {
port : this.channelTree.server.remote_address.port ,
hostname : this.channelTree.server.remote_address.host
} , {
flag_password : true
} as any ) ;
this . startConnection ( this . channelTree . server . remote_address . host + ":" + this . channelTree . server . remote_address . port , profile , false , cprops ) ;
2019-04-04 19:47:52 +00:00
} ) . open ( ) ;
break ;
case DisconnectReason . CLIENT_KICKED :
2019-10-24 16:37:12 +00:00
const have_invoker = typeof ( data [ "invokerid" ] ) !== "undefined" && parseInt ( data [ "invokerid" ] ) !== 0 ;
const modal = createErrorModal (
2019-10-19 15:13:40 +00:00
tr ( "You've been kicked" ) ,
2020-03-30 11:44:18 +00:00
formatMessage (
2019-10-24 16:37:12 +00:00
have_invoker ? tr ( "You've been kicked from the server by {0}:{:br:}{1}" ) : tr ( "You've been kicked from the server:{:br:}{1}" ) ,
have_invoker ?
htmltags . generate_client_object ( { client_id : parseInt ( data [ "invokerid" ] ) , client_unique_id : data [ "invokeruid" ] , client_name : data [ "invokername" ] } ) :
2019-10-24 16:22:26 +00:00
"" ,
2019-10-24 16:37:12 +00:00
data [ "extra_message" ] || data [ "reasonmsg" ] || ""
2019-10-24 16:22:26 +00:00
)
2019-10-24 16:37:12 +00:00
) ;
modal . htmlTag . find ( ".modal-body" ) . addClass ( "modal-disconnect-kick" ) ;
modal . open ( ) ;
2019-04-15 14:58:42 +00:00
this . sound . play ( Sound . SERVER_KICKED ) ;
auto_reconnect = false ;
break ;
case DisconnectReason . HANDSHAKE_BANNED :
2019-12-21 15:23:35 +00:00
//Reason message already printed because of the command error handling
this . sound . play ( Sound . CONNECTION_BANNED ) ;
2019-04-04 19:47:52 +00:00
break ;
case DisconnectReason . CLIENT_BANNED :
2020-07-21 22:55:28 +00:00
this . log . log ( EventType . SERVER_BANNED , {
2019-08-21 08:00:01 +00:00
invoker : {
client_name : data [ "invokername" ] ,
client_id : parseInt ( data [ "invokerid" ] ) ,
client_unique_id : data [ "invokeruid" ]
} ,
message : data [ "reasonmsg" ] ,
time : parseInt ( data [ "time" ] )
} ) ;
2019-12-21 15:23:35 +00:00
this . sound . play ( Sound . CONNECTION_BANNED ) ;
2019-04-04 19:47:52 +00:00
break ;
default :
2019-08-30 21:06:39 +00:00
log . error ( LogCategory . CLIENT , tr ( "Got uncaught disconnect!" ) ) ;
log . error ( LogCategory . CLIENT , tr ( "Type: %o Data: %o" ) , type , data ) ;
2019-04-04 19:47:52 +00:00
break ;
}
2019-08-21 08:00:01 +00:00
this . channelTree . unregisterClient ( this . _local_client ) ; /* if we dont unregister our client here the client will be destroyed */
2019-04-04 19:47:52 +00:00
this . channelTree . reset ( ) ;
if ( this . serverConnection )
this . serverConnection . disconnect ( ) ;
2019-08-21 08:00:01 +00:00
this . hostbanner . update ( ) ;
2019-04-04 19:47:52 +00:00
if ( auto_reconnect ) {
if ( ! this . serverConnection ) {
2019-08-30 21:06:39 +00:00
log . info ( LogCategory . NETWORKING , tr ( "Allowed to auto reconnect but cant reconnect because we dont have any information left..." ) ) ;
2019-04-04 19:47:52 +00:00
return ;
}
2020-07-21 22:55:28 +00:00
this . log . log ( EventType . RECONNECT_SCHEDULED , { timeout : 5000 } ) ;
2019-04-04 19:47:52 +00:00
2019-08-30 21:06:39 +00:00
log . info ( LogCategory . NETWORKING , tr ( "Allowed to auto reconnect. Reconnecting in 5000ms" ) ) ;
2019-04-15 13:33:51 +00:00
const server_address = this . serverConnection . remote_address ( ) ;
const profile = this . serverConnection . handshake_handler ( ) . profile ;
2019-04-04 19:47:52 +00:00
this . _reconnect_timer = setTimeout ( ( ) = > {
this . _reconnect_timer = undefined ;
2020-07-21 22:55:28 +00:00
this . log . log ( EventType . RECONNECT_EXECUTE , { } ) ;
2019-06-19 13:56:51 +00:00
log . info ( LogCategory . NETWORKING , tr ( "Reconnecting..." ) ) ;
2019-10-19 15:13:40 +00:00
this . startConnection ( server_address . host + ":" + server_address . port , profile , false , Object . assign ( this . reconnect_properties ( profile ) , { auto_reconnect_attempt : true } ) ) ;
2019-04-04 19:47:52 +00:00
} , 5000 ) ;
}
2020-04-21 15:11:06 +00:00
this . serverConnection . updateConnectionState ( ConnectionState . UNCONNECTED ) ; /* Fix for the native client... */
2019-04-04 19:47:52 +00:00
}
2019-08-21 08:00:01 +00:00
cancel_reconnect ( log_event : boolean ) {
2019-04-04 19:47:52 +00:00
if ( this . _reconnect_timer ) {
2020-07-21 22:55:28 +00:00
if ( log_event ) this . log . log ( EventType . RECONNECT_CANCELED , { } ) ;
2019-04-04 19:47:52 +00:00
clearTimeout ( this . _reconnect_timer ) ;
this . _reconnect_timer = undefined ;
}
}
2020-04-09 13:10:14 +00:00
private on_connection_state_changed ( old_state : ConnectionState , new_state : ConnectionState ) {
2020-04-11 09:43:41 +00:00
console . log ( "From %s to %s" , ConnectionState [ old_state ] , ConnectionState [ new_state ] ) ;
2020-04-09 13:10:14 +00:00
this . event_registry . fire ( "notify_connection_state_changed" , {
old_state : old_state ,
new_state : new_state
} ) ;
2019-04-04 19:47:52 +00:00
}
2020-08-19 17:33:57 +00:00
private _last_record_error_popup : number = 0 ;
2019-04-04 19:47:52 +00:00
update_voice_status ( targetChannel? : ChannelEntry ) {
2020-08-19 17:33:57 +00:00
if ( ! this . _local_client ) {
/* we've been destroyed */
return ;
}
2019-08-21 08:00:01 +00:00
2020-08-19 17:33:57 +00:00
if ( typeof targetChannel === "undefined" )
targetChannel = this . getClient ( ) . currentChannel ( ) ;
2019-04-04 19:47:52 +00:00
2020-08-10 12:41:34 +00:00
const vconnection = this . serverConnection . getVoiceConnection ( ) ;
2019-04-04 19:47:52 +00:00
2020-08-19 17:33:57 +00:00
const codecEncodeSupported = ! targetChannel || vconnection . encoding_supported ( targetChannel . properties . channel_codec ) ;
const codecDecodeSupported = ! targetChannel || vconnection . decoding_supported ( targetChannel . properties . channel_codec ) ;
2020-08-13 11:05:37 +00:00
2019-04-04 19:47:52 +00:00
const property_update = {
client_input_muted : this.client_status.input_muted ,
client_output_muted : this.client_status.output_muted
} ;
2020-08-19 17:33:57 +00:00
/* update the encoding codec */
if ( codecEncodeSupported && targetChannel ) {
2019-09-12 21:59:35 +00:00
vconnection . set_encoder_codec ( targetChannel . properties . channel_codec ) ;
2020-08-19 17:33:57 +00:00
}
2019-09-12 21:59:35 +00:00
2020-08-10 12:41:34 +00:00
if ( ! this . serverConnection . connected ( ) || vconnection . getConnectionState ( ) !== VoiceConnectionStatus . Connected ) {
2019-04-04 19:47:52 +00:00
property_update [ "client_input_hardware" ] = false ;
property_update [ "client_output_hardware" ] = false ;
} else {
2020-08-19 17:33:57 +00:00
const recording_supported =
this . getInputHardwareState ( ) === InputHardwareState . VALID &&
( ! targetChannel || vconnection . encoding_supported ( targetChannel . properties . channel_codec ) ) &&
vconnection . getConnectionState ( ) === VoiceConnectionStatus . Connected ;
const playback_supported = this . hasOutputHardware ( ) && ( ! targetChannel || vconnection . decoding_supported ( targetChannel . properties . channel_codec ) ) ;
2019-04-04 19:47:52 +00:00
property_update [ "client_input_hardware" ] = recording_supported ;
property_update [ "client_output_hardware" ] = playback_supported ;
2020-08-19 17:33:57 +00:00
}
2019-04-04 19:47:52 +00:00
2020-08-19 17:33:57 +00:00
{
2019-04-04 19:47:52 +00:00
const client_properties = this . getClient ( ) . properties ;
for ( const key of Object . keys ( property_update ) ) {
if ( client_properties [ key ] === property_update [ key ] )
delete property_update [ key ] ;
}
if ( Object . keys ( property_update ) . length > 0 ) {
this . serverConnection . send_command ( "clientupdate" , property_update ) . catch ( error = > {
log . warn ( LogCategory . GENERAL , tr ( "Failed to update client audio hardware properties. Error: %o" ) , error ) ;
2020-08-19 17:33:57 +00:00
this . log . log ( EventType . ERROR_CUSTOM , { message : tr ( "Failed to update audio hardware properties." ) } ) ;
2019-04-04 19:47:52 +00:00
/* Update these properties anyways (for case the server fails to handle the command) */
const updates = [ ] ;
for ( const key of Object . keys ( property_update ) )
updates . push ( { key : key , value : ( property_update [ key ] ) + "" } ) ;
this . getClient ( ) . updateVariables ( . . . updates ) ;
} ) ;
}
2019-12-21 12:16:10 +00:00
}
2019-04-04 19:47:52 +00:00
2020-08-19 17:33:57 +00:00
if ( targetChannel ) {
if ( this . client_status . channel_codec_decoding_supported !== codecDecodeSupported || this . client_status . channel_codec_encoding_supported !== codecEncodeSupported ) {
this . client_status . channel_codec_decoding_supported = codecDecodeSupported ;
this . client_status . channel_codec_encoding_supported = codecEncodeSupported ;
2019-04-04 19:47:52 +00:00
let message ;
2020-08-19 17:33:57 +00:00
if ( ! codecEncodeSupported && ! codecDecodeSupported ) {
2019-04-04 19:47:52 +00:00
message = tr ( "This channel has an unsupported codec.<br>You cant speak or listen to anybody within this channel!" ) ;
2020-08-19 17:33:57 +00:00
} else if ( ! codecEncodeSupported ) {
2019-04-04 19:47:52 +00:00
message = tr ( "This channel has an unsupported codec.<br>You cant speak within this channel!" ) ;
2020-08-19 17:33:57 +00:00
} else if ( ! codecDecodeSupported ) {
message = tr ( "This channel has an unsupported codec.<br>You cant listen to anybody within this channel!" ) ;
}
if ( message ) {
2019-04-04 19:47:52 +00:00
createErrorModal ( tr ( "Channel codec unsupported" ) , message ) . open ( ) ;
2020-08-19 17:33:57 +00:00
}
2019-04-04 19:47:52 +00:00
}
}
this . client_status = this . client_status || { } as any ;
2020-08-19 17:33:57 +00:00
this . client_status . sound_record_supported = codecEncodeSupported ;
this . client_status . sound_playback_supported = codecDecodeSupported ;
2019-04-04 19:47:52 +00:00
2020-08-19 17:33:57 +00:00
{
const enableRecording = ! this . client_status . input_muted && ! this . client_status . output_muted ;
2019-08-21 18:07:00 +00:00
/* No need to start the microphone when we're not even connected */
2020-02-22 13:30:17 +00:00
2020-08-19 17:33:57 +00:00
const input = vconnection . voice_recorder ( ) ? . input ;
2020-02-22 13:30:17 +00:00
if ( input ) {
2020-08-19 17:33:57 +00:00
if ( enableRecording && this . serverConnection . connected ( ) ) {
if ( this . getInputHardwareState ( ) !== InputHardwareState . START_FAILED )
this . startVoiceRecorder ( Date . now ( ) - this . _last_record_error_popup > 10 * 1000 ) ;
2020-02-22 13:30:17 +00:00
} else {
input . stop ( ) ;
2019-08-21 08:00:01 +00:00
}
}
2019-05-20 16:57:14 +00:00
}
2019-04-04 19:47:52 +00:00
2020-04-09 13:10:14 +00:00
//TODO: Only trigger events for stuff which has been updated
this . event_registry . fire ( "notify_state_updated" , {
state : "microphone"
} ) ;
2020-08-19 17:33:57 +00:00
2020-04-09 13:10:14 +00:00
this . event_registry . fire ( "notify_state_updated" , {
state : "speaker"
} ) ;
top_menu . update_state ( ) ; //TODO: Top-Menu should register their listener
2019-04-04 19:47:52 +00:00
}
sync_status_with_server() {
if ( this . serverConnection . connected ( ) )
this . serverConnection . send_command ( "clientupdate" , {
client_input_muted : this.client_status.input_muted ,
client_output_muted : this.client_status.output_muted ,
client_away : typeof ( this . client_status . away ) === "string" || this . client_status . away ,
client_away_message : typeof ( this . client_status . away ) === "string" ? this . client_status . away : "" ,
2020-08-19 17:33:57 +00:00
client_input_hardware : this.client_status.sound_record_supported && this . getInputHardwareState ( ) === InputHardwareState . VALID ,
2019-04-04 19:47:52 +00:00
client_output_hardware : this.client_status.sound_playback_supported
} ) . catch ( error = > {
log . warn ( LogCategory . GENERAL , tr ( "Failed to sync handler state with server. Error: %o" ) , error ) ;
2020-07-21 22:55:28 +00:00
this . log . log ( EventType . ERROR_CUSTOM , { message : tr ( "Failed to sync handler state with server." ) } ) ;
2019-04-04 19:47:52 +00:00
} ) ;
}
2020-08-19 17:33:57 +00:00
/* can be called as much as you want, does nothing if nothing changed */
async acquireInputHardware() {
/* if we're having multiple recorders, try to get the right one */
let recorder : RecorderProfile = default_recorder ;
try {
await this . serverConnection . getVoiceConnection ( ) . acquire_voice_recorder ( recorder ) ;
} catch ( error ) {
logError ( LogCategory . AUDIO , tr ( "Failed to acquire recorder: %o" ) , error ) ;
createErrorModal ( tr ( "Failed to acquire recorder" ) , tr ( "Failed to acquire recorder.\nLookup the console for more details." ) ) . open ( ) ;
return ;
}
if ( this . connection_state === ConnectionState . CONNECTED ) {
await this . startVoiceRecorder ( true ) ;
} else {
this . setInputHardwareState ( InputHardwareState . VALID ) ;
}
2019-04-04 19:47:52 +00:00
}
2020-08-19 17:33:57 +00:00
async startVoiceRecorder ( notifyError : boolean ) {
const input = this . getVoiceRecorder ( ) ? . input ;
if ( ! input ) return ;
if ( input . currentState ( ) === InputState . PAUSED && this . connection_state === ConnectionState . CONNECTED ) {
try {
const result = await input . start ( ) ;
if ( result !== InputStartResult . EOK ) {
throw result ;
}
this . setInputHardwareState ( InputHardwareState . VALID ) ;
this . update_voice_status ( ) ;
} catch ( error ) {
this . setInputHardwareState ( InputHardwareState . START_FAILED ) ;
let errorMessage ;
if ( error === InputStartResult . ENOTSUPPORTED ) {
errorMessage = tr ( "Your browser does not support voice recording" ) ;
} else if ( error === InputStartResult . EBUSY ) {
errorMessage = tr ( "The input device is busy" ) ;
} else if ( error === InputStartResult . EDEVICEUNKNOWN ) {
errorMessage = tr ( "Invalid input device" ) ;
} else if ( error === InputStartResult . ENOTALLOWED ) {
errorMessage = tr ( "No permissions" ) ;
} else if ( error instanceof Error ) {
errorMessage = error . message ;
} else if ( typeof error === "string" ) {
errorMessage = error ;
} else {
errorMessage = tr ( "lookup the console" ) ;
}
log . warn ( LogCategory . VOICE , tr ( "Failed to start microphone input (%s)." ) , error ) ;
if ( notifyError ) {
this . _last_record_error_popup = Date . now ( ) ;
createErrorModal ( tr ( "Failed to start recording" ) , tra ( "Microphone start failed.\nError: {}" , errorMessage ) ) . open ( ) ;
}
}
} else {
this . setInputHardwareState ( InputHardwareState . VALID ) ;
}
2019-04-04 19:47:52 +00:00
}
2019-06-19 13:56:51 +00:00
2020-08-10 12:41:34 +00:00
getVoiceRecorder ( ) : RecorderProfile | undefined { return this . serverConnection . getVoiceConnection ( ) . voice_recorder ( ) ; }
2020-04-09 13:10:14 +00:00
2020-03-30 11:44:18 +00:00
reconnect_properties ( profile? : ConnectionProfile ) : ConnectParameters {
2019-06-19 13:56:51 +00:00
const name = ( this . getClient ( ) ? this . getClient ( ) . clientNickName ( ) : "" ) ||
( this . serverConnection && this . serverConnection . handshake_handler ( ) ? this . serverConnection . handshake_handler ( ) . parameters . nickname : "" ) ||
2020-03-30 11:44:18 +00:00
StaticSettings . instance . static ( Settings . KEY_CONNECT_USERNAME , profile ? profile.default_username : undefined ) ||
2019-06-19 13:56:51 +00:00
"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 : "" ) ;
const channel_password = ( this . getClient ( ) && this . getClient ( ) . currentChannel ( ) ? this . getClient ( ) . currentChannel ( ) . cached_password ( ) : "" ) ||
( this . serverConnection && this . serverConnection . handshake_handler ( ) ? ( this . serverConnection . handshake_handler ( ) . parameters . channel || { } as any ) . password : "" ) ;
return {
channel : channel ? { target : "/" + channel , password : channel_password } : undefined ,
nickname : name ,
password : this.serverConnection && this . serverConnection . handshake_handler ( ) ? this . serverConnection . handshake_handler ( ) . parameters.password : undefined
}
}
2019-08-21 08:00:01 +00:00
update_avatar() {
2020-03-30 11:44:18 +00:00
spawnAvatarUpload ( data = > {
2019-08-21 08:00:01 +00:00
if ( typeof ( data ) === "undefined" )
return ;
if ( data === null ) {
2019-08-30 21:06:39 +00:00
log . info ( LogCategory . CLIENT , tr ( "Deleting existing avatar" ) ) ;
2019-08-21 08:00:01 +00:00
this . serverConnection . send_command ( 'ftdeletefile' , {
name : "/avatar_" , /* delete own avatar */
path : "" ,
cid : 0
} ) . then ( ( ) = > {
createInfoModal ( tr ( "Avatar deleted" ) , tr ( "Avatar successfully deleted" ) ) . open ( ) ;
} ) . catch ( error = > {
2019-08-30 21:06:39 +00:00
log . error ( LogCategory . GENERAL , tr ( "Failed to reset avatar flag: %o" ) , error ) ;
2019-08-21 08:00:01 +00:00
let message ;
if ( error instanceof CommandResult )
2020-03-30 11:44:18 +00:00
message = formatMessage ( tr ( "Failed to delete avatar.{:br:}Error: {0}" ) , error . extra_message || error . message ) ;
2019-08-21 08:00:01 +00:00
if ( ! message )
2020-03-30 11:44:18 +00:00
message = formatMessage ( tr ( "Failed to delete avatar.{:br:}Lookup the console for more details" ) ) ;
2019-08-21 08:00:01 +00:00
createErrorModal ( tr ( "Failed to delete avatar" ) , message ) . open ( ) ;
return ;
} ) ;
} else {
2019-08-30 21:06:39 +00:00
log . info ( LogCategory . CLIENT , tr ( "Uploading new avatar" ) ) ;
2019-08-21 08:00:01 +00:00
( async ( ) = > {
2020-06-10 16:13:56 +00:00
const transfer = this . fileManager . initializeFileUpload ( {
name : "/avatar" ,
path : "" ,
channel : 0 ,
channelPassword : undefined ,
source : async ( ) = > await TransferProvider . provider ( ) . createBufferSource ( data )
} ) ;
await transfer . awaitFinished ( ) ;
if ( transfer . transferState ( ) !== FileTransferState . FINISHED ) {
if ( transfer . transferState ( ) === FileTransferState . ERRORED ) {
log . warn ( LogCategory . FILE_TRANSFER , tr ( "Failed to upload clients avatar: %o" ) , transfer . currentError ( ) ) ;
createErrorModal ( tr ( "Failed to upload avatar" ) , traj ( "Failed to upload avatar:{:br:}{0}" , transfer . currentErrorMessage ( ) ) ) . open ( ) ;
return ;
} else if ( transfer . transferState ( ) === FileTransferState . CANCELED ) {
createErrorModal ( tr ( "Failed to upload avatar" ) , tr ( "Your avatar upload has been canceled." ) ) . open ( ) ;
return ;
} else {
createErrorModal ( tr ( "Failed to upload avatar" ) , tr ( "Avatar upload finished with an unknown finished state." ) ) . open ( ) ;
return ;
2019-08-21 08:00:01 +00:00
}
}
try {
await this . serverConnection . send_command ( 'clientupdate' , {
2020-06-10 16:13:56 +00:00
client_flag_avatar : md5 ( new Uint8Array ( data ) )
2019-08-21 08:00:01 +00:00
} ) ;
} catch ( error ) {
2019-08-30 21:06:39 +00:00
log . error ( LogCategory . GENERAL , tr ( "Failed to update avatar flag: %o" ) , error ) ;
2019-08-21 08:00:01 +00:00
let message ;
if ( error instanceof CommandResult )
2020-03-30 11:44:18 +00:00
message = formatMessage ( tr ( "Failed to update avatar flag.{:br:}Error: {0}" ) , error . extra_message || error . message ) ;
2019-08-21 08:00:01 +00:00
if ( ! message )
2020-03-30 11:44:18 +00:00
message = formatMessage ( tr ( "Failed to update avatar flag.{:br:}Lookup the console for more details" ) ) ;
2019-08-21 08:00:01 +00:00
createErrorModal ( tr ( "Failed to set avatar" ) , message ) . open ( ) ;
return ;
}
createInfoModal ( tr ( "Avatar successfully uploaded" ) , tr ( "Your avatar has been uploaded successfully!" ) ) . open ( ) ;
} ) ( ) ;
}
} ) ;
}
destroy() {
2020-04-09 13:10:14 +00:00
this . event_registry . unregister_handler ( this ) ;
2019-08-21 08:00:01 +00:00
this . cancel_reconnect ( true ) ;
this . tag_connection_handler && this . tag_connection_handler . remove ( ) ;
this . tag_connection_handler = undefined ;
this . hostbanner && this . hostbanner . destroy ( ) ;
this . hostbanner = undefined ;
2020-08-07 11:40:11 +00:00
this . pluginCmdRegistry && this . pluginCmdRegistry . destroy ( ) ;
this . pluginCmdRegistry = undefined ;
2019-08-21 08:00:01 +00:00
this . _local_client && this . _local_client . destroy ( ) ;
this . _local_client = undefined ;
this . channelTree && this . channelTree . destroy ( ) ;
this . channelTree = undefined ;
this . side_bar && this . side_bar . destroy ( ) ;
this . side_bar = undefined ;
this . log && this . log . destroy ( ) ;
this . log = undefined ;
this . permissions && this . permissions . destroy ( ) ;
this . permissions = undefined ;
this . groups && this . groups . destroy ( ) ;
this . groups = undefined ;
this . fileManager && this . fileManager . destroy ( ) ;
this . fileManager = undefined ;
this . settings && this . settings . destroy ( ) ;
this . settings = undefined ;
if ( this . serverConnection ) {
2020-08-10 12:41:34 +00:00
getServerConnectionFactory ( ) . destroy ( this . serverConnection ) ;
2019-08-21 08:00:01 +00:00
}
this . serverConnection = undefined ;
this . sound = undefined ;
this . _local_client = undefined ;
}
2020-04-09 13:10:14 +00:00
/* state changing methods */
setMicrophoneMuted ( muted : boolean ) {
if ( this . client_status . input_muted === muted ) return ;
this . client_status . input_muted = muted ;
this . sound . play ( muted ? Sound.MICROPHONE_MUTED : Sound.MICROPHONE_ACTIVATED ) ;
this . update_voice_status ( ) ;
}
2020-04-10 18:57:50 +00:00
toggleMicrophone() { this . setMicrophoneMuted ( ! this . isMicrophoneMuted ( ) ) ; }
2020-04-09 13:10:14 +00:00
2020-08-19 17:33:57 +00:00
isMicrophoneMuted() { return this . client_status . input_muted ; }
isMicrophoneDisabled() { return this . inputHardwareState !== InputHardwareState . VALID ; }
2020-04-09 13:10:14 +00:00
setSpeakerMuted ( muted : boolean ) {
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 ;
if ( ! muted ) this . sound . play ( Sound . SOUND_ACTIVATED ) ; /* play the sound *after* we're setting we've unmuted the sound */
this . update_voice_status ( ) ;
}
2020-04-10 18:57:50 +00:00
toggleSpeakerMuted() { this . setSpeakerMuted ( ! this . isSpeakerMuted ( ) ) ; }
2020-04-09 13:10:14 +00:00
isSpeakerMuted() { return this . client_status . output_muted ; }
/ *
* Returns whatever the client is able to playback sound ( voice ) . Reasons for returning true could be :
* - Channel codec isn ' t supported
* - Voice bridge hasn ' t been set upped yet
* /
//TODO: This currently returns false
2020-08-07 11:40:11 +00:00
isSpeakerDisabled ( ) : boolean { return false ; }
2020-04-09 13:10:14 +00:00
setSubscribeToAllChannels ( flag : boolean ) {
if ( this . client_status . channel_subscribe_all === flag ) return ;
this . client_status . channel_subscribe_all = flag ;
if ( flag )
this . channelTree . subscribe_all_channels ( ) ;
else
this . channelTree . unsubscribe_all_channels ( ) ;
this . event_registry . fire ( "notify_state_updated" , { state : "subscribe" } ) ;
}
2020-08-07 11:40:11 +00:00
isSubscribeToAllChannels ( ) : boolean { return this . client_status . channel_subscribe_all ; }
2020-04-09 13:10:14 +00:00
setAway ( state : boolean | string ) {
this . setAway_ ( state , true ) ;
}
private setAway_ ( state : boolean | string , play_sound : boolean ) {
if ( this . client_status . away === state )
return ;
const was_away = this . isAway ( ) ;
const will_away = typeof state === "boolean" ? state : true ;
if ( was_away != will_away && play_sound )
this . sound . play ( will_away ? Sound.AWAY_ACTIVATED : Sound.AWAY_DEACTIVATED ) ;
this . client_status . away = state ;
this . serverConnection . send_command ( "clientupdate" , {
client_away : typeof ( this . client_status . away ) === "string" || this . client_status . away ,
client_away_message : typeof ( this . client_status . away ) === "string" ? this . client_status . away : "" ,
} ) . catch ( error = > {
log . warn ( LogCategory . GENERAL , tr ( "Failed to update away status. Error: %o" ) , error ) ;
2020-07-21 22:55:28 +00:00
this . log . log ( EventType . ERROR_CUSTOM , { message : tr ( "Failed to update away status." ) } ) ;
2020-04-09 13:10:14 +00:00
} ) ;
this . event_registry . fire ( "notify_state_updated" , {
state : "away"
} ) ;
}
2020-04-10 18:57:50 +00:00
toggleAway() { this . setAway ( ! this . isAway ( ) ) ; }
2020-04-09 13:10:14 +00:00
isAway ( ) : boolean { return typeof this . client_status . away !== "boolean" || this . client_status . away ; }
setQueriesShown ( flag : boolean ) {
if ( this . client_status . queries_visible === flag ) return ;
this . client_status . queries_visible = flag ;
this . channelTree . toggle_server_queries ( flag ) ;
this . event_registry . fire ( "notify_state_updated" , {
state : "query"
} ) ;
}
2020-08-07 11:40:11 +00:00
areQueriesShown ( ) : boolean {
2020-04-09 13:10:14 +00:00
return this . client_status . queries_visible ;
}
2020-07-19 15:12:41 +00:00
2020-08-19 17:33:57 +00:00
getInputHardwareState ( ) : InputHardwareState { return this . inputHardwareState ; }
private setInputHardwareState ( state : InputHardwareState ) {
if ( this . inputHardwareState === state )
return ;
this . inputHardwareState = state ;
this . event_registry . fire ( "notify_state_updated" , { state : "microphone" } ) ;
}
hasOutputHardware ( ) : boolean { return true ; }
2020-08-07 11:40:11 +00:00
getPluginCmdRegistry ( ) : PluginCmdRegistry { return this . pluginCmdRegistry ; }
2020-04-09 13:10:14 +00:00
}
export type ConnectionStateUpdateType = "microphone" | "speaker" | "away" | "subscribe" | "query" ;
export interface ConnectionEvents {
notify_state_updated : {
state : ConnectionStateUpdateType ;
}
notify_connection_state_changed : {
old_state : ConnectionState ,
new_state : ConnectionState
2020-06-11 09:17:56 +00:00
} ,
/* the handler has become visible/invisible for the client */
notify_visibility_changed : {
visible : boolean
2020-07-17 21:56:20 +00:00
} ,
/* fill only trigger once, after everything has been constructed */
notify_handler_initialized : { }
2019-04-04 19:47:52 +00:00
}