2020-07-21 22:55:28 +00:00
import * as loader from "tc-loader" ;
import { Stage } from "tc-loader" ;
2020-09-24 09:24:31 +00:00
import { server_connections } from "tc-shared/ConnectionManager" ;
2020-09-25 23:22:21 +00:00
import { getIconManager } from "tc-shared/file/Icons" ;
2021-01-10 16:36:57 +00:00
import { tr , tra } from "tc-shared/i18n/localize" ;
2020-12-18 16:06:38 +00:00
import { EventClient , EventServerAddress , EventType , TypeInfo } from "tc-shared/connectionlog/Definitions" ;
import { Settings , settings } from "tc-shared/settings" ;
import { format_time } from "tc-shared/ui/frames/chat" ;
import { ViewReasonId } from "tc-shared/ConnectionHandler" ;
import { formatDate } from "tc-shared/MessageFormatter" ;
import { renderBBCodeAsText } from "tc-shared/text/bbcode" ;
2021-01-10 16:36:57 +00:00
import { LogCategory , logInfo , logTrace } from "tc-shared/log" ;
2020-07-21 22:55:28 +00:00
export type DispatcherLog < T extends keyof TypeInfo > = ( data : TypeInfo [ T ] , handlerId : string , eventType : T ) = > void ;
2020-12-18 16:06:38 +00:00
const notificationDefaultStatus : { [ T in keyof TypeInfo ] ? : boolean } = { } ;
notificationDefaultStatus [ "client.poke.received" ] = true ;
notificationDefaultStatus [ "server.banned" ] = true ;
notificationDefaultStatus [ "server.closed" ] = true ;
notificationDefaultStatus [ "server.host.message.disconnect" ] = true ;
notificationDefaultStatus [ "global.message" ] = true ;
notificationDefaultStatus [ "connection.failed" ] = true ;
notificationDefaultStatus [ "private.message.received" ] = true ;
notificationDefaultStatus [ "connection.voice.dropped" ] = true ;
2020-07-21 22:55:28 +00:00
let windowFocused = false ;
document . addEventListener ( "focusin" , ( ) = > windowFocused = true ) ;
document . addEventListener ( "focusout" , ( ) = > windowFocused = false ) ;
const dispatchers : { [ key : string ] : DispatcherLog < any > } = { } ;
function registerDispatcher < T extends keyof TypeInfo > ( key : T , builder : DispatcherLog < T > ) {
dispatchers [ key ] = builder ;
}
export function findNotificationDispatcher < T extends keyof TypeInfo > ( type : T ) : DispatcherLog < T > {
return dispatchers [ type ] ;
}
export function getRegisteredNotificationDispatchers ( ) : TypeInfo [ ] {
return Object . keys ( dispatchers ) as any ;
}
export function isNotificationEnabled ( type : EventType ) {
2021-01-10 15:13:15 +00:00
return settings . getValue ( Settings . FN_EVENTS_NOTIFICATION_ENABLED ( type ) , notificationDefaultStatus [ type as any ] || false ) ;
2020-07-21 22:55:28 +00:00
}
const kDefaultIcon = "img/teaspeak_cup_animated.png" ;
async function resolveAvatarUrl ( client : EventClient , handlerId : string ) {
const connection = server_connections . findConnection ( handlerId ) ;
const avatar = connection . fileManager . avatars . resolveClientAvatar ( { clientUniqueId : client.client_unique_id , id : client.client_id } ) ;
await avatar . awaitLoaded ( ) ;
return avatar . getAvatarUrl ( ) ;
}
async function resolveServerIconUrl ( handlerId : string ) {
const connection = server_connections . findConnection ( handlerId ) ;
if ( connection . channelTree . server . properties . virtualserver_icon_id ) {
2020-09-25 23:22:21 +00:00
const icon = getIconManager ( ) . resolveIcon ( connection . channelTree . server . properties . virtualserver_icon_id , connection . getCurrentServerUniqueId ( ) , connection . handlerId ) ;
await icon . awaitLoaded ( ) ;
if ( icon . getState ( ) === "loaded" && icon . iconId > 1000 ) {
return icon . getImageUrl ( ) ;
}
2020-07-21 22:55:28 +00:00
}
return kDefaultIcon ;
}
function spawnNotification ( title : string , options : NotificationOptions ) {
if ( ! options . icon )
options . icon = kDefaultIcon ;
if ( 'Notification' in window ) {
try {
new Notification ( title , options ) ;
} catch ( error ) {
2021-01-10 16:36:57 +00:00
logTrace ( LogCategory . GENERAL , tr ( "Failed to spawn notification: %o" ) , error ) ;
2020-07-21 22:55:28 +00:00
}
}
}
function spawnServerNotification ( handlerId : string , options : NotificationOptions ) {
resolveServerIconUrl ( handlerId ) . then ( iconUrl = > {
const connection = server_connections . findConnection ( handlerId ) ;
if ( ! connection ) return ;
options . icon = iconUrl ;
spawnNotification ( connection . channelTree . server . properties . virtualserver_name , options ) ;
} ) ;
}
function spawnClientNotification ( handlerId : string , client : EventClient , options : NotificationOptions ) {
resolveAvatarUrl ( client , handlerId ) . then ( avatarUrl = > {
const connection = server_connections . findConnection ( handlerId ) ;
if ( ! connection ) return ;
options . icon = avatarUrl ;
spawnNotification ( connection . channelTree . server . properties . virtualserver_name , options ) ;
} ) ;
}
const formatServerAddress = ( address : EventServerAddress ) = > address . server_hostname + ( address . server_port === 9987 ? "" : ":" + address . server_port ) ;
registerDispatcher ( EventType . CONNECTION_BEGIN , data = > {
spawnNotification ( tr ( "Connecting..." ) , {
body : tra ( "Connecting to {}" , formatServerAddress ( data . address ) )
} ) ;
} ) ;
/* Snipped CONNECTION_HOSTNAME_RESOLVE */
registerDispatcher ( EventType . CONNECTION_HOSTNAME_RESOLVED , data = > {
spawnNotification ( tr ( "Hostname resolved" ) , {
body : tra ( "Hostname resolved successfully to {}" , formatServerAddress ( data . address ) )
} ) ;
} ) ;
registerDispatcher ( EventType . CONNECTION_HOSTNAME_RESOLVE_ERROR , data = > {
spawnNotification ( tr ( "Connect failed" ) , {
body : tra ( "Failed to resolve hostname.\nConnecting to given hostname.\nError: {0}" , data . message )
} ) ;
} ) ;
/* Snipped CONNECTION_LOGIN */
registerDispatcher ( EventType . CONNECTION_CONNECTED , data = > {
spawnNotification ( tra ( "Connected to {}" , formatServerAddress ( data . serverAddress ) ) , {
body : tra ( "You connected as {}" , data . own_client . client_name )
} ) ;
} ) ;
registerDispatcher ( EventType . CONNECTION_FAILED , data = > {
spawnNotification ( tra ( "Connection to {} failed" , formatServerAddress ( data . serverAddress ) ) , {
body : tra ( "Failed to connect to {}." , formatServerAddress ( data . serverAddress ) )
} ) ;
} ) ;
registerDispatcher ( EventType . DISCONNECTED , ( ) = > {
spawnNotification ( tra ( "You disconnected from the server" ) , { } ) ;
} ) ;
/* snipped RECONNECT_SCHEDULED */
/* snipped RECONNECT_EXECUTE */
/* snipped RECONNECT_CANCELED */
2020-08-10 17:39:28 +00:00
registerDispatcher ( EventType . CONNECTION_VOICE_CONNECT , ( data , handlerId ) = > {
spawnServerNotification ( handlerId , {
body : tr ( "Connecting voice bridge." )
} ) ;
} ) ;
registerDispatcher ( EventType . CONNECTION_VOICE_CONNECT_SUCCEEDED , ( data , handlerId ) = > {
spawnServerNotification ( handlerId , {
body : tr ( "Voice bridge successfully connected." )
} ) ;
} ) ;
registerDispatcher ( EventType . CONNECTION_VOICE_CONNECT_FAILED , ( data , handlerId ) = > {
2020-07-21 22:55:28 +00:00
spawnServerNotification ( handlerId , {
body : tra ( "Failed to setup voice bridge: {0}. Allow reconnect: {1}" , data . reason , data . reconnect_delay > 0 ? tr ( "Yes" ) : tr ( "No" ) )
} ) ;
} ) ;
2020-08-10 17:39:28 +00:00
registerDispatcher ( EventType . CONNECTION_VOICE_DROPPED , ( data , handlerId ) = > {
spawnServerNotification ( handlerId , {
body : tr ( "Voice bridge has been dropped. Trying to reconnect." )
} ) ;
} ) ;
2020-07-21 22:55:28 +00:00
registerDispatcher ( EventType . CONNECTION_COMMAND_ERROR , ( data , handlerId ) = > {
spawnServerNotification ( handlerId , {
body : tra ( "Command execution resulted in an error." )
} ) ;
} ) ;
registerDispatcher ( EventType . SERVER_WELCOME_MESSAGE , ( data , handlerId ) = > {
spawnServerNotification ( handlerId , {
body : tra ( "Welcome message:\n{}" , data . message )
} ) ;
} ) ;
registerDispatcher ( EventType . SERVER_HOST_MESSAGE , ( data , handlerId ) = > {
spawnServerNotification ( handlerId , {
body : tra ( "Host message:\n{}" , data . message )
} ) ;
} ) ;
registerDispatcher ( EventType . SERVER_HOST_MESSAGE_DISCONNECT , ( data ) = > {
spawnNotification ( tr ( "Connection to server denied" ) , {
body : tra ( "Server message:\n{}" , data . message )
} ) ;
} ) ;
registerDispatcher ( EventType . SERVER_CLOSED , ( data , handlerId ) = > {
spawnServerNotification ( handlerId , {
body : data.message ? tra ( "Server has been closed ({})" , data . message ) : tr ( "Server has been closed" )
} ) ;
} ) ;
registerDispatcher ( EventType . SERVER_BANNED , ( data , handlerId ) = > {
const time = data . time === 0 ? "ever" : format_time ( data . time * 1000 , tr ( "one second" ) ) ;
const reason = data . message ? " Reason: " + data . message : "" ;
spawnServerNotification ( handlerId , {
body : data.invoker.client_id > 0 ? tra ( "You've been banned from the server by {0} for {1}.{2}" , data . invoker . client_name , time , reason ) :
2020-12-18 16:06:38 +00:00
tra ( "You've been banned from the server for {0}.{1}" , time , reason )
2020-07-21 22:55:28 +00:00
} ) ;
} ) ;
registerDispatcher ( EventType . SERVER_REQUIRES_PASSWORD , ( ) = > {
spawnNotification ( tra ( "Failed to connect to the server" ) , {
body : tr ( "Server requires a password to connect." )
} ) ;
} ) ;
registerDispatcher ( EventType . CLIENT_VIEW_ENTER , ( data , handlerId ) = > {
let message ;
switch ( data . reason ) {
case ViewReasonId . VREASON_USER_ACTION :
if ( data . channel_from ) {
message = tra ( "{0} appeared from {1} to {2}" , data . client . client_name , data . channel_from . channel_name , data . channel_to . channel_name ) ;
} else {
message = tra ( "{0} appeared to channel {1}" , data . client . client_name , data . channel_to . channel_name ) ;
}
break ;
case ViewReasonId . VREASON_MOVED :
if ( data . channel_from ) {
message = tra ( "{0} appeared from {1} to {2}, moved by {3}" , data . client . client_name , data . channel_from . channel_name , data . channel_to . channel_name , data . invoker . client_name ) ;
} else {
message = tra ( "{0} appeared to {1}, moved by {2}" , data . client . client_name , data . channel_to . channel_name , data . invoker . client_name ) ;
}
break ;
case ViewReasonId . VREASON_CHANNEL_KICK :
if ( data . channel_from ) {
message = tra ( "{0} appeared from {1} to {2}, kicked by {3}{4}" , data . client . client_name , data . channel_from . channel_name , data . channel_to . channel_name , data . invoker . client_name , data . message ? " (" + data . message + ")" : "" ) ;
} else {
message = tra ( "{0} appeared to {1}, kicked by {2}{3}" , data . client . client_name , data . channel_to . channel_name , data . invoker . client_name , data . message ? " (" + data . message + ")" : "" ) ;
}
break ;
default :
return ;
}
spawnClientNotification ( handlerId , data . client , {
body : message
} ) ;
} ) ;
registerDispatcher ( EventType . CLIENT_VIEW_ENTER_OWN_CHANNEL , ( data , handlerId ) = > {
let message ;
switch ( data . reason ) {
case ViewReasonId . VREASON_USER_ACTION :
if ( data . channel_from ) {
message = tra ( "{0} appeared from {1} to your channel {2}" , data . client . client_name , data . channel_from . channel_name , data . channel_to . channel_name ) ;
} else {
message = tra ( "{0} appeared to your channel {1}" , data . client . client_name , data . channel_to . channel_name ) ;
}
break ;
case ViewReasonId . VREASON_MOVED :
if ( data . channel_from ) {
message = tra ( "{0} appeared from {1} to your channel {2}, moved by {3}" , data . client . client_name , data . channel_from . channel_name , data . channel_to . channel_name , data . invoker . client_name ) ;
} else {
message = tra ( "{0} appeared to your channel {1}, moved by {2}" , data . client . client_name , data . channel_to . channel_name , data . invoker . client_name ) ;
}
break ;
case ViewReasonId . VREASON_CHANNEL_KICK :
if ( data . channel_from ) {
message = tra ( "{0} appeared from {1} to your channel {2}, kicked by {3}{4}" , data . client . client_name , data . channel_from . channel_name , data . channel_to . channel_name , data . invoker . client_name , data . message ? " (" + data . message + ")" : "" ) ;
} else {
message = tra ( "{0} appeared to your channel {1}, kicked by {2}{3}" , data . client . client_name , data . channel_to . channel_name , data . invoker . client_name , data . message ? " (" + data . message + ")" : "" ) ;
}
break ;
default :
return ;
}
spawnClientNotification ( handlerId , data . client , {
body : message
} ) ;
} ) ;
registerDispatcher ( EventType . CLIENT_VIEW_MOVE , ( data , handlerId ) = > {
let message ;
switch ( data . reason ) {
case ViewReasonId . VREASON_MOVED :
message = tra ( "{0} was moved from channel {1} to {2} by {3}" , data . client . client_name , data . channel_from . channel_name , data . channel_to . channel_name , data . invoker . client_name ) ;
break ;
case ViewReasonId . VREASON_USER_ACTION :
message = tra ( "{0} switched from channel {1} to {2}" , data . client . client_name , data . channel_from . channel_name , data . channel_to . channel_name ) ;
break ;
case ViewReasonId . VREASON_CHANNEL_KICK :
message = tra ( "{0} got kicked from channel {1} to {2} by {3}{4}" , data . client . client_name , data . channel_from . channel_name , data . channel_to . channel_name , data . invoker . client_name , data . message ? " (" + data . message + ")" : "" ) ;
break ;
default :
return ;
}
spawnClientNotification ( handlerId , data . client , {
body : message
} ) ;
} ) ;
2020-12-18 16:06:38 +00:00
registerDispatcher ( EventType . CLIENT_VIEW_MOVE_OWN_CHANNEL , findNotificationDispatcher ( EventType . CLIENT_VIEW_MOVE ) ) ;
2020-07-21 22:55:28 +00:00
registerDispatcher ( EventType . CLIENT_VIEW_MOVE_OWN , ( data , handlerId ) = > {
let message ;
switch ( data . reason ) {
case ViewReasonId . VREASON_MOVED :
message = tra ( "You have been moved by {3} from channel {1} to {2}" , data . client . client_name , data . channel_from . channel_name , data . channel_to . channel_name , data . invoker . client_name ) ;
break ;
case ViewReasonId . VREASON_USER_ACTION :
/* no need to notify here */
return ;
case ViewReasonId . VREASON_CHANNEL_KICK :
message = tra ( "You got kicked out of the channel {1} to channel {2} by {3}{4}" , data . client . client_name , data . channel_from . channel_name , data . channel_to . channel_name , data . invoker . client_name , data . message ? " (" + data . message + ")" : "" ) ;
break ;
default :
return ;
}
spawnClientNotification ( handlerId , data . client , {
body : message
} ) ;
} ) ;
registerDispatcher ( EventType . CLIENT_VIEW_LEAVE , ( data , handlerId ) = > {
let message ;
switch ( data . reason ) {
case ViewReasonId . VREASON_USER_ACTION :
message = tra ( "{0} disappeared from {1} to {2}" , data . client . client_name , data . channel_from . channel_name , data . channel_to . channel_name ) ;
break ;
case ViewReasonId . VREASON_SERVER_LEFT :
message = tra ( "{0} left the server{1}" , data . client . client_name , data . message ? " (" + data . message + ")" : "" ) ;
break ;
case ViewReasonId . VREASON_SERVER_KICK :
2021-04-24 11:59:49 +00:00
message = tra ( "{0} was kicked from the server by {1}.{2}" , data . client . client_name , data . invoker . client_name , data . message ? " (" + data . message + ")" : "" ) ;
2020-07-21 22:55:28 +00:00
break ;
case ViewReasonId . VREASON_CHANNEL_KICK :
2021-04-24 11:59:49 +00:00
message = tra ( "{0} was kicked from channel {1} by {2}.{3}" , data . client . client_name , data . channel_from . channel_name , data . invoker . client_name , data . message ? " (" + data . message + ")" : "" ) ;
2020-07-21 22:55:28 +00:00
break ;
case ViewReasonId . VREASON_BAN :
let duration = "permanently" ;
if ( data . ban_time )
duration = tr ( "for" ) + " " + formatDate ( data . ban_time ) ;
message = tra ( "{0} was banned {1} by {2}.{3}" , data . client . client_name , duration , data . invoker . client_name , data . message ? " (" + data . message + ")" : "" ) ;
break ;
case ViewReasonId . VREASON_TIMEOUT :
message = tra ( "{0} timed out{1}" , data . client . client_name , data . message ? " (" + data . message + ")" : "" ) ;
break ;
case ViewReasonId . VREASON_MOVED :
message = tra ( "{0} disappeared from {1} to {2}, moved by {3}" , data . client . client_name , data . channel_from . channel_name , data . channel_to . channel_name , data . invoker . client_name ) ;
break ;
default :
return ;
}
spawnClientNotification ( handlerId , data . client , {
body : message
} ) ;
} ) ;
registerDispatcher ( EventType . CLIENT_VIEW_LEAVE_OWN_CHANNEL , ( data , handlerId ) = > {
let message ;
switch ( data . reason ) {
case ViewReasonId . VREASON_USER_ACTION :
message = tra ( "{0} disappeared from your channel {1} to {2}" , data . client . client_name , data . channel_from . channel_name , data . channel_to . channel_name ) ;
break ;
case ViewReasonId . VREASON_MOVED :
message = tra ( "{0} disappeared from your channel {1} to {2}, moved by {3}" , data . client . client_name , data . channel_from . channel_name , data . channel_to . channel_name , data . invoker . client_name ) ;
break ;
default :
2020-12-18 16:06:38 +00:00
return findNotificationDispatcher ( "client.view.leave" ) ( data , handlerId , EventType . CLIENT_VIEW_LEAVE ) ;
2020-07-21 22:55:28 +00:00
}
spawnClientNotification ( handlerId , data . client , {
body : message
} ) ;
} ) ;
registerDispatcher ( EventType . CLIENT_NICKNAME_CHANGED , ( data , handlerId ) = > {
spawnClientNotification ( handlerId , data . client , {
body : tra ( "{0} changed his nickname from \"{1}\" to \"{2}\"" , data . client . client_name , data . old_name , data . new_name )
} ) ;
} ) ;
/* snipped CLIENT_NICKNAME_CHANGED_OWN */
/* snipped CLIENT_NICKNAME_CHANGE_FAILED */
registerDispatcher ( EventType . CHANNEL_CREATE , ( data , handlerId ) = > {
spawnServerNotification ( handlerId , {
body : tra ( "Channel {} has been created by {}." , data . channel . channel_name , data . creator . client_name )
} ) ;
} ) ;
registerDispatcher ( EventType . CHANNEL_DELETE , ( data , handlerId ) = > {
spawnServerNotification ( handlerId , {
body : tra ( "Channel {} has been deleted by {}." , data . channel . channel_name , data . deleter . client_name )
} ) ;
} ) ;
/* snipped CHANNEL_CREATE_OWN */
/* snipped CHANNEL_DELETE_OWN */
/* snipped ERROR_CUSTOM */
/* snipped ERROR_PERMISSION */
/ * T O D O !
CLIENT_SERVER_GROUP_ADD = "client.server.group.add" ,
CLIENT_SERVER_GROUP_REMOVE = "client.server.group.remove" ,
CLIENT_CHANNEL_GROUP_CHANGE = "client.channel.group.change" ,
* /
registerDispatcher ( EventType . CLIENT_POKE_RECEIVED , ( data , handlerId ) = > {
resolveAvatarUrl ( data . sender , handlerId ) . then ( avatarUrl = > {
const connection = server_connections . findConnection ( handlerId ) ;
if ( ! connection ) return ;
new Notification ( connection . channelTree . server . properties . virtualserver_name , {
body : tr ( "You've peen poked by" ) + " " + data . sender . client_name + ( data . message ? ":\n" + renderBBCodeAsText ( data . message ) : "" ) ,
icon : avatarUrl
} ) ;
} ) ;
} ) ;
/* snipped CLIENT_POKE_SEND */
registerDispatcher ( EventType . GLOBAL_MESSAGE , ( data , handlerId ) = > {
if ( windowFocused )
return ;
spawnServerNotification ( handlerId , {
body : tra ( "{} send a server message: {}" , data . sender . client_name , renderBBCodeAsText ( data . message ) ) ,
} ) ;
} ) ;
registerDispatcher ( EventType . PRIVATE_MESSAGE_RECEIVED , ( data , handlerId ) = > {
if ( windowFocused )
return ;
spawnClientNotification ( handlerId , data . sender , {
body : tra ( "Private message from {}: {}" , data . sender . client_name , renderBBCodeAsText ( data . message ) ) ,
} ) ;
} ) ;
2020-11-17 12:10:24 +00:00
registerDispatcher ( EventType . WEBRTC_FATAL_ERROR , ( data , handlerId ) = > {
2020-12-18 16:06:38 +00:00
if ( data . retryTimeout ) {
let time = Math . ceil ( data . retryTimeout / 1000 ) ;
let minutes = Math . floor ( time / 60 ) ;
let seconds = time % 60 ;
spawnServerNotification ( handlerId , {
body : tra ( "WebRTC connection closed due to a fatal error:\n{}\nRetry scheduled in {}." , data . message , ( minutes > 0 ? minutes + "m" : "" ) + seconds + "s" )
} ) ;
} else {
spawnServerNotification ( handlerId , {
body : tra ( "WebRTC connection closed due to a fatal error:\n{}\nNo retry scheduled." , data . message )
} ) ;
}
2020-11-17 12:10:24 +00:00
} ) ;
2020-07-21 22:55:28 +00:00
/* snipped PRIVATE_MESSAGE_SEND */
loader . register_task ( Stage . LOADED , {
function : async ( ) = > {
if ( ! ( 'Notification' in window ) )
return ;
if ( Notification . permission === "granted" )
return ;
2020-09-07 12:52:30 +00:00
/* yeahr fuck safari */
const promise = Notification . requestPermission ( result = > {
2020-12-18 16:06:38 +00:00
logInfo ( LogCategory . GENERAL , tr ( "Notification permission request (callback) resulted in %s" ) , result ) ;
2020-09-07 12:52:30 +00:00
} )
if ( typeof promise !== "undefined" && 'then' in promise ) {
promise . then ( result = > {
2020-12-18 16:06:38 +00:00
logInfo ( LogCategory . GENERAL , tr ( "Notification permission request resulted in %s" ) , result ) ;
2020-09-07 12:52:30 +00:00
} ) . catch ( error = > {
2020-12-18 16:06:38 +00:00
logInfo ( LogCategory . GENERAL , tr ( "Failed to execute notification permission request: %O" ) , error ) ;
2020-09-07 12:52:30 +00:00
} ) ;
}
2020-07-21 22:55:28 +00:00
} ,
name : "Request notifications" ,
priority : 1
} ) ;