summaryrefslogtreecommitdiffstats
path: root/comm/chat/protocols/irc/ircCommands.sys.mjs
diff options
context:
space:
mode:
Diffstat (limited to 'comm/chat/protocols/irc/ircCommands.sys.mjs')
-rw-r--r--comm/chat/protocols/irc/ircCommands.sys.mjs599
1 files changed, 599 insertions, 0 deletions
diff --git a/comm/chat/protocols/irc/ircCommands.sys.mjs b/comm/chat/protocols/irc/ircCommands.sys.mjs
new file mode 100644
index 0000000000..78d984b179
--- /dev/null
+++ b/comm/chat/protocols/irc/ircCommands.sys.mjs
@@ -0,0 +1,599 @@
+/* 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 is to be exported directly onto the IRC prplIProtocol object, directly
+// implementing the commands field before we register them.
+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")
+);
+
+// Shortcut to get the JavaScript conversation object.
+function getConv(aConv) {
+ return aConv.wrappedJSObject;
+}
+
+// Shortcut to get the JavaScript account object.
+function getAccount(aConv) {
+ return getConv(aConv)._account;
+}
+
+// Trim leading and trailing spaces and split a string by any type of space.
+function splitInput(aString) {
+ return aString.trim().split(/\s+/);
+}
+
+// Kick a user from a channel
+// aMsg is <user> [comment]
+function kickCommand(aMsg, aConv) {
+ if (!aMsg.length) {
+ return false;
+ }
+
+ let params = [aConv.name];
+ let offset = aMsg.indexOf(" ");
+ if (offset != -1) {
+ params.push(aMsg.slice(0, offset));
+ params.push(aMsg.slice(offset + 1));
+ } else {
+ params.push(aMsg);
+ }
+
+ getAccount(aConv).sendMessage("KICK", params);
+ return true;
+}
+
+// Send a message directly to a user.
+// aMsg is <user> <message>
+// aReturnedConv is optional and returns the resulting conversation.
+function messageCommand(aMsg, aConv, aReturnedConv, aIsNotice = false) {
+ // Trim leading whitespace.
+ aMsg = aMsg.trimLeft();
+
+ let nickname = aMsg;
+ let message = "";
+
+ let sep = aMsg.indexOf(" ");
+ if (sep > -1) {
+ nickname = aMsg.slice(0, sep);
+ message = aMsg.slice(sep + 1);
+ }
+ if (!nickname.length) {
+ return false;
+ }
+
+ let conv = getAccount(aConv).getConversation(nickname);
+ if (aReturnedConv) {
+ aReturnedConv.value = conv;
+ }
+
+ if (!message.length) {
+ return true;
+ }
+
+ return privateMessage(aConv, message, nickname, aReturnedConv, aIsNotice);
+}
+
+// aAdd is true to add a mode, false to remove a mode.
+function setMode(aNickname, aConv, aMode, aAdd) {
+ if (!aNickname.length) {
+ return false;
+ }
+
+ // Change the mode for each nick, as separator by spaces.
+ return splitInput(aNickname).every(aNick =>
+ simpleCommand(aConv, "MODE", [
+ aConv.name,
+ (aAdd ? "+" : "-") + aMode,
+ aNick,
+ ])
+ );
+}
+
+function actionCommand(aMsg, aConv) {
+ // Don't try to send an empty action.
+ if (!aMsg || !aMsg.trim().length) {
+ return false;
+ }
+
+ let conv = getConv(aConv);
+
+ conv.sendMsg(aMsg, true);
+
+ return true;
+}
+
+// This will open the conversation, and send and display the text.
+// aReturnedConv is optional and returns the resulting conversation.
+// aIsNotice is optional and sends a NOTICE instead of a PRIVMSG.
+function privateMessage(aConv, aMsg, aNickname, aReturnedConv, aIsNotice) {
+ if (!aMsg.length) {
+ return false;
+ }
+
+ let conv = getAccount(aConv).getConversation(aNickname);
+ conv.sendMsg(aMsg, false, aIsNotice);
+ if (aReturnedConv) {
+ aReturnedConv.value = conv;
+ }
+ return true;
+}
+
+// This will send a command to the server, if no parameters are given, it is
+// assumed that the command takes no parameters. aParams can be either a single
+// string or an array of parameters.
+function simpleCommand(aConv, aCommand, aParams) {
+ if (!aParams || !aParams.length) {
+ getAccount(aConv).sendMessage(aCommand);
+ } else {
+ getAccount(aConv).sendMessage(aCommand, aParams);
+ }
+ return true;
+}
+
+// Sends a CTCP message to aTarget using the CTCP command aCommand and aMsg as
+// a CTCP parameter.
+function ctcpCommand(aConv, aTarget, aCommand, aParams) {
+ return getAccount(aConv).sendCTCPMessage(aTarget, false, aCommand, aParams);
+}
+
+// Replace the command name in the help string so translators do not attempt to
+// translate it.
+export var commands = [
+ {
+ name: "action",
+ get helpString() {
+ return lazy._("command.action", "action");
+ },
+ run: actionCommand,
+ },
+ {
+ name: "ban",
+ get helpString() {
+ return lazy._("command.ban", "ban");
+ },
+ usageContext: Ci.imICommand.CMD_CONTEXT_CHAT,
+ run: (aMsg, aConv) => setMode(aMsg, aConv, "b", true),
+ },
+ {
+ name: "ctcp",
+ get helpString() {
+ return lazy._("command.ctcp", "ctcp");
+ },
+ run(aMsg, aConv) {
+ let separator = aMsg.indexOf(" ");
+ // Ensure we have two non-empty parameters.
+ if (separator < 1 || separator + 1 == aMsg.length) {
+ return false;
+ }
+
+ // The first word is used as the target, the rest is used as CTCP command
+ // and parameters.
+ ctcpCommand(aConv, aMsg.slice(0, separator), aMsg.slice(separator + 1));
+ return true;
+ },
+ },
+ {
+ name: "chanserv",
+ get helpString() {
+ return lazy._("command.chanserv", "chanserv");
+ },
+ run: (aMsg, aConv) => privateMessage(aConv, aMsg, "ChanServ"),
+ },
+ {
+ name: "deop",
+ get helpString() {
+ return lazy._("command.deop", "deop");
+ },
+ usageContext: Ci.imICommand.CMD_CONTEXT_CHAT,
+ run: (aMsg, aConv) => setMode(aMsg, aConv, "o", false),
+ },
+ {
+ name: "devoice",
+ get helpString() {
+ return lazy._("command.devoice", "devoice");
+ },
+ usageContext: Ci.imICommand.CMD_CONTEXT_CHAT,
+ run: (aMsg, aConv) => setMode(aMsg, aConv, "v", false),
+ },
+ {
+ name: "invite",
+ get helpString() {
+ return lazy._("command.invite2", "invite");
+ },
+ run(aMsg, aConv) {
+ let params = splitInput(aMsg);
+
+ // Try to find one, and only one, channel in the list of parameters.
+ let channel;
+ let account = getAccount(aConv);
+ // Find the first param that could be a channel name.
+ for (let i = 0; i < params.length; ++i) {
+ if (account.isMUCName(params[i])) {
+ // If channel is set, two channel names have been found.
+ if (channel) {
+ return false;
+ }
+
+ // Remove that parameter and store it.
+ channel = params.splice(i, 1)[0];
+ }
+ }
+
+ // If no parameters or only a channel are given.
+ if (!params[0].length) {
+ return false;
+ }
+
+ // Default to using the current conversation as the channel to invite to.
+ if (!channel) {
+ channel = aConv.name;
+ }
+
+ params.forEach(p => simpleCommand(aConv, "INVITE", [p, channel]));
+ return true;
+ },
+ },
+ {
+ name: "join",
+ get helpString() {
+ return lazy._("command.join", "join");
+ },
+ run(aMsg, aConv, aReturnedConv) {
+ let params = aMsg.trim().split(/,\s*/);
+ let account = getAccount(aConv);
+ let conv;
+ if (!params[0]) {
+ conv = getConv(aConv);
+ if (!conv.isChat || !conv.left) {
+ return false;
+ }
+ // Rejoin the current channel. If the channel was explicitly parted
+ // by the user, chatRoomFields will have been deleted.
+ // Otherwise, make use of it (e.g. if the user was kicked).
+ if (conv.chatRoomFields) {
+ account.joinChat(conv.chatRoomFields);
+ return true;
+ }
+ params = [conv.name];
+ }
+ params.forEach(function (joinParam) {
+ if (joinParam) {
+ let chatroomfields = account.getChatRoomDefaultFieldValues(joinParam);
+ conv = account.joinChat(chatroomfields);
+ }
+ });
+ if (aReturnedConv) {
+ aReturnedConv.value = conv;
+ }
+ return true;
+ },
+ },
+ {
+ name: "kick",
+ get helpString() {
+ return lazy._("command.kick", "kick");
+ },
+ usageContext: Ci.imICommand.CMD_CONTEXT_CHAT,
+ run: kickCommand,
+ },
+ {
+ name: "list",
+ get helpString() {
+ return lazy._("command.list", "list");
+ },
+ run(aMsg, aConv, aReturnedConv) {
+ let account = getAccount(aConv);
+ let serverName = account._currentServerName;
+ let serverConv = account.getConversation(serverName);
+ let pendingChats = [];
+ account.requestRoomInfo(
+ {
+ onRoomInfoAvailable(aRooms) {
+ if (!pendingChats.length) {
+ (async function () {
+ // pendingChats has no rooms added yet, so ensure we wait a tick.
+ let t = 0;
+ const kMaxBlockTime = 10; // Unblock every 10ms.
+ do {
+ if (Date.now() > t) {
+ await new Promise(resolve =>
+ Services.tm.dispatchToMainThread(resolve)
+ );
+ t = Date.now() + kMaxBlockTime;
+ }
+ let name = pendingChats.pop();
+ let roomInfo = account.getRoomInfo(name);
+ serverConv.writeMessage(
+ serverName,
+ name +
+ " (" +
+ roomInfo.participantCount +
+ ") " +
+ roomInfo.topic,
+ {
+ incoming: true,
+ noLog: true,
+ }
+ );
+ } while (pendingChats.length);
+ })();
+ }
+ pendingChats = pendingChats.concat(aRooms);
+ },
+ },
+ true
+ );
+ if (aReturnedConv) {
+ aReturnedConv.value = serverConv;
+ }
+ return true;
+ },
+ },
+ {
+ name: "me",
+ get helpString() {
+ return lazy._("command.action", "me");
+ },
+ run: actionCommand,
+ },
+ {
+ name: "memoserv",
+ get helpString() {
+ return lazy._("command.memoserv", "memoserv");
+ },
+ run: (aMsg, aConv) => privateMessage(aConv, aMsg, "MemoServ"),
+ },
+ {
+ name: "mode",
+ get helpString() {
+ return (
+ lazy._("command.modeUser2", "mode") +
+ "\n" +
+ lazy._("command.modeChannel2", "mode")
+ );
+ },
+ run(aMsg, aConv) {
+ function isMode(aString) {
+ return "+-".includes(aString[0]);
+ }
+ let params = splitInput(aMsg);
+ let channel = aConv.name;
+ // Add the channel as parameter when the target is not specified. i.e
+ // 1. message is empty.
+ // 2. the first parameter is a mode.
+ if (!aMsg) {
+ params = [channel];
+ } else if (isMode(params[0])) {
+ params.unshift(channel);
+ }
+
+ // Ensure mode string to be the second argument.
+ if (params.length >= 2 && !isMode(params[1])) {
+ return false;
+ }
+
+ return simpleCommand(aConv, "MODE", params);
+ },
+ },
+ {
+ name: "msg",
+ get helpString() {
+ return lazy._("command.msg", "msg");
+ },
+ run: messageCommand,
+ },
+ {
+ name: "nick",
+ get helpString() {
+ return lazy._("command.nick", "nick");
+ },
+ run(aMsg, aConv) {
+ let newNick = aMsg.trim();
+ // eslint-disable-next-line mozilla/use-includes-instead-of-indexOf
+ if (newNick.indexOf(/\s+/) != -1) {
+ return false;
+ }
+
+ let account = getAccount(aConv);
+ // The user wants to change their nick, so overwrite the account
+ // nickname for this session.
+ account._requestedNickname = newNick;
+ account.changeNick(newNick);
+
+ return true;
+ },
+ },
+ {
+ name: "nickserv",
+ get helpString() {
+ return lazy._("command.nickserv", "nickserv");
+ },
+ run: (aMsg, aConv) => privateMessage(aConv, aMsg, "NickServ"),
+ },
+ {
+ name: "notice",
+ get helpString() {
+ return lazy._("command.notice", "notice");
+ },
+ run: (aMsg, aConv, aReturnedConv) =>
+ messageCommand(aMsg, aConv, aReturnedConv, true),
+ },
+ {
+ name: "op",
+ get helpString() {
+ return lazy._("command.op", "op");
+ },
+ usageContext: Ci.imICommand.CMD_CONTEXT_CHAT,
+ run: (aMsg, aConv) => setMode(aMsg, aConv, "o", true),
+ },
+ {
+ name: "operserv",
+ get helpString() {
+ return lazy._("command.operserv", "operserv");
+ },
+ run: (aMsg, aConv) => privateMessage(aConv, aMsg, "OperServ"),
+ },
+ {
+ name: "part",
+ get helpString() {
+ return lazy._("command.part", "part");
+ },
+ usageContext: Ci.imICommand.CMD_CONTEXT_CHAT,
+ run(aMsg, aConv) {
+ getConv(aConv).part(aMsg);
+ return true;
+ },
+ },
+ {
+ name: "ping",
+ get helpString() {
+ return lazy._("command.ping", "ping");
+ },
+ run(aMsg, aConv) {
+ // Send a ping to the entered nick using the current time (in
+ // milliseconds) as the param. If no nick is entered, ping the
+ // server.
+ if (aMsg && aMsg.trim().length) {
+ ctcpCommand(aConv, aMsg, "PING", Date.now());
+ } else {
+ getAccount(aConv).sendMessage("PING", Date.now());
+ }
+
+ return true;
+ },
+ },
+ {
+ name: "query",
+ get helpString() {
+ return lazy._("command.msg", "query");
+ },
+ run: messageCommand,
+ },
+ {
+ name: "quit",
+ get helpString() {
+ return lazy._("command.quit", "quit");
+ },
+ run(aMsg, aConv) {
+ let account = getAccount(aConv);
+ account.disconnect(aMsg);
+ // While prpls shouldn't usually touch imAccount, this disconnection
+ // is an action the user requested via the UI. Without this call,
+ // the imAccount would immediately reconnect the account.
+ account.imAccount.disconnect();
+ return true;
+ },
+ },
+ {
+ name: "quote",
+ get helpString() {
+ return lazy._("command.quote", "quote");
+ },
+ run(aMsg, aConv) {
+ if (!aMsg.length) {
+ return false;
+ }
+
+ getAccount(aConv).sendRawMessage(aMsg);
+ return true;
+ },
+ },
+ {
+ name: "remove",
+ get helpString() {
+ return lazy._("command.kick", "remove");
+ },
+ usageContext: Ci.imICommand.CMD_CONTEXT_CHAT,
+ run: kickCommand,
+ },
+ {
+ name: "time",
+ get helpString() {
+ return lazy._("command.time", "time");
+ },
+ run(aMsg, aConv) {
+ // Send a time command to the entered nick using the current time (in
+ // milliseconds) as the param. If no nick is entered, get the current
+ // server time.
+ if (aMsg && aMsg.trim().length) {
+ ctcpCommand(aConv, aMsg, "TIME");
+ } else {
+ getAccount(aConv).sendMessage("TIME");
+ }
+
+ return true;
+ },
+ },
+ {
+ name: "topic",
+ get helpString() {
+ return lazy._("command.topic", "topic");
+ },
+ usageContext: Ci.imICommand.CMD_CONTEXT_CHAT,
+ run(aMsg, aConv) {
+ aConv.topic = aMsg;
+ return true;
+ },
+ },
+ {
+ name: "umode",
+ get helpString() {
+ return lazy._("command.umode", "umode");
+ },
+ run(aMsg, aConv) {
+ let params = aMsg ? splitInput(aMsg) : [];
+ params.unshift(getAccount(aConv)._nickname);
+ return simpleCommand(aConv, "MODE", params);
+ },
+ },
+ {
+ name: "version",
+ get helpString() {
+ return lazy._("command.version", "version");
+ },
+ run(aMsg, aConv) {
+ if (!aMsg || !aMsg.trim().length) {
+ return false;
+ }
+ ctcpCommand(aConv, aMsg, "VERSION");
+ return true;
+ },
+ },
+ {
+ name: "voice",
+ get helpString() {
+ return lazy._("command.voice", "voice");
+ },
+ usageContext: Ci.imICommand.CMD_CONTEXT_CHAT,
+ run: (aMsg, aConv) => setMode(aMsg, aConv, "v", true),
+ },
+ {
+ name: "whois",
+ get helpString() {
+ return lazy._("command.whois2", "whois");
+ },
+ run(aMsg, aConv) {
+ // Note that this will automatically run whowas if the nick is offline.
+ aMsg = aMsg.trim();
+ // If multiple parameters are given, this is an error.
+ if (aMsg.includes(" ")) {
+ return false;
+ }
+ // If the user does not provide a nick, but is in a private conversation,
+ // assume the user is trying to whois the person they are talking to.
+ if (!aMsg) {
+ if (aConv.isChat) {
+ return false;
+ }
+ aMsg = aConv.name;
+ }
+ getConv(aConv).requestCurrentWhois(aMsg);
+ return true;
+ },
+ },
+];