import { Axios } from 'axios'; import { randomUUID } from 'crypto'; import { Socket } from 'net'; import { Batch } from './Batch.js'; import { Channel } from './Channel.js'; import { MatrixUser } from './MatrixUser.js'; import { IRCMessage, parseIRCMessage } from './Message.js'; import { Server } from './Server.js'; export class Client { capVersion: string enabledCaps: Map allCaps: Map user: MatrixUser isRegistered: boolean apiCall: Axios batchesInProgress: Map constructor(public server: Server) { this.capVersion = '301'; this.enabledCaps = new Map(); this.allCaps = new Map([ ["account-tag", ""], ["batch", ""], ["draft/channel-rename", ""], ["draft/multiline", "max-bytes=4096,max-lines=20"], ["echo-message", ""], ["reflectionircd.chat/edit-message", ""], ["reflectionircd.chat/extended-invite", ""], ["extended-join", ""], ["invite-notify", ""], ["labeled-response", ""], ["message-tags", ""], ["sasl", "PLAIN"], ["server-time", ""], ]); this.user = this.server.ourMatrixUser; this.isRegistered = false; this.apiCall = this.server.apiCall; this.batchesInProgress = new Map(); } checkIfRegistered() { return this.isRegistered; } 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); }); } getMatrixUserFromNick(targetNick: string, message: IRCMessage) { const target = this.server.nickToMatrixUser.get(targetNick); if (!target) { this.sendMessage(this.server.name, "401", [this.user.nick, targetNick, "No such nick"], message.tags); return false; } return target; } getChannel(channel: string, message: IRCMessage) { const targetChannel = this.server.channels.get(channel); if (!targetChannel) { this.sendMessage(this.server.name, "403", [this.user.nick, channel, "No such channel"], message.tags); return false; } return targetChannel; } checkIfInChannel(channel: Channel, message: IRCMessage) { if (!this.server.channels.get(channel.name)) { this.sendMessage(this.server.name, "442", [this.user.nick, "You're not on that channel"], message.tags); return false; } return true; } checkMinParams(message: IRCMessage, neededNumber: number) { if (message.params.length < neededNumber) { this.sendMessage(this.server.name, "461", [this.user.nick, message.command, "Not enough parameters"], message.tags); return false; } return true; } getCapString(capVersion: string) { let capArray: string[] = []; 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(' '); } routeMessage(data: string) { const message = parseIRCMessage(data); const maybeBatchRef = message.tags.get('batch'); if (maybeBatchRef) { const maybeBatch = this.batchesInProgress.get(maybeBatchRef); if (maybeBatch) { maybeBatch.messages.add(message); } return; } switch (message.command.toUpperCase()) { case 'AUTHENTICATE': this.doAUTHENTICATE(message); break; case 'AWAY': this.doAWAY(message); break; case 'BATCH': this.doBATCH(message); break; case 'CAP': this.doCAP(message); break; case 'DELETEMSG': this.doDELETEMSG(message); break; case 'INVITE': this.doINVITE(message); break; case 'KICK': this.doKICK(message); break; case 'MODE': this.doMODE(message); break; case 'NAMES': this.doNAMES(message); break; case 'NOTICE': this.doMSG(message); break; case 'PART': this.doPART(message); break; case 'PING': this.sendMessage(this.server.name, "PONG", message.params, message.tags); break; case 'PRIVMSG': this.doMSG(message); break; case 'TAGMSG': this.doTAGMSG(message); break; case 'TOPIC': this.doTOPIC(message); break; case 'WHO': this.doWHO(message); break; case 'WHOIS': this.doWHOIS(message); break; case 'JOIN': case 'NICK': case 'USER': // Exempting these from sending a 421, otherwise it will get annoying break; default: this.sendMessage(this.server.name, "421", [message.command, 'Unknown command'], message.tags); console.log(`unknown command ${message.command}`); break; } } doAUTHENTICATE(message: IRCMessage) { if (message.params[0] === "PLAIN") { this.sendMessage("", "AUTHENTICATE", ["+"]); } 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', ['*', "SASL Authentication failed"], message.tags) this.closeConnectionWithError('Invalid authentication') } if (authArray[2] === this.server.config.SASLPassword) { this.sendMessage(this.server.name, '900', [this.user.nick, this.server.getMask(), this.user.accountName, `You are now logged in as ${this.user.nick}`], message.tags); this.sendMessage(this.server.name, '903', [this.user.nick, "SASL authentication successful"], message.tags); this.isRegistered = true; } } } doAWAY(message: IRCMessage) { const data = { presence: 'online', status_msg: '' } if (message.params.length === 1) { data.presence = 'unavailable'; data.status_msg = message.params[0]; } this.apiCall.put(`/presence/${this.user.mxid}/status`, data).then(r => { // Returning the IRC numerics here because most servers have presence disabled anyways if (data.presence === 'online') { this.sendMessage(this.server.name, "305", [this.user.nick, "You are no longer marked as being away"], message.tags); } else { this.sendMessage(this.server.name, "306", [this.user.nick, "You have been marked as being away"], 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); } }) } doBATCH(message: IRCMessage) { const referenceTag = message.params[0].substring(1); if (message.params[0].startsWith('+')) { this.batchesInProgress.set(referenceTag, new Batch(referenceTag, message)) } else if (message.params[0].startsWith('-')) { const readyBatch = this.batchesInProgress.get(referenceTag); if (readyBatch) { switch (readyBatch.batchType) { case 'draft/multiline': case 'multiline': this.doMultiline(readyBatch); break; default: break; } } this.batchesInProgress.delete(referenceTag); } } 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.isRegistered) { this.doRegistration(message); } else { this.closeConnectionWithError("You must use SASL to connect to this server"); } } } } doDELETEMSG(message: IRCMessage) { if (!this.checkIfRegistered() || !this.checkMinParams(message, 1)) return; const targetChannel = this.getChannel(message.params[0], message); const eventId = message.tags.get("reflectionircd.chat/delete-message"); if (!this.user || !targetChannel || !eventId) return; if (!this.checkIfInChannel(targetChannel, message)) return; const data = { "reason": (message.params.length === 2) ? message.params[1] : "" } const newTxnid = randomUUID(); this.apiCall.put(`/rooms/${targetChannel.roomId}/redact/${eventId}/${newTxnid}`, data).then(r => { const maybeEventID = r.data["event_id"]; if (maybeEventID) { this.server.eventIdStore.set(maybeEventID, this); const maybeLabel = message.tags.get("label") || ""; if (maybeLabel !== "") { this.server.eventIDToLabel.set(maybeEventID, maybeLabel) } } }).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); } }) } doINVITE(message: IRCMessage) { if (!this.checkIfRegistered() || !this.checkMinParams(message, 2)) return; const targetUser = this.getMatrixUserFromNick(message.params[0], message); const targetChannel = this.getChannel(message.params[1], message); if (!this.user || !targetUser || !targetChannel) return; if (!this.checkIfInChannel(targetChannel, message)) return; if (targetChannel.matrixUsers.has(targetUser.nick)) { this.sendMessage(this.server.name, "443", [this.user.nick, targetUser.nick, "is already on channel"], message.tags); return; } const reason = (message.params.length === 3) ? message.params[2] : ""; const data = { "reason": reason, "user_id": targetUser.mxid } this.apiCall.post(`/rooms/${targetChannel.roomId}/invite`, data).then(r => { const maybeEventID = r.data["event_id"]; if (maybeEventID) { this.server.eventIdStore.set(maybeEventID, this); const maybeLabel = message.tags.get("label") || ""; if (maybeLabel !== "") { this.server.eventIDToLabel.set(maybeEventID, maybeLabel) } } }).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); } }) } doKICK(message: IRCMessage) { if (!this.checkIfRegistered() || !this.checkMinParams(message, 2)) return; const targetChannel = this.getChannel(message.params[0], message); const targetUser = this.getMatrixUserFromNick(message.params[1], message); if (!this.user || !targetUser || !targetChannel) return; if (!this.checkIfInChannel(targetChannel, message)) return; if (!targetChannel.matrixUsers.has(targetUser.nick)) { this.sendMessage( this.server.name, "441", [this.user.nick, targetUser.nick, targetChannel.name, "They aren't on that channel"], message.tags ); return; } const reason = (message.params.length === 3) ? message.params[2] : ""; const data = { "reason": reason, "user_id": targetUser.mxid } this.apiCall.post(`/rooms/${targetChannel.roomId}/kick`, data).then(r => { const maybeEventID = r.data["event_id"]; if (maybeEventID) { this.server.eventIdStore.set(maybeEventID, this); const maybeLabel = message.tags.get("label") || ""; if (maybeLabel !== "") { this.server.eventIDToLabel.set(maybeEventID, maybeLabel) } } }).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); } }) } doMODE(message: IRCMessage) { if (!this.checkIfRegistered() || !this.checkMinParams(message, 1) || !this.user) return; const targetChannel = this.server.channels.get(message.params[0]); if (!targetChannel) { if (message.params[0] !== this.user.nick) { this.sendMessage(this.server.name, "502", [this.user.nick, "Can't view mode for other users"], message.tags); return; } this.sendMessage(this.server.name, "221", [this.user.nick, "+i"]); return; } if (!this.checkIfInChannel(targetChannel, message)) return; if (message.params.length === 1) { const chanModes = [...targetChannel.channelModes.keys()].sort().join(''); this.sendMessage(this.server.name, "324", [this.user.nick, targetChannel.name, `+${chanModes}`], message.tags); return; } } doMultiline(batch: Batch) { if (batch.messages.size === 0) { return; } let fullMessage = ''; const firstMessage = [...batch.messages][0]; const msgType = (firstMessage.command === 'NOTICE') ? 'm.notice' : 'm.text'; const targetChannel = this.getChannel(firstMessage.params[0], firstMessage); if (!this.user || !targetChannel) return; if (!this.checkIfInChannel(targetChannel, firstMessage)) return; if (targetChannel.roomType === "m.space") { this.sendMessage(this.server.name, "NOTICE", [targetChannel.name, "Sending messages to spaces is not allowed"], batch.openingBatch.tags); return; } for (const msg of batch.messages) { console.log(firstMessage.params[1], msg.params[1]); const separator = (msg.tags.has('draft/multiline-concat') || firstMessage.params[1] === msg.params[1]) ? '' : '\n'; fullMessage = `${fullMessage}${separator}${msg.params[1]}`; console.log(fullMessage); } const highlightFilteredMsg = fullMessage.split(" ").map(w => { if (!w.startsWith('@')) return w; const endingCharMatch = w.match(/[,:]$/); const endingChar = (endingCharMatch) ? endingCharMatch[0] : ""; const endingCharIndex = (endingCharMatch) ? endingCharMatch.index : 0; const nickToSearch = (endingCharIndex === 0) ? w.substring(1) : w.substring(1, endingCharIndex); const maybeHighlight = targetChannel.matrixUsers.get(nickToSearch); return (maybeHighlight) ? `${maybeHighlight.mxid}${endingChar}` : w; }) const content = { "body": highlightFilteredMsg.join(" "), "msgtype": msgType, "m.relates_to": {} } if (batch.openingBatch.tags.has("+draft/reply")) { content["m.relates_to"] = { "m.in_reply_to": { "event_id": batch.openingBatch.tags.get("+draft/reply") } } } const newTxnid = randomUUID(); this.apiCall.put(`/rooms/${targetChannel.roomId}/send/m.room.message/${newTxnid}`, content).then(r => { const maybeEventID = r.data["event_id"]; if (maybeEventID) { this.server.eventIdStore.set(maybeEventID, this); const maybeLabel = batch.openingBatch.tags.get("label") || ""; if (maybeLabel !== "") { this.server.eventIDToLabel.set(maybeEventID, maybeLabel) } } }).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); } }); } doMSG(message: IRCMessage) { if (!this.checkIfRegistered() || !this.checkMinParams(message, 2)) return; const targetChannel = this.getChannel(message.params[0], message); if (!this.user || !targetChannel) return; if (!this.checkIfInChannel(targetChannel, message)) return; if (targetChannel.roomType === "m.space") { this.sendMessage(this.server.name, "NOTICE", [targetChannel.name, "Sending messages to spaces is not allowed"], message.tags); return; } let msgtype = 'm.text'; let msgbody = message.params[1]; if (message.command === 'NOTICE') msgtype = 'm.notice'; else if (message.params[1].startsWith('\x01')) { msgtype = 'm.emote'; msgbody = msgbody.replace(/\x01(ACTION\s)?/gi, ''); } const highlightFilteredMsg = msgbody.split(" ").map(w => { if (!w.startsWith('@')) return w; const endingCharMatch = w.match(/[,:]$/); const endingChar = (endingCharMatch) ? endingCharMatch[0] : ""; const endingCharIndex = (endingCharMatch) ? endingCharMatch.index : 0; const nickToSearch = (endingCharIndex === 0) ? w.substring(1) : w.substring(1, endingCharIndex); const maybeHighlight = targetChannel.matrixUsers.get(nickToSearch); return (maybeHighlight) ? `${maybeHighlight.mxid}${endingChar}` : w; }) const content = { "body": highlightFilteredMsg.join(" "), "msgtype": msgtype, "m.relates_to": {} } if (message.tags.has("+draft/reply")) { content["m.relates_to"] = { "m.in_reply_to": { "event_id": message.tags.get("+draft/reply") } } } const newTxnid = randomUUID(); this.apiCall.put(`/rooms/${targetChannel.roomId}/send/m.room.message/${newTxnid}`, content).then(r => { const maybeEventID = r.data["event_id"]; if (maybeEventID) { this.server.eventIdStore.set(maybeEventID, this); const maybeLabel = message.tags.get("label") || ""; if (maybeLabel !== "") { this.server.eventIDToLabel.set(maybeEventID, maybeLabel) } } }).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); } }); } sendNAMES(targetChannel: Channel, batchLabel: string = "") { if (!this.user) return; const newTag = new Map(); if (batchLabel) { newTag.set('batch', batchLabel); } let namesList: string[] = []; for (const matrixUser of targetChannel.matrixUsers.values()) { const opStatus = targetChannel.getNickPowerLevelMapping(matrixUser.nick); namesList.push(`${opStatus}${matrixUser.nick}`); } let singleNamesList: string[] = [] namesList.forEach((singleName, index) => { if (index === 0 || index % 20 !== 0) { singleNamesList.push(singleName); } else { this.sendMessage(this.server.name, "353", [this.user.nick, "=", targetChannel.name, `${singleNamesList.join(' ')}`], newTag); singleNamesList = []; } }) if (singleNamesList.length !== 0) { this.sendMessage(this.server.name, "353", [this.user.nick, "=", targetChannel.name, `${singleNamesList.join(' ')}`], newTag); } this.sendMessage(this.server.name, "366", [this.user.nick, targetChannel.name, "End of /NAMES list"], newTag); } doNAMES(message: IRCMessage) { if (!this.checkIfRegistered() || !this.checkMinParams(message, 1)) return; const targetChannel = this.getChannel(message.params[0], message); if (!this.user || !targetChannel) return; if (!this.checkIfInChannel(targetChannel, message)) return; const messageLabel = message.tags.get('label') || ""; let batchLabel = ""; if (messageLabel) { batchLabel = Math.random().toString(36).substring(2,7); this.sendMessage(this.server.name, 'BATCH', [`+${batchLabel}`, 'labeled-response'], message.tags); } this.sendNAMES(targetChannel, batchLabel); if (messageLabel) { this.sendMessage(this.server.name, 'BATCH', [`-${batchLabel}`]); } } doPART(message: IRCMessage) { if (!this.checkIfRegistered() || !this.checkMinParams(message, 1)) return; const targetChannel = this.getChannel(message.params[0], message); if (!this.user || !targetChannel) return; if (!this.checkIfInChannel(targetChannel, message)) return; const reason = (message.params.length === 2) ? message.params[1] : ""; this.apiCall.post(`/rooms/${targetChannel.roomId}/leave`, {"reason": reason}).then(response => { if (response.status === 200) { //@ts-ignore this.user.getClients().forEach(c => { //@ts-ignore c.sendMessage(this.user.getMask(), "PART", [targetChannel.name, reason], message.tags); }) //@ts-ignore this.user.channels.delete(targetChannel.name); //@ts-ignore this.user.roomIdToChannel.delete(targetChannel.roomId); } else { //@ts-ignore this.sendMessage(this.server.name, "NOTICE", [this.user.nick, JSON.stringify(response.data)], 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); } }) } doTAGMSG(message: IRCMessage) { if (!this.checkIfRegistered() || !this.checkMinParams(message, 1)) return; const targetChannel = this.getChannel(message.params[0], message); if (!this.user || !targetChannel) return; if (!this.checkIfInChannel(targetChannel, message)) return; if (message.tags.has("+draft/react") && message.tags.has("+draft/reply")) { const content = { "m.relates_to": { "event_id": message.tags.get("+draft/reply"), "key": message.tags.get("+draft/react"), "rel_type": "m.annotation" } } const newTxnid = randomUUID(); this.apiCall.put(`/rooms/${targetChannel.roomId}/send/m.reaction/${newTxnid}`, content).then(r => { const maybeEventID = r.data["event_id"]; if (maybeEventID) { this.server.eventIdStore.set(maybeEventID, this); const maybeLabel = message.tags.get("label") || ""; if (maybeLabel !== "") { this.server.eventIDToLabel.set(maybeEventID, maybeLabel) } } }).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); } }); } if (message.tags.has("+typing")) { const data = { "typing": false } if (message.tags.get("+typing") === "active") { data.typing = true; } this.apiCall.put(`/rooms/${targetChannel.roomId}/typing/${this.user.mxid}`, data).then(r => { // No response body for successful request }).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); } }); } } sendTOPIC(targetChannel: Channel, messageTags: Map) { if (!this.user) return; const topicText = targetChannel.topic.get('text') || ''; const topicSetter = targetChannel.topic.get('setter') || 'matrix'; const topicTimestamp = targetChannel.topic.get('timestamp') || '0'; if (topicText === '') { this.sendMessage(this.server.name, '331', [this.user.nick, targetChannel.name, 'No topic is set'], messageTags); return; } const messageLabel = messageTags.get('label') || ""; let batchLabel = ""; if (messageLabel) { batchLabel = Math.random().toString(36).substring(2,7); this.sendMessage(this.server.name, 'BATCH', [`+${batchLabel}`, 'labeled-response'], messageTags); } const newTag = new Map(); if (batchLabel) { newTag.set('batch', batchLabel); } this.sendMessage(this.server.name, '332', [this.user.nick, targetChannel.name, topicText], newTag); this.sendMessage(this.server.name, '333', [this.user.nick, targetChannel.name, topicSetter, topicTimestamp], newTag); if (messageLabel) { this.sendMessage(this.server.name, 'BATCH', [`-${batchLabel}`]); } } doTOPIC(message: IRCMessage) { if (!this.checkIfRegistered() || !this.checkMinParams(message, 1)) return; const targetChannel = this.getChannel(message.params[0], message); if (!this.user || !targetChannel) return; if (!this.checkIfInChannel(targetChannel, message)) return; if (message.params.length === 1) { this.sendTOPIC(targetChannel, message.tags); return; } const topic = message.params[1]; this.apiCall.put(`/rooms/${targetChannel.roomId}/state/m.room.topic`, {"topic": topic}).then(r => { const maybeEventID = r.data["event_id"]; if (maybeEventID) { this.server.eventIdStore.set(maybeEventID, this); const maybeLabel = message.tags.get("label") || ""; if (maybeLabel !== "") { this.server.eventIDToLabel.set(maybeEventID, maybeLabel) } } }).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); } }) } doWHO(message: IRCMessage) { if (!this.checkIfRegistered() || !this.checkMinParams(message, 1)) return; const targetChannel = this.getChannel(message.params[0], message); if (!this.user || !targetChannel) return; if (!this.checkIfInChannel(targetChannel, message)) return; const newTags = new Map(); if (message.tags.get('label')) { const batchLabel = Math.random().toString(36).substring(2,7); this.sendMessage(this.server.name, 'BATCH', [`+${batchLabel}`, 'labeled-response'], message.tags); newTags.set('batch', batchLabel); } for (const matrixUser of targetChannel.matrixUsers.values()) { const opStatus = targetChannel.getNickPowerLevelMapping(matrixUser.nick); const userParams = [ this.user.nick, targetChannel.name, matrixUser.ident, matrixUser.hostname, this.server.name, matrixUser.nick, `H${opStatus}`, `0 ${matrixUser.realname}` ] this.sendMessage(this.server.name, '352', userParams, newTags); } this.sendMessage(this.server.name, '315', [this.user.nick, targetChannel.name, "End of /WHO"], newTags); if (message.tags.get('label')) { this.sendMessage(this.server.name, 'BATCH', [`-${newTags.get('batch')}`]); } } doWHOIS(message: IRCMessage) { if (!this.checkIfRegistered() || !this.checkMinParams(message, 1)) return; const targetUser = this.getMatrixUserFromNick(message.params[0], message); if (!targetUser) return; const tNick = targetUser.nick; const newTags = new Map(); if (message.tags.get('label')) { const batchLabel = Math.random().toString(36).substring(2,7); this.sendMessage(this.server.name, 'BATCH', [`+${batchLabel}`, 'labeled-response'], message.tags); newTags.set('batch', batchLabel); } this.sendMessage(this.server.name, '311', [this.user.nick, tNick, targetUser.ident, targetUser.hostname, '*', targetUser.realname], newTags); this.sendMessage(this.server.name, '330', [this.user.nick, tNick, targetUser.accountName, 'is logged in as'], newTags); this.sendMessage(this.server.name, '318', [this.user.nick, tNick, "End of /WHOIS list"], newTags); if (message.tags.get('label')) { this.sendMessage(this.server.name, 'BATCH', [`-${newTags.get('batch')}`]); } } 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', [this.user.nick, `Welcome to the ${this.server.name} network, ${this.user.nick}`]) this.sendMessage(this.server.name, '002', [this.user.nick, `Your host is ${this.server.name}, running version 0.1.0`]); this.sendMessage(this.server.name, '003', [this.user.nick, `This server was created yesterday`]); this.sendMessage(this.server.name, '004', [this.user.nick, this.server.name, '0.1.0', 'i', 'Shnouv']); const iSupportArray = [ 'CASEMAPPING=ascii', 'CHANMODES=,,,Snu', 'CHANTYPES=#&!', 'MAXTARGETS=1', 'MODES=1', 'PREFIX=(ohv)@%+', ] if (this.enabledCaps.has('draft/chathistory')) { iSupportArray.push('CHATHISTORY=50'); } this.sendMessage(this.server.name, '005', [this.user.nick, ...iSupportArray, 'are supported by this server']); this.sendMessage(this.server.name, '375', [this.user.nick, "- Start of MOTD"]); this.sendMessage(this.server.name, '372', [this.user.nick, "It's an MOTD"]); this.sendMessage(this.server.name, '376', [this.user.nick, "- End of MOTD"]); this.sendMessage(this.user.nick, 'MODE', [this.user.nick, '+i']); this.server.addClient(this); } sendMessage(prefix: string, command: string, params: string[], tags: Map = new Map()) { const capTagMapping = new Map([ ['account', 'account-tag'], ['batch', 'batch'], ['label', 'labeled-response'], ['msgid', 'message-tags'], ['reflectionircd.chat/delete-message', 'reflectionircd.chat/edit-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(); this.writeMessage(`${msgToSend}\r\n`); } writeMessage(message: string): void {}; closeConnectionWithError(message: string) { this.sendMessage(this.server.name, 'ERROR', [message], new Map()); this.closeConnection(); } closeConnection(): void {}; }