summaryrefslogtreecommitdiffstats
path: root/comm/chat/protocols/irc/ircCTCP.sys.mjs
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--comm/chat/protocols/irc/ircCTCP.sys.mjs291
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;
+ },
+ },
+};