import { Socket } from 'net'; 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 allCaps: Map localNick: string localUsername: string localRealname: string 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", ""], ["invite-notify", ""], ["message-tags", ""], ["sasl", "PLAIN"], ["server-time", ""], ]); this.localNick = 'none'; this.localUsername = 'none'; this.localRealname = 'none'; 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; } } } 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 => { if (cap in this.allCaps) { 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]; const thisIRCUser = this.server.ircUsers.get(mxid) || new IRCUser(this, mxid, accessToken); if (thisIRCUser.isAuthed) { if (!thisIRCUser.verifyCredentials(accessToken)) { this.sendMessage(this.server.name, '904', numerics['904']('*'), message.tags) this.closeConnectionWithError('Invalid authentication') } else { this.user = thisIRCUser; } } } } doRegistration(message: IRCMessage) { if (this.user === null) { this.closeConnectionWithError('Authentication failed'); 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); } sendMessage(prefix: string, command: string, params: string[], tags: Map) { 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 = 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(); console.log(`SENT: ${msgToSend}`); this.socket.write(`${msgToSend}\r\n`); } closeConnectionWithError(message: string) { this.sendMessage(this.server.name, 'ERROR', [message], new Map()); this.socket.destroy(); } }