2021-12-06 10:23:29 -05:00
import axios from 'axios' ;
2021-12-05 18:33:49 -05:00
import { Socket } from 'net' ;
2021-12-06 10:23:29 -05:00
import { Channel } from './Channel.js' ;
2021-12-05 18:33:49 -05:00
import { IRCUser } from './IRCUser.js' ;
import { IRCMessage , parseIRCMessage } from './Message.js' ;
import numerics from './numerics.js' ;
import { Server } from './Server.js' ;
export class Client {
user : IRCUser | null
capVersion : string
enabledCaps : Map < string , string >
allCaps : Map < string , string >
localNick : string
localUsername : string
localRealname : string
2021-12-06 10:23:29 -05:00
deviceId : string
2021-12-05 18:33:49 -05:00
constructor ( private socket : Socket , public server : Server ) {
this . user = null ;
this . capVersion = '301' ;
this . enabledCaps = new Map ( ) ;
this . allCaps = new Map ( [
[ "account-tag" , "" ] ,
[ "batch" , "" ] ,
[ "draft/chathistory" , "" ] ,
[ "echo-message" , "" ] ,
[ "draft/event-playback" , "" ] ,
2021-12-06 10:23:29 -05:00
[ "extended-join" , "" ] ,
2021-12-05 18:33:49 -05:00
[ "invite-notify" , "" ] ,
[ "message-tags" , "" ] ,
[ "sasl" , "PLAIN" ] ,
[ "server-time" , "" ] ,
] ) ;
this . localNick = 'none' ;
this . localUsername = 'none' ;
this . localRealname = 'none' ;
2021-12-06 10:23:29 -05:00
this . deviceId = "" ;
2021-12-05 18:33:49 -05:00
this . socket . on ( 'data' , ( data ) = > this . receiveData ( data ) ) ;
//this.socket.on('close', (e) => {if (this.user) this.user.handleClientClose(this, e)});
}
receiveData ( data : Buffer | String ) {
const dataArray = data . toString ( ) . split ( '\r\n' ) ;
dataArray . forEach ( m = > {
const trimmedMsg = m . replace ( '\r' , '' ) . replace ( '\n' , '' ) ;
if ( trimmedMsg !== '' )
this . routeMessage ( trimmedMsg ) ;
} ) ;
}
routeMessage ( data : string ) {
const message = parseIRCMessage ( data ) ;
switch ( message . command . toUpperCase ( ) ) {
case 'AUTHENTICATE' : {
this . doAUTHENTICATE ( message ) ;
break ;
}
case 'CAP' : {
this . doCAP ( message ) ;
break ;
}
2021-12-06 10:23:29 -05:00
case 'MODE' : {
if ( ! this . user ) {
return ;
}
const maybeChannel = this . user . channels . get ( message . params [ 0 ] ) ;
if ( maybeChannel ) {
maybeChannel . sendMode ( this , message . tags ) ;
}
break ;
}
case 'NAMES' : {
if ( ! this . user ) {
return ;
}
const maybeChannel = this . user . channels . get ( message . params [ 0 ] ) ;
if ( maybeChannel ) {
maybeChannel . sendNames ( this , message . tags ) ;
}
break ;
}
2021-12-07 10:36:09 -05:00
case 'NOTICE' : {
if ( this . user ) {
this . user . sendMessageToMatrix ( message , this ) ;
}
break ;
}
2021-12-05 18:47:52 -05:00
case 'PING' : {
this . sendMessage ( this . server . name , "PONG" , message . params , message . tags ) ;
break ;
}
2021-12-07 10:36:09 -05:00
case 'PRIVMSG' : {
if ( this . user ) {
this . user . sendMessageToMatrix ( message , this ) ;
}
break ;
}
2021-12-06 10:23:29 -05:00
case 'WHO' : {
if ( ! this . user ) {
return ;
}
const maybeChannel = this . user . channels . get ( message . params [ 0 ] ) ;
if ( maybeChannel ) {
maybeChannel . sendWho ( this , message . tags ) ;
}
break ;
}
2021-12-05 18:33:49 -05:00
}
}
getCapString ( capVersion : string ) {
let capArray = [ ] ;
for ( const [ key , value ] of this . allCaps . entries ( ) ) {
if ( capVersion === '301' || value . length === 0 ) {
capArray . push ( key ) ;
}
else {
capArray . push ( ` ${ key } = ${ value } ` ) ;
}
}
return capArray . join ( ' ' ) ;
}
doCAP ( message : IRCMessage ) {
switch ( message . params [ 0 ] ) {
case 'LS' : {
if ( message . params . length === 2 ) {
this . capVersion = message . params [ 1 ] ;
}
this . sendMessage ( this . server . name , "CAP" , [ "*" , "LS" , this . getCapString ( this . capVersion ) ] , message . tags ) ;
break ;
}
case 'LIST' : {
this . sendMessage ( this . server . name , "CAP" , [ "*" , "LIST" , this . getCapString ( this . capVersion ) ] , message . tags ) ;
break ;
}
case 'REQ' : {
const capsToChange = ( message . params [ 1 ] . indexOf ( ' ' ) === - 1 ) ? [ message . params [ 1 ] ] : message . params [ 1 ] . split ( ' ' ) ;
const capsEnabled : string [ ] = [ ] ;
capsToChange . forEach ( cap = > {
2021-12-07 10:37:07 -05:00
if ( this . allCaps . has ( cap ) ) {
2021-12-05 18:33:49 -05:00
this . enabledCaps . set ( cap , '' ) ;
capsEnabled . push ( cap ) ;
}
} ) ;
this . sendMessage ( this . server . name , "CAP" , [ "*" , "ACK" , capsEnabled . join ( ' ' ) ] , message . tags ) ;
break ;
}
case 'END' : {
if ( this . user !== null ) {
this . doRegistration ( message ) ;
}
else {
this . closeConnectionWithError ( "You must use SASL to connect to this server" ) ;
}
}
}
}
doAUTHENTICATE ( message : IRCMessage ) {
if ( message . params [ 0 ] === "PLAIN" ) {
this . sendMessage ( "" , "AUTHENTICATE" , [ "+" ] , message . tags ) ;
}
else {
const authArray = Buffer . from ( message . params [ 0 ] , 'base64' ) . toString ( 'utf-8' ) . split ( '\0' ) ;
if ( ! authArray || authArray . length !== 3 ) {
this . sendMessage ( this . server . name , '904' , numerics [ '904' ] ( '*' ) , message . tags )
this . closeConnectionWithError ( 'Invalid authentication' )
}
const mxid = authArray [ 1 ] ;
const accessToken = authArray [ 2 ] ;
2021-12-06 10:23:29 -05:00
const thisIRCUser = this . server . getOrCreateIRCUser ( mxid , accessToken ) ;
thisIRCUser . getVerification ( ) . then ( ( response ) = > {
if ( response . status === 401 || response . status === 403 ) {
2021-12-05 18:33:49 -05:00
this . sendMessage ( this . server . name , '904' , numerics [ '904' ] ( '*' ) , message . tags )
this . closeConnectionWithError ( 'Invalid authentication' )
2021-12-06 10:23:29 -05:00
} else if ( response . status === 429 ) {
this . sendMessage ( this . server . name , '904' , numerics [ '904' ] ( '*' ) , message . tags )
this . closeConnectionWithError ( 'rate limited, please try again later' )
} else if ( response . status !== 200 ) {
this . sendMessage ( this . server . name , '904' , numerics [ '904' ] ( '*' ) , message . tags )
this . closeConnectionWithError ( 'verification failed, please check credentials' )
2021-12-05 18:33:49 -05:00
}
2021-12-06 10:23:29 -05:00
this . deviceId = response . data . device_id
if ( response . data . user_id !== mxid ) {
this . sendMessage ( this . server . name , '904' , numerics [ '904' ] ( '*' ) , message . tags )
this . closeConnectionWithError ( 'access token does not match mxid, please check credentials' )
} else {
2021-12-05 18:33:49 -05:00
this . user = thisIRCUser ;
2021-12-05 18:44:21 -05:00
this . sendMessage ( this . server . name , '900' , numerics [ '900' ] ( this . user . getMask ( ) , this . user . nick ) , new Map ( ) ) ;
this . sendMessage ( this . server . name , '903' , numerics [ '903' ] ( this . user . nick ) , new Map ( ) ) ;
2021-12-06 12:07:40 -05:00
if ( this . user . isSynced ( ) ) {
2021-12-06 10:23:29 -05:00
this . user . addClient ( this , message . tags ) ;
2021-12-06 12:07:40 -05:00
} else {
axios . get ( ` https://matrix.org/_matrix/client/v3/sync?access_token= ${ accessToken } ` ) . then ( response = > {
const data = response . data ;
const rooms = data . rooms ;
if ( this . user === null )
return ;
if ( rooms [ 'join' ] ) {
for ( const roomId of Object . keys ( rooms . join ) ) {
2021-12-07 08:51:31 -05:00
const targetChannel = this . server . getOrCreateIRCChannel ( roomId ) ;
2021-12-06 12:07:40 -05:00
this . user . channels . set ( targetChannel . name , targetChannel ) ;
rooms . join [ roomId ] . state . events . forEach ( ( nextEvent : any ) = > targetChannel . routeMatrixEvent ( nextEvent ) ) ;
}
}
if ( this . user === null )
return ;
this . user . nextBatch = data . next_batch ;
2021-12-07 11:12:25 -05:00
this . sendMessage ( this . server . name , 'NOTICE' , [ this . user . nick , 'You are now synced to the network!' ] , message . tags ) ;
2021-12-06 12:07:40 -05:00
this . user . addClient ( this , message . tags ) ;
} )
}
2021-12-05 18:33:49 -05:00
}
2021-12-06 10:23:29 -05:00
} )
2021-12-05 18:33:49 -05:00
}
}
doRegistration ( message : IRCMessage ) {
if ( this . user === null ) {
2021-12-05 18:44:21 -05:00
this . closeConnectionWithError ( "You must use SASL to connect to this server" ) ;
2021-12-05 18:33:49 -05:00
return ;
}
this . sendMessage ( this . server . name , '001' , numerics [ '001' ] ( this . user . nick , this . server . name ) , message . tags ) ;
this . sendMessage ( this . server . name , '002' , numerics [ '002' ] ( this . user . nick , this . server . name , '0.0.1' ) , message . tags ) ;
this . sendMessage ( this . server . name , '003' , numerics [ '003' ] ( this . user . nick , 'yesterday' ) , message . tags ) ;
this . sendMessage ( this . server . name , '004' , numerics [ '004' ] ( this . user . nick , this . server . name , '0.0.1' , 'i' , 'Lhionpsv' ) , message . tags ) ;
const iSupportArray = [
'CASEMAPPING=ascii' ,
'CHANMODES=,,,Linps' ,
'CHANTYPES=#&!' ,
'MAXTARGETS=1' ,
'PREFIX=(ohv)@%+' ,
]
if ( this . enabledCaps . has ( 'draft/chathistory' ) ) {
iSupportArray . push ( 'CHATHISTORY=50' ) ;
}
this . sendMessage ( this . server . name , '005' , numerics [ '005' ] ( this . user . nick , iSupportArray ) , message . tags ) ;
this . sendMessage ( this . server . name , '375' , numerics [ '375' ] ( this . user . nick ) , message . tags ) ;
this . sendMessage ( this . server . name , '372' , numerics [ '372' ] ( this . user . nick , "It's an MOTD" ) , message . tags ) ;
this . sendMessage ( this . server . name , '376' , numerics [ '376' ] ( this . user . nick ) , message . tags ) ;
this . sendMessage ( this . user . nick , 'MODE' , [ this . user . nick , '+i' ] , message . tags ) ;
2021-12-06 12:07:40 -05:00
if ( ! this . user . isSynced ( ) )
2021-12-06 10:23:29 -05:00
this . sendMessage ( this . server . name , 'NOTICE' , [ this . user . nick , 'Please wait for initial sync, this may take a while if you are in many large channels' ] , message . tags ) ;
2021-12-05 18:33:49 -05:00
}
sendMessage ( prefix : string , command : string , params : string [ ] , tags : Map < string , string > ) {
const capTagMapping = new Map ( [
[ 'account' , 'account-tag' ] ,
[ 'label' , 'labeled-response' ] ,
[ 'msgid' , 'message-tags' ] ,
[ 'reflectionircd.chat/delete-message' , 'reflectionircd.chat/delete-message' ] ,
[ 'reflectionircd.chat/edit-message' , 'reflectionircd.chat/edit-message' ] ,
[ 'time' , 'server-time' ] ,
] )
const ourTags : Map < string , string > = new Map ( ) ;
if ( this . enabledCaps . has ( 'server-time' ) && ! tags . has ( 'time' ) )
ourTags . set ( 'time' , new Date ( ) . toISOString ( ) ) ;
tags . forEach ( ( v , k ) = > {
if ( k . startsWith ( '+' ) ) {
if ( this . enabledCaps . has ( 'message-tags' ) ) {
ourTags . set ( k , v ) ;
}
}
else {
const capToCheck = capTagMapping . get ( k ) || '' ;
if ( this . enabledCaps . has ( capToCheck ) ) {
ourTags . set ( k , v ) ;
}
}
} )
const newMsg = new IRCMessage ( ourTags , prefix , command , params ) ;
const msgToSend = newMsg . toString ( ) ;
2021-12-06 18:33:32 -05:00
//console.log(`SENT: ${msgToSend}`);
2021-12-05 18:33:49 -05:00
this . socket . write ( ` ${ msgToSend } \ r \ n ` ) ;
}
closeConnectionWithError ( message : string ) {
this . sendMessage ( this . server . name , 'ERROR' , [ message ] , new Map ( ) ) ;
this . socket . destroy ( ) ;
}
}