diff --git a/src/Client.ts b/src/Client.ts index 5af1944..82bd028 100644 --- a/src/Client.ts +++ b/src/Client.ts @@ -51,27 +51,27 @@ export abstract class Client { }); } - getMatrixUserFromNick(targetNick: string) { + 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"]); + this.sendMessage(this.server.name, "401", [this.user.nick, targetNick, "No such nick"], message.tags); return false; } return target; } - getChannel(channel: string) { + 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"]); + this.sendMessage(this.server.name, "403", [this.user.nick, channel, "No such channel"], message.tags); return false; } return targetChannel; } - checkIfInChannel(channel: Channel) { + 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"]); + this.sendMessage(this.server.name, "442", [this.user.nick, "You're not on that channel"], message.tags); return false; } return true; @@ -79,7 +79,7 @@ export abstract class Client { checkMinParams(message: IRCMessage, neededNumber: number) { if (message.params.length < neededNumber) { - this.sendMessage(this.server.name, "461", [this.user.nick, message.command, "Not enough parameters"]); + this.sendMessage(this.server.name, "461", [this.user.nick, message.command, "Not enough parameters"], message.tags); return false; } return true; @@ -112,6 +112,9 @@ export abstract class Client { case 'AUTHENTICATE': this.doAUTHENTICATE(message); break; + case 'BATCH': + this.doBATCH(message); + break; case 'CAP': this.doCAP(message); break; @@ -151,6 +154,18 @@ export abstract class Client { 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; } } @@ -161,12 +176,12 @@ export abstract class Client { 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"]) + 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}`]); - this.sendMessage(this.server.name, '903', [this.user.nick, "SASL authentication successful"]); + 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; } } @@ -198,11 +213,11 @@ export abstract class Client { if (message.params.length === 2) { this.capVersion = message.params[1]; } - this.sendMessage(this.server.name, "CAP", ["*", "LS", this.getCapString(this.capVersion)]); + 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)]); + this.sendMessage(this.server.name, "CAP", ["*", "LIST", this.getCapString(this.capVersion)], message.tags); break; } case 'REQ': { @@ -214,7 +229,7 @@ export abstract class Client { capsEnabled.push(cap); } }); - this.sendMessage(this.server.name, "CAP", ["*", "ACK", capsEnabled.join(' ')]); + this.sendMessage(this.server.name, "CAP", ["*", "ACK", capsEnabled.join(' ')], message.tags); break; } case 'END': { @@ -231,10 +246,10 @@ export abstract class Client { doDELETEMSG(message: IRCMessage) { if (!this.checkIfRegistered() || !this.checkMinParams(message, 1)) return; - const targetChannel = this.getChannel(message.params[0]); + 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)) return; + if (!this.checkIfInChannel(targetChannel, message)) return; const data = { "reason": (message.params.length === 2) ? message.params[1] : "" } @@ -263,10 +278,10 @@ export abstract class Client { doINVITE(message: IRCMessage) { if (!this.checkIfRegistered() || !this.checkMinParams(message, 2)) return; - const targetUser = this.getMatrixUserFromNick(message.params[0]); - const targetChannel = this.getChannel(message.params[1]); + 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)) 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"]); return; @@ -300,10 +315,10 @@ export abstract class Client { doKICK(message: IRCMessage) { if (!this.checkIfRegistered() || !this.checkMinParams(message, 2)) return; - const targetChannel = this.getChannel(message.params[0]); - const targetUser = this.getMatrixUserFromNick(message.params[1]); + 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)) 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"]); return; @@ -340,16 +355,16 @@ export abstract class Client { 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"]); + 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)) 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}`]); + this.sendMessage(this.server.name, "324", [this.user.nick, targetChannel.name, `+${chanModes}`], message.tags); return; } } @@ -361,9 +376,9 @@ export abstract class Client { doMSG(message: IRCMessage) { if (!this.checkIfRegistered() || !this.checkMinParams(message, 2)) return; - const targetChannel = this.getChannel(message.params[0]); + const targetChannel = this.getChannel(message.params[0], message); if (!this.user || !targetChannel) return; - if (!this.checkIfInChannel(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"], new Map()); return; @@ -419,8 +434,12 @@ export abstract class Client { }); } - sendNAMES(targetChannel: Channel) { + 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); @@ -432,31 +451,40 @@ export abstract class Client { singleNamesList.push(singleName); } else { - this.sendMessage(this.server.name, "353", [this.user.nick, "=", targetChannel.name, `${singleNamesList.join(' ')}`]); + 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(' ')}`]); + 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"]); + 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]); + const targetChannel = this.getChannel(message.params[0], message); if (!this.user || !targetChannel) return; - if (!this.checkIfInChannel(targetChannel)) return; - this.sendNAMES(targetChannel); + 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]); + const targetChannel = this.getChannel(message.params[0], message); if (!this.user || !targetChannel) return; - if (!this.checkIfInChannel(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) { @@ -489,9 +517,9 @@ export abstract class Client { doTAGMSG(message: IRCMessage) { if (!this.checkIfRegistered() || !this.checkMinParams(message, 1)) return; - const targetChannel = this.getChannel(message.params[0]); + const targetChannel = this.getChannel(message.params[0], message); if (!this.user || !targetChannel) return; - if (!this.checkIfInChannel(targetChannel)) return; + if (!this.checkIfInChannel(targetChannel, message)) return; if (message.tags.has("+draft/react") && message.tags.has("+draft/reply")) { const content = { "m.relates_to": { @@ -539,9 +567,9 @@ export abstract class Client { doTOPIC(message: IRCMessage) { if (!this.checkIfRegistered() || !this.checkMinParams(message, 1)) return; - const targetChannel = this.getChannel(message.params[0]); + const targetChannel = this.getChannel(message.params[0], message); if (!this.user || !targetChannel) return; - if (!this.checkIfInChannel(targetChannel)) return; + if (!this.checkIfInChannel(targetChannel, message)) return; if (message.params.length === 1) { this.sendTOPIC(targetChannel); return; @@ -571,9 +599,15 @@ export abstract class Client { doWHO(message: IRCMessage) { if (!this.checkIfRegistered() || !this.checkMinParams(message, 1)) return; - const targetChannel = this.getChannel(message.params[0]); + const targetChannel = this.getChannel(message.params[0], message); if (!this.user || !targetChannel) return; - if (!this.checkIfInChannel(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 = [ @@ -586,9 +620,32 @@ export abstract class Client { `H${opStatus}`, `0 ${matrixUser.realname}` ] - this.sendMessage(this.server.name, '352', userParams); + 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')}`]); } - this.sendMessage(this.server.name, '315', [this.user.nick, targetChannel.name, "End of /WHO"]); } doRegistration(message: IRCMessage) { @@ -624,6 +681,7 @@ export abstract class Client { 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'],