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