summaryrefslogtreecommitdiffstats
path: root/comm/chat/protocols/irc/ircUtils.sys.mjs
diff options
context:
space:
mode:
Diffstat (limited to 'comm/chat/protocols/irc/ircUtils.sys.mjs')
-rw-r--r--comm/chat/protocols/irc/ircUtils.sys.mjs303
1 files changed, 303 insertions, 0 deletions
diff --git a/comm/chat/protocols/irc/ircUtils.sys.mjs b/comm/chat/protocols/irc/ircUtils.sys.mjs
new file mode 100644
index 0000000000..190ed8f830
--- /dev/null
+++ b/comm/chat/protocols/irc/ircUtils.sys.mjs
@@ -0,0 +1,303 @@
+/* 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/. */
+
+import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
+import { l10nHelper } from "resource:///modules/imXPCOMUtils.sys.mjs";
+
+const lazy = {};
+XPCOMUtils.defineLazyGetter(lazy, "_", () =>
+ l10nHelper("chrome://chat/locale/irc.properties")
+);
+
+XPCOMUtils.defineLazyGetter(lazy, "TXTToHTML", function () {
+ let cs = Cc["@mozilla.org/txttohtmlconv;1"].getService(Ci.mozITXTToHTMLConv);
+ return aTXT => cs.scanTXT(aTXT, cs.kEntities);
+});
+
+// The timespan after which we consider LIST roomInfo to be stale.
+export var kListRefreshInterval = 12 * 60 * 60 * 1000; // 12 hours.
+
+/*
+ * The supported formatting control characters, as described in
+ * http://www.invlogic.com/irc/ctcp.html#3.11
+ * If a string is given, it will replace the control character; if null is
+ * given, the current HTML tag stack will be closed; if a function is given,
+ * it expects two parameters:
+ * aStack The ordered list of open HTML tags.
+ * aInput The current input string.
+ * There are three output values returned in an array:
+ * The new ordered list of open HTML tags.
+ * The new text output to append.
+ * The number of characters (from the start of the input string) that the
+ * function handled.
+ */
+var CTCP_TAGS = {
+ "\x02": "b", // \002, ^B, Bold
+ "\x16": "i", // \026, ^V, Reverse or Inverse (Italics)
+ "\x1D": "i", // \035, ^], Italics (mIRC)
+ "\x1F": "u", // \037, ^_, Underline
+ "\x03": mIRCColoring, // \003, ^C, Coloring
+ "\x0F": null, // \017, ^O, Clear all formatting
+};
+
+// Generate an expression that will search for any of the control characters.
+var CTCP_TAGS_EXP = new RegExp("[" + Object.keys(CTCP_TAGS).join("") + "]");
+
+// Remove all CTCP formatting characters.
+export function ctcpFormatToText(aString) {
+ let next,
+ input = aString,
+ output = "",
+ length;
+
+ while ((next = CTCP_TAGS_EXP.exec(input))) {
+ if (next.index > 0) {
+ output += input.substr(0, next.index);
+ }
+ // We assume one character will be stripped.
+ length = 1;
+ let tag = CTCP_TAGS[input[next.index]];
+ // If the tag is a function, calculate how many characters are handled.
+ if (typeof tag == "function") {
+ [, , length] = tag([], input.substr(next.index));
+ }
+
+ // Avoid infinite loops.
+ length = Math.max(1, length);
+ // Skip to after the last match.
+ input = input.substr(next.index + length);
+ }
+ // Append the unmatched bits before returning the output.
+ return output + input;
+}
+
+function openStack(aStack) {
+ return aStack.map(aTag => "<" + aTag + ">").join("");
+}
+
+// Close the tags in the opposite order they were opened.
+function closeStack(aStack) {
+ return aStack
+ .reverse()
+ .map(aTag => "</" + aTag.split(" ", 1) + ">")
+ .join("");
+}
+
+/**
+ * Convert a string from CTCP escaped formatting to HTML markup.
+ *
+ * @param aString the string with CTCP formatting to parse
+ * @returns The HTML output string
+ */
+export function ctcpFormatToHTML(aString) {
+ let next,
+ stack = [],
+ input = lazy.TXTToHTML(aString),
+ output = "",
+ newOutput,
+ length;
+
+ while ((next = CTCP_TAGS_EXP.exec(input))) {
+ if (next.index > 0) {
+ output += input.substr(0, next.index);
+ }
+ length = 1;
+ let tag = CTCP_TAGS[input[next.index]];
+ if (tag === null) {
+ // Clear all formatting.
+ output += closeStack(stack);
+ stack = [];
+ } else if (typeof tag == "function") {
+ [stack, newOutput, length] = tag(stack, input.substr(next.index));
+ output += newOutput;
+ } else {
+ let offset = stack.indexOf(tag);
+ if (offset == -1) {
+ // Tag not found; open new tag.
+ output += "<" + tag + ">";
+ stack.push(tag);
+ } else {
+ // Tag found; close existing tag (and all tags after it).
+ output += closeStack(stack.slice(offset));
+ // Reopen the tags that came after it.
+ output += openStack(stack.slice(offset + 1));
+ // Remove the tag from the stack.
+ stack.splice(offset, 1);
+ }
+ }
+
+ // Avoid infinite loops.
+ length = Math.max(1, length);
+ // Skip to after the last match.
+ input = input.substr(next.index + length);
+ }
+ // Return unmatched bits and close any open tags at the end.
+ return output + input + closeStack(stack);
+}
+
+// mIRC colors are defined at http://www.mirc.com/colors.html.
+// This expression matches \003<one or two digits>[,<one or two digits>].
+// eslint-disable-next-line no-control-regex
+var M_IRC_COLORS_EXP = /^\x03(?:(\d\d?)(?:,(\d\d?))?)?/;
+var M_IRC_COLOR_MAP = {
+ 0: "white",
+ 1: "black",
+ 2: "navy", // blue (navy)
+ 3: "green",
+ 4: "red",
+ 5: "maroon", // brown (maroon)
+ 6: "purple",
+ 7: "orange", // orange (olive)
+ 8: "yellow",
+ 9: "lime", // light green (lime)
+ 10: "teal", // teal (a green/blue cyan)
+ 11: "aqua", // light cyan (cyan) (aqua)
+ 12: "blue", // light blue (royal)",
+ 13: "fuchsia", // pink (light purple) (fuchsia)
+ 14: "grey",
+ 15: "silver", // light grey (silver)
+ 99: "transparent",
+};
+
+function mIRCColoring(aStack, aInput) {
+ function getColor(aKey) {
+ let key = aKey;
+ // Single digit numbers can (must?) be prefixed by a zero.
+ if (key.length == 2 && key[0] == "0") {
+ key = key[1];
+ }
+
+ if (M_IRC_COLOR_MAP.hasOwnProperty(key)) {
+ return M_IRC_COLOR_MAP[key];
+ }
+
+ return null;
+ }
+
+ let matches,
+ stack = aStack,
+ input = aInput,
+ output = "",
+ length = 1;
+
+ if ((matches = M_IRC_COLORS_EXP.exec(input))) {
+ let format = ["font"];
+
+ // Only \003 was found with no formatting digits after it, close the
+ // first open font tag.
+ if (!matches[1]) {
+ // Find the first font tag.
+ let offset = stack.map(aTag => aTag.indexOf("font") === 0).indexOf(true);
+
+ // Close all tags from the first font tag on.
+ output = closeStack(stack.slice(offset));
+ // Remove the font tags from the stack.
+ stack = stack.filter(aTag => aTag.indexOf("font"));
+ // Reopen the other tags.
+ output += openStack(stack.slice(offset));
+ } else {
+ // Otherwise we have a match and are setting new colors.
+ // The foreground color.
+ let color = getColor(matches[1]);
+ if (color) {
+ format.push('color="' + color + '"');
+ }
+
+ // The background color.
+ if (matches[2]) {
+ let color = getColor(matches[2]);
+ if (color) {
+ format.push('background="' + color + '"');
+ }
+ }
+
+ if (format.length > 1) {
+ let tag = format.join(" ");
+ output = "<" + tag + ">";
+ stack.push(tag);
+ length = matches[0].length;
+ }
+ }
+ }
+
+ return [stack, output, length];
+}
+
+// Print an error message into a conversation, optionally mark the conversation
+// as not joined and/or not rejoinable.
+export function conversationErrorMessage(
+ aAccount,
+ aMessage,
+ aError,
+ aJoinFailed = false,
+ aRejoinable = true
+) {
+ let conv = aAccount.getConversation(aMessage.params[1]);
+ conv.writeMessage(
+ aMessage.origin,
+ lazy._(aError, aMessage.params[1], aMessage.params[2] || undefined),
+ {
+ error: true,
+ system: true,
+ }
+ );
+ delete conv._pendingMessage;
+
+ // Channels have a couple extra things that can be done to them.
+ if (aAccount.isMUCName(aMessage.params[1])) {
+ // If a value for joining is explicitly given, mark it.
+ if (aJoinFailed) {
+ conv.joining = false;
+ }
+ // If the conversation cannot be rejoined automatically, delete
+ // chatRoomFields.
+ if (!aRejoinable) {
+ delete conv.chatRoomFields;
+ }
+ }
+
+ return true;
+}
+
+/**
+ * Display a PRIVMSG or NOTICE in a conversation.
+ *
+ * @param {ircAccount} aAccount - The current account.
+ * @param {ircMessage} aMessage - The IRC message to display, provides the IRC
+ * tags, conversation name, and sender.
+ * @param {object} aExtraParams - (Extra) parameters to pass to ircConversation.writeMessage.
+ * @param {string|null} aText - The text to display, defaults to the second parameter
+ * on aMessage.
+ * @returns {boolean} True if the message was sent successfully.
+ */
+export function displayMessage(aAccount, aMessage, aExtraParams, aText) {
+ let params = { tags: aMessage.tags, ...aExtraParams };
+ // If the the message is from our nick, it is outgoing to the conversation it
+ // is targeting. Otherwise, the message is incoming, but could be for a
+ // private message or a channel.
+ //
+ // Note that the only time it is expected to receive a message from us is if
+ // the echo-message capability is enabled.
+ let convName;
+ if (
+ aAccount.normalizeNick(aMessage.origin) ==
+ aAccount.normalizeNick(aAccount._nickname)
+ ) {
+ params.outgoing = true;
+ // The conversation name is who it is being sent to.
+ convName = aMessage.params[0];
+ } else {
+ params.incoming = true;
+ // If the target is a MUC name, use the target as the conversation name.
+ // Otherwise, this is a private message: use the sender as the conversation
+ // name.
+ convName = aAccount.isMUCName(aMessage.params[0])
+ ? aMessage.params[0]
+ : aMessage.origin;
+ }
+ aAccount
+ .getConversation(convName)
+ .writeMessage(aMessage.origin, aText || aMessage.params[1], params);
+ return true;
+}