/* 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 WATCH and MONITOR commands: ways to more efficiently * (compared to ISON) keep track of a user's status. * * MONITOR (supported by Charybdis) * https://github.com/atheme/charybdis/blob/master/doc/monitor.txt * WATCH (supported by Bahamut and UnrealIRCd) * http://www.stack.nl/~jilles/cgi-bin/hgwebdir.cgi/irc-documentation-jilles/raw-file/tip/reference/draft-meglio-irc-watch-00.txt */ import { clearTimeout } from "resource://gre/modules/Timer.sys.mjs"; import { ircHandlerPriorities } from "resource:///modules/ircHandlerPriorities.sys.mjs"; function setStatus(aAccount, aNick, aStatus) { if (!aAccount.watchEnabled && !aAccount.monitorEnabled) { return false; } if (aStatus == "AWAY") { // We need to request the away message. aAccount.requestCurrentWhois(aNick); } else { // Clear the WHOIS information. aAccount.removeBuddyInfo(aNick); } let buddy = aAccount.buddies.get(aNick); if (!buddy) { return false; } buddy.setStatus(Ci.imIStatusInfo["STATUS_" + aStatus], ""); return true; } function trackBuddyWatch(aNicks) { // aNicks is an array when WATCH is initialized, and a single nick // in all later calls. if (!Array.isArray(aNicks)) { // We update the trackQueue if an individual nick is being added, // so the nick will also be monitored after a reconnect. Object.getPrototypeOf(this).trackBuddy.call(this, aNicks); aNicks = [aNicks]; } let nicks = aNicks.map(aNick => "+" + aNick); if (!nicks.length) { return; } let newWatchLength = this.watchLength + nicks.length; if (newWatchLength > this.maxWatchLength) { this.WARN( "Attempting to WATCH " + newWatchLength + " nicks; maximum size is " + this.maxWatchLength + "." ); // TODO We should trim the list and add the extra users to an ISON queue, // but that's not currently implemented, so just hope the server doesn't // enforce it's own limit. } this.watchLength = newWatchLength; // Watch away as well as online. let params = []; if (this.watchAwayEnabled) { params.push("A"); } let maxLength = this.maxMessageLength - 2 - this.countBytes(this.buildMessage("WATCH", params)); for (let nick of nicks) { if (this.countBytes(params + " " + nick) >= maxLength) { // If the message would be too long, first send this message. this.sendMessage("WATCH", params); // Reset for the next message. params = []; if (this.watchAwayEnabled) { params.push("A"); } } params.push(nick); } this.sendMessage("WATCH", params); } function untrackBuddyWatch(aNick) { --this.watchLength; this.sendMessage("WATCH", "-" + aNick); Object.getPrototypeOf(this).untrackBuddy.call(this, aNick); } export var isupportWATCH = { name: "WATCH", // Slightly above default ISUPPORT priority. priority: ircHandlerPriorities.DEFAULT_PRIORITY + 10, isEnabled: () => true, commands: { WATCH(aMessage) { if (!aMessage.isupport.useDefault) { this.maxWatchLength = 128; } else { let size = parseInt(aMessage.isupport.value, 10); if (isNaN(size)) { return false; } this.maxWatchLength = size; } this.watchEnabled = true; // Clear our watchlist in case there is garbage in it. this.sendMessage("WATCH", "C"); this.watchLength = 0; // Kill the ISON polling loop. clearTimeout(this._isOnTimer); return true; }, WATCHOPTS(aMessage) { const watchOptToOption = { H: "watchMasksEnabled", A: "watchAwayEnabled", }; // For each option, mark it as supported. aMessage.isupport.value.split("").forEach(function (aWatchOpt) { if (watchOptToOption.hasOwnProperty(aWatchOpt)) { this[watchOptToOption[aWatchOpt]] = true; } }, this); return true; }, }, }; export var ircWATCH = { name: "WATCH", // Slightly above default IRC priority. priority: ircHandlerPriorities.DEFAULT_PRIORITY + 10, // Use WATCH if it is supported. isEnabled() { return !!this.watchEnabled; }, commands: { 251(aMessage) { // RPL_LUSERCLIENT // ":There are users and services on servers" // Assume that this will always be sent after the 005 handler on // connection registration. If WATCH is enabled, then set the new function // to keep track of nicks and send the messages to watch the nicks. // Ensure that any new buddies are set to be watched, and removed buddies // are no longer watched. this.trackBuddy = trackBuddyWatch; this.untrackBuddy = untrackBuddyWatch; // Build the watchlist from the current list of nicks. this.trackBuddy(this.trackQueue); // Fall through to other handlers since we're only using this as an entry // point and not actually handling the message. return false; }, 301(aMessage) { // RPL_AWAY // : // Set the received away message. let buddy = this.buddies.get(aMessage.params[1]); if (buddy) { buddy.setStatus(Ci.imIStatusInfo.STATUS_AWAY, aMessage.params[2]); } // Fall through to the other implementations after setting the status // message. return false; }, 303(aMessage) { // RPL_ISON // :*1 *( " " ) // We don't want ircBase to interfere with us, so override the ISON // handler to do nothing. return true; }, 512(aMessage) { // ERR_TOOMANYWATCH // Maximum size for WATCH-list is entries this.ERROR( "Maximum size for WATCH list exceeded (" + this.watchLength + ")." ); return true; }, 597(aMessage) { // RPL_REAWAY // : return setStatus(this, aMessage.params[1], "AWAY"); }, 598(aMessage) { // RPL_GONEAWAY // : // We use a negative index as inspircd versions < 2.0.18 don't send // the user's nick as the first parameter (see bug 1078223). return setStatus( this, aMessage.params[aMessage.params.length - 5], "AWAY" ); }, 599(aMessage) { // RPL_NOTAWAY // :is no longer away // We use a negative index as inspircd versions < 2.0.18 don't send // the user's nick as the first parameter (see bug 1078223). return setStatus( this, aMessage.params[aMessage.params.length - 5], "AVAILABLE" ); }, 600(aMessage) { // RPL_LOGON // :logged on return setStatus(this, aMessage.params[1], "AVAILABLE"); }, 601(aMessage) { // RPL_LOGOFF // :logged off return setStatus(this, aMessage.params[1], "OFFLINE"); }, 602(aMessage) { // RPL_WATCHOFF // :stopped watching return true; }, 603(aMessage) { // RPL_WATCHSTAT // You have and are on WATCH entries // TODO I don't think we really need to care about this. return false; }, 604(aMessage) { // RPL_NOWON // :is online return setStatus(this, aMessage.params[1], "AVAILABLE"); }, 605(aMessage) { // RPL_NOWOFF // :is offline return setStatus(this, aMessage.params[1], "OFFLINE"); }, 606(aMessage) { // RPL_WATCHLIST // // TODO return false; }, 607(aMessage) { // RPL_ENDOFWATCHLIST // End of WATCH // TODO return false; }, 608(aMessage) { // RPL_CLEARWATCH // Your WATCH list is now empty // Note that this is optional for servers to send, so ignore it. return true; }, 609(aMessage) { // RPL_NOWISAWAY // : return setStatus(this, aMessage.params[1], "AWAY"); }, }, }; export var isupportMONITOR = { name: "MONITOR", // Slightly above default ISUPPORT priority. priority: ircHandlerPriorities.DEFAULT_PRIORITY + 10, isEnabled: () => true, commands: { MONITOR(aMessage) { if (!aMessage.isupport.useDefault) { this.maxMonitorLength = Infinity; } else { let size = parseInt(aMessage.isupport.value, 10); if (isNaN(size)) { return false; } this.maxMonitorLength = size; } this.monitorEnabled = true; // Clear our monitor list in case there is garbage in it. this.sendMessage("MONITOR", "C"); this.monitorLength = 0; // Kill the ISON polling loop. clearTimeout(this._isOnTimer); return true; }, }, }; function trackBuddyMonitor(aNicks) { // aNicks is an array when MONITOR is initialized, and a single nick // in all later calls. if (!Array.isArray(aNicks)) { // We update the trackQueue if an individual nick is being added, // so the nick will also be monitored after a reconnect. Object.getPrototypeOf(this).trackBuddy.call(this, aNicks); aNicks = [aNicks]; } let nicks = aNicks; if (!nicks.length) { return; } let newMonitorLength = this.monitorLength + nicks.length; if (newMonitorLength > this.maxMonitorLength) { this.WARN( "Attempting to MONITOR " + newMonitorLength + " nicks; maximum size is " + this.maxMonitorLength + "." ); // TODO We should trim the list and add the extra users to an ISON queue, // but that's not currently implemented, so just hope the server doesn't // enforce it's own limit. } this.monitorLength = newMonitorLength; let params = []; let maxLength = this.maxMessageLength - 2 - this.countBytes(this.buildMessage("MONITOR", "+")); for (let nick of nicks) { if (this.countBytes(params + " " + nick) >= maxLength) { // If the message would be too long, first send this message. this.sendMessage("MONITOR", ["+", params.join(",")]); // Reset for the next message. params = []; } params.push(nick); } this.sendMessage("MONITOR", ["+", params.join(",")]); } function untrackBuddyMonitor(aNick) { --this.monitorLength; this.sendMessage("MONITOR", ["-", aNick]); Object.getPrototypeOf(this).untrackBuddy.call(this, aNick); } export var ircMONITOR = { name: "MONITOR", // Slightly above default IRC priority. priority: ircHandlerPriorities.DEFAULT_PRIORITY + 10, // Use MONITOR only if MONITOR is enabled and WATCH is not enabled, as WATCH // supports more features. isEnabled() { return this.monitorEnabled && !this.watchEnabled; }, commands: { 251(aMessage) { // RPL_LUSERCLIENT // ":There are users and services on servers" // Assume that this will always be sent after the 005 handler on // connection registration. If MONITOR is enabled, then set the new // function to keep track of nicks and send the messages to watch the // nicks. // Ensure that any new buddies are set to be watched, and removed buddies // are no longer watched. this.trackBuddy = trackBuddyMonitor; this.untrackBuddy = untrackBuddyMonitor; // Build the watchlist from the current list of nicks. this.trackBuddy(this.trackQueue); // Fall through to other handlers since we're only using this as an entry // point and not actually handling the message. return false; }, 303(aMessage) { // RPL_ISON // :*1 *( " " ) // We don't want ircBase to interfere with us, so override the ISON // handler to do nothing if we're using MONITOR. return true; }, 730(aMessage) { // RPL_MONONLINE // : 730 :nick!user@host[,nick!user@host]* // Mark each nick as online. return aMessage.params[1] .split(",") .map(aNick => setStatus(this, aNick.split("!", 1)[0], "AVAILABLE")) .every(aResult => aResult); }, 731(aMessage) { // RPL_MONOFFLINE // : 731 :nick[,nick1]* return aMessage.params[1] .split(",") .map(aNick => setStatus(this, aNick, "OFFLINE")) .every(aResult => aResult); }, 732(aMessage) { // RPL_MONLIST // : 732 :nick[,nick1]* return false; }, 733(aMessage) { // RPL_ENDOFMONLIST // : 733 :End of MONITOR list return false; }, 734(aMessage) { // ERR_MONLISTFULL // : 734 :Monitor list is full. this.ERROR( "Maximum size for MONITOR list exceeded (" + this.params[1] + ")." ); return true; }, }, };