diff options
Diffstat (limited to 'comm/chat/protocols/irc/ircUtils.sys.mjs')
-rw-r--r-- | comm/chat/protocols/irc/ircUtils.sys.mjs | 303 |
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; +} |