diff options
Diffstat (limited to '')
-rw-r--r-- | comm/chat/protocols/irc/ircCTCP.sys.mjs | 291 |
1 files changed, 291 insertions, 0 deletions
diff --git a/comm/chat/protocols/irc/ircCTCP.sys.mjs b/comm/chat/protocols/irc/ircCTCP.sys.mjs new file mode 100644 index 0000000000..475f1f8a8d --- /dev/null +++ b/comm/chat/protocols/irc/ircCTCP.sys.mjs @@ -0,0 +1,291 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/** + * This implements the Client-to-Client Protocol (CTCP), a subprotocol of IRC. + * REVISED AND UPDATED CTCP SPECIFICATION + * http://www.alien.net.au/irc/ctcp.txt + */ + +import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs"; +import { l10nHelper } from "resource:///modules/imXPCOMUtils.sys.mjs"; +import { ircHandlerPriorities } from "resource:///modules/ircHandlerPriorities.sys.mjs"; +import { displayMessage } from "resource:///modules/ircUtils.sys.mjs"; + +const lazy = {}; +XPCOMUtils.defineLazyGetter(lazy, "_", () => + l10nHelper("chrome://chat/locale/irc.properties") +); + +// Split into a CTCP message which is a single command and a single parameter: +// <command> " " <parameter> +// The high level dequote is to unescape \001 in the message content. +export function CTCPMessage(aMessage, aRawCTCPMessage) { + let message = Object.assign({}, aMessage); + message.ctcp = {}; + message.ctcp.rawMessage = aRawCTCPMessage; + + // High/CTCP level dequote: replace the quote char \134 followed by a or \134 + // with \001 or \134, respectively. Any other character after \134 is replaced + // with itself. + let dequotedCTCPMessage = message.ctcp.rawMessage.replace( + /\\(.|$)/g, + aStr => { + if (aStr[1]) { + return aStr[1] == "a" ? "\x01" : aStr[1]; + } + return ""; + } + ); + + let separator = dequotedCTCPMessage.indexOf(" "); + // If there's no space, then only a command is given. + // Do not capitalize the command, case sensitive + if (separator == -1) { + message.ctcp.command = dequotedCTCPMessage; + message.ctcp.param = ""; + } else { + message.ctcp.command = dequotedCTCPMessage.slice(0, separator); + message.ctcp.param = dequotedCTCPMessage.slice(separator + 1); + } + return message; +} + +// This is the CTCP handler for IRC protocol, it will call each CTCP handler. +export var ircCTCP = { + name: "CTCP", + // Slightly above default RFC 2812 priority. + priority: ircHandlerPriorities.HIGH_PRIORITY, + isEnabled: () => true, + + // CTCP uses only PRIVMSG and NOTICE commands. + commands: { + PRIVMSG: ctcpHandleMessage, + NOTICE: ctcpHandleMessage, + }, +}; + +// Parse the message and call all CTCP handlers on the message. +function ctcpHandleMessage(message, ircHandlers) { + // If there are no CTCP handlers, then don't parse the CTCP message. + if (!ircHandlers.hasCTCPHandlers) { + return false; + } + + // The raw CTCP message is in the last parameter of the IRC message. + let rawCTCPParam = message.params.slice(-1)[0]; + + // Split the raw message into the multiple CTCP messages and pull out the + // command and parameters. + let ctcpMessages = []; + let otherMessage = rawCTCPParam.replace( + // eslint-disable-next-line no-control-regex + /\x01([^\x01]*)\x01/g, + function (aMatch, aMsg) { + if (aMsg) { + ctcpMessages.push(new CTCPMessage(message, aMsg)); + } + return ""; + } + ); + + // If no CTCP messages were found, return false. + if (!ctcpMessages.length) { + return false; + } + + // If there's some message left, send it back through the IRC handlers after + // stripping out the CTCP information. I highly doubt this will ever happen, + // but just in case. ;) + if (otherMessage) { + message.params.pop(); + message.params.push(otherMessage); + ircHandlers.handleMessage(this, message); + } + + // Loop over each raw CTCP message. + for (let message of ctcpMessages) { + if (!ircHandlers.handleCTCPMessage(this, message)) { + this.WARN( + "Unhandled CTCP message: " + + message.ctcp.rawMessage + + "\nin IRC message: " + + message.rawMessage + ); + // For unhandled CTCP message, respond with a NOTICE ERRMSG that echoes + // back the original command. + this.sendCTCPMessage(message.origin, true, "ERRMSG", [ + message.ctcp.rawMessage, + ":Unhandled CTCP command", + ]); + } + } + + // We have handled this message as much as we can. + return true; +} + +// This is the the basic CTCP protocol. +export var ctcpBase = { + // Parameters + name: "CTCP", + priority: ircHandlerPriorities.DEFAULT_PRIORITY, + isEnabled: () => true, + + // These represent CTCP commands. + commands: { + ACTION(aMessage) { + // ACTION <text> + // Display message in conversation + return displayMessage( + this, + aMessage, + { action: true }, + aMessage.ctcp.param + ); + }, + + // Used when an error needs to be replied with. + ERRMSG(aMessage) { + this.WARN( + aMessage.origin + + " failed to handle CTCP message: " + + aMessage.ctcp.param + ); + return true; + }, + + // This is commented out since CLIENTINFO automatically returns the + // supported CTCP parameters and this is not supported. + + // Returns the user's full name, and idle time. + // "FINGER": function(aMessage) { return false; }, + + // Dynamic master index of what a client knows. + CLIENTINFO(message, ircHandlers) { + if (message.command == "PRIVMSG") { + // Received a CLIENTINFO request, respond with the support CTCP + // messages. + let info = new Set(); + for (let handler of ircHandlers._ctcpHandlers) { + for (let command in handler.commands) { + info.add(command); + } + } + + let supportedCtcp = [...info].join(" "); + this.LOG( + "Reporting support for the following CTCP messages: " + supportedCtcp + ); + this.sendCTCPMessage(message.origin, true, "CLIENTINFO", supportedCtcp); + } else { + // Received a CLIENTINFO response, store the information for future + // use. + let info = message.ctcp.param.split(" "); + this.setWhois(message.origin, { clientInfo: info }); + } + return true; + }, + + // Used to measure the delay of the IRC network between clients. + PING(aMessage) { + // PING timestamp + if (aMessage.command == "PRIVMSG") { + // Received PING request, send PING response. + this.LOG( + "Received PING request from " + + aMessage.origin + + '. Sending PING response: "' + + aMessage.ctcp.param + + '".' + ); + this.sendCTCPMessage( + aMessage.origin, + true, + "PING", + aMessage.ctcp.param + ); + return true; + } + return this.handlePingReply(aMessage.origin, aMessage.ctcp.param); + }, + + // These are commented out since CLIENTINFO automatically returns the + // supported CTCP parameters and this is not supported. + + // An encryption protocol between clients without any known reference. + // "SED": function(aMessage) { return false; }, + + // Where to obtain a copy of a client. + // "SOURCE": function(aMessage) { return false; }, + + // Gets the local date and time from other clients. + TIME(aMessage) { + if (aMessage.command == "PRIVMSG") { + // TIME + // Received a TIME request, send a human readable response. + let now = new Date().toString(); + this.LOG( + "Received TIME request from " + + aMessage.origin + + '. Sending TIME response: "' + + now + + '".' + ); + this.sendCTCPMessage(aMessage.origin, true, "TIME", ":" + now); + } else { + // TIME :<human-readable-time-string> + // Received a TIME reply, display it. + // Remove the : prefix, if it exists and display the result. + let time = aMessage.ctcp.param.slice(aMessage.ctcp.param[0] == ":"); + this.getConversation(aMessage.origin).writeMessage( + aMessage.origin, + lazy._("ctcp.time", aMessage.origin, time), + { system: true, tags: aMessage.tags } + ); + } + return true; + }, + + // This is commented out since CLIENTINFO automatically returns the + // supported CTCP parameters and this is not supported. + + // A string set by the user (never the client coder) + // "USERINFO": function(aMessage) { return false; }, + + // The version and type of the client. + VERSION(aMessage) { + if (aMessage.command == "PRIVMSG") { + // VERSION + // Received VERSION request, send VERSION response. + let version = Services.appinfo.name + " " + Services.appinfo.version; + this.LOG( + "Received VERSION request from " + + aMessage.origin + + '. Sending VERSION response: "' + + version + + '".' + ); + this.sendCTCPMessage(aMessage.origin, true, "VERSION", version); + } else if (aMessage.command == "NOTICE" && aMessage.ctcp.param.length) { + // VERSION #:#:# + // Received VERSION response, display to the user. + let response = lazy._( + "ctcp.version", + aMessage.origin, + aMessage.ctcp.param + ); + this.getConversation(aMessage.origin).writeMessage( + aMessage.origin, + response, + { + system: true, + tags: aMessage.tags, + } + ); + } + return true; + }, + }, +}; |