import axios from 'axios'; import { Socket } from 'net'; import { Channel } from './Channel.js'; 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 deviceId: 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", ""], ["draft/channel-rename", ""], ["echo-message", ""], ["draft/event-playback", ""], ["extended-join", ""], ["invite-notify", ""], ["message-tags", ""], ["sasl", "PLAIN"], ["server-time", ""], ]); this.localNick = 'none'; this.localUsername = 'none'; this.localRealname = 'none'; this.deviceId = ""; 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; } case 'INVITE': { const targetUser = this.server.getOrCreateMatrixUser(message.params[0]) const targetChannel = this.server.ircChannels.get(message.params[1]); const reason = (message.params.length === 3) ? message.params[2] : ""; if (this.user && targetChannel && targetChannel.ircUsers.get(this.user.nick)) this.user.inviteMatrixUser(this, targetChannel, targetUser, reason, message.tags); break; } case 'KICK': { const targetChannel = this.server.ircChannels.get(message.params[0]); const targetMxid = this.server.nickToMxid.get(message.params[1]); if (!targetMxid) return; console.log(targetMxid); const reason = (message.params.length === 3) ? message.params[2] : ""; if (this.user && targetChannel && targetChannel.ircUsers.get(this.user.nick)) this.user.kickMatrixUser(this, targetChannel, targetMxid, reason, message.tags); break; } case 'MODE': { const targetChannel = this.server.ircChannels.get(message.params[0]); if (this.user && targetChannel && targetChannel.ircUsers.get(this.user.nick)) targetChannel.sendMode(this, message.tags); break; } case 'NAMES': { const targetChannel = this.server.ircChannels.get(message.params[0]); if (this.user && targetChannel && targetChannel.ircUsers.get(this.user.nick)) targetChannel.sendNames(this, message.tags); break; } case 'NOTICE': { if (this.user) { this.user.sendMessageToMatrix(message, this); } break; } case 'PART': { const targetChannel = this.server.ircChannels.get(message.params[0]); const reason = (message.params.length === 2) ? message.params[1] : ""; if (this.user && targetChannel && targetChannel.ircUsers.get(this.user.nick)) this.user.partMatrixRoom(this, targetChannel, reason, message.tags); break; } case 'PING': { this.sendMessage(this.server.name, "PONG", message.params, message.tags); break; } case 'PRIVMSG': { if (this.user) { this.user.sendMessageToMatrix(message, this); } break; } case 'TAGMSG': { if (this.user) { this.user.sendTagToMatrix(message, this); } break; } case 'TOPIC': { const targetChannel = this.server.ircChannels.get(message.params[0]); if (!this.user || !targetChannel || !targetChannel.ircUsers.get(this.user.nick)) break; if (message.params.length === 1) { targetChannel.sendTopic(this, message.tags); break; } const topic = message.params[1]; this.user.changeRoomTopic(this, targetChannel, topic, message.tags); break; } case 'WHO': { const targetChannel = this.server.ircChannels.get(message.params[0]); if (this.user && targetChannel && targetChannel.ircUsers.get(this.user.nick)) targetChannel.sendWho(this, message.tags); 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 (this.allCaps.has(cap)) { 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 accessTokenAndServer = authArray[2]; const sepIndex = accessTokenAndServer.indexOf(":"); const accessToken = accessTokenAndServer.slice(0, sepIndex); const homeserver = accessTokenAndServer.slice(sepIndex+1); console.log(accessToken, homeserver); const thisIRCUser = this.server.getOrCreateIRCUser(mxid, accessToken, homeserver); thisIRCUser.getVerification().then((response) => { if (response.status === 401 || response.status === 403) { this.sendMessage(this.server.name, '904', numerics['904']('*'), message.tags) this.closeConnectionWithError('Invalid authentication') } 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') } 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 { this.user = thisIRCUser; 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()); if (this.user.isSynced()) { this.user.addClient(this, message.tags); } else { axios.get(`https://${this.user.homeserver}/_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']) { const joinedRooms: Set = new Set(); for (const roomId of Object.keys(rooms.join)) { const targetChannel = this.server.getOrCreateIRCChannel(roomId); joinedRooms.add(targetChannel); rooms.join[roomId].state.events.forEach((nextEvent: any) => targetChannel.routeMatrixEvent(nextEvent)); } joinedRooms.forEach(c => {if (this.user !== null) this.user.channels.add(c)}); } if (this.user === null) return; this.user.nextBatch = data.next_batch; this.sendMessage(this.server.name, 'NOTICE', [this.user.nick, 'You are now synced to the network!'], message.tags); this.user.addClient(this, message.tags); }).catch(function (error) { if (error.response) { console.log(error.response.data); } else if (error.request) { console.log(error.request); } else { console.log('Error', error.message); console.log(error.config); } }) } } }) } } doRegistration(message: IRCMessage) { if (this.user === null) { this.closeConnectionWithError("You must use SASL to connect to this server"); 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); if (!this.user.isSynced()) 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); } 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(); } }