suite/chatzilla/lib/irc.js (3,187 lines of code) (raw):

/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*- * * 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/. */ const JSIRC_ERR_NO_SOCKET = "JSIRCE:NS"; const JSIRC_ERR_EXHAUSTED = "JSIRCE:E"; const JSIRC_ERR_CANCELLED = "JSIRCE:C"; const JSIRC_ERR_NO_SECURE = "JSIRCE:NO_SECURE"; const JSIRC_ERR_OFFLINE = "JSIRCE:OFFLINE"; const JSIRC_ERR_PAC_LOADING = "JSIRCE:PAC_LOADING"; const JSIRCV3_SUPPORTED_CAPS = [ "account-notify", "account-tag", "away-notify", "batch", "cap-notify", "chghost", "echo-message", "extended-join", "invite-notify", //"labeled-response", "message-tags", //"metadata", "multi-prefix", "sasl", "server-time", "tls", "userhost-in-names", ]; function renameProperty(obj, oldname, newname) { if (oldname == newname) { return; } obj[newname] = obj[oldname]; delete obj[oldname]; } function userIsMe(user) { switch (user.TYPE) { case "IRCUser": return user == user.parent.me; break; case "IRCChanUser": return user.__proto__ == user.parent.parent.me; break; default: return false; } return false; } /* * Attached to event objects in onRawData */ function decodeParam(number, charsetOrObject) { if (!charsetOrObject) { charsetOrObject = this.currentObject; } var rv = toUnicode(this.params[number], charsetOrObject); return rv; } // JavaScript won't let you delete things declared with "var", workaround: window.i = 1; const NET_OFFLINE = i++; // Initial, disconected. const NET_WAITING = i++; // Waiting before trying. const NET_CONNECTING = i++; // Trying a connect... const NET_CANCELLING = i++; // Cancelling connect. const NET_ONLINE = i++; // Connected ok. const NET_DISCONNECTING = i++; // Disconnecting. delete window.i; function CIRCNetwork(name, serverList, eventPump, temporary) { this.unicodeName = name; this.viewName = name; this.canonicalName = name; this.collectionKey = ":" + name; this.encodedName = name; this.servers = {}; this.serverList = []; this.ignoreList = {}; this.ignoreMaskCache = {}; this.state = NET_OFFLINE; this.temporary = Boolean(temporary); for (var i = 0; i < serverList.length; ++i) { var server = serverList[i]; var password = "password" in server ? server.password : null; var isSecure = "isSecure" in server ? server.isSecure : false; this.serverList.push( new CIRCServer(this, server.name, server.port, isSecure, password) ); } this.eventPump = eventPump; if ("onInit" in this) { this.onInit(); } } /** Clients should override this stuff themselves **/ CIRCNetwork.prototype.INITIAL_NICK = "js-irc"; CIRCNetwork.prototype.INITIAL_NAME = "INITIAL_NAME"; CIRCNetwork.prototype.INITIAL_DESC = "INITIAL_DESC"; CIRCNetwork.prototype.USE_SASL = false; CIRCNetwork.prototype.UPGRADE_INSECURE = false; CIRCNetwork.prototype.STS_MODULE = null; /* set INITIAL_CHANNEL to "" if you don't want a primary channel */ CIRCNetwork.prototype.INITIAL_CHANNEL = "#jsbot"; CIRCNetwork.prototype.INITIAL_UMODE = "+iw"; CIRCNetwork.prototype.MAX_CONNECT_ATTEMPTS = 5; CIRCNetwork.prototype.PAC_RECONNECT_DELAY = 5 * 1000; CIRCNetwork.prototype.getReconnectDelayMs = function () { return 15000; }; CIRCNetwork.prototype.stayingPower = false; // "http" = use HTTP proxy, "none" = none, anything else = auto. CIRCNetwork.prototype.PROXY_TYPE_OVERRIDE = ""; CIRCNetwork.prototype.TYPE = "IRCNetwork"; /** * Returns the IRC URL representation of this network. * * @param target A network-specific object to target the URL at. Instead of * passing it in here, call the target's |getURL| method. * @param flags An |Object| with flags (as properties) to be applied to the URL. */ CIRCNetwork.prototype.getURL = function (target, flags) { if (this.temporary) { return this.serverList[0].getURL(target, flags); } /* Determine whether to use the irc:// or ircs:// scheme */ var scheme = "irc"; if ( ("primServ" in this && this.primServ.isConnected && this.primServ.isSecure) || this.hasOnlySecureServers() ) { scheme = "ircs"; } var obj = { host: this.unicodeName, scheme }; if (target) { obj.target = target; } if (flags) { for (var i = 0; i < flags.length; i++) { obj[flags[i]] = true; } } return constructIRCURL(obj); }; CIRCNetwork.prototype.getUser = function (nick) { if ("primServ" in this && this.primServ) { return this.primServ.getUser(nick); } return null; }; CIRCNetwork.prototype.addServer = function (host, port, isSecure, password) { this.serverList.push(new CIRCServer(this, host, port, isSecure, password)); }; /** * Returns |true| iif a network has a secure server in its list. */ CIRCNetwork.prototype.hasSecureServer = function () { for (var i = 0; i < this.serverList.length; i++) { if (this.serverList[i].isSecure) { return true; } } return false; }; /** * Returns |true| iif a network only has secure servers in its list. */ CIRCNetwork.prototype.hasOnlySecureServers = function () { for (var i = 0; i < this.serverList.length; i++) { if (!this.serverList[i].isSecure) { return false; } } return true; }; CIRCNetwork.prototype.clearServerList = function () { /* Note: we don't have to worry about being connected, since primServ * keeps the currently connected server alive if we still need it. */ this.servers = {}; this.serverList = []; }; /** * Trigger an |onDoConnect| event after a delay. */ CIRCNetwork.prototype.delayedConnect = function (eventProperties) { function reconnectFn(network, eventProperties) { network.immediateConnect(eventProperties); } if ( -1 != this.MAX_CONNECT_ATTEMPTS && this.connectAttempt >= this.MAX_CONNECT_ATTEMPTS ) { this.state = NET_OFFLINE; var ev = new CEvent("network", "error", this, "onError"); ev.debug = "Connection attempts exhausted, giving up."; ev.errorCode = JSIRC_ERR_EXHAUSTED; this.eventPump.addEvent(ev); return; } this.state = NET_WAITING; this.reconnectTimer = setTimeout( reconnectFn, this.getReconnectDelayMs(), this, eventProperties ); }; /** * Immediately trigger an |onDoConnect| event. Use |delayedConnect| for automatic * repeat attempts, instead, to throttle the attempts to a reasonable pace. */ CIRCNetwork.prototype.immediateConnect = function (eventProperties) { var ev = new CEvent("network", "do-connect", this, "onDoConnect"); if (typeof eventProperties != "undefined") { for (var key in eventProperties) { ev[key] = eventProperties[key]; } } this.eventPump.addEvent(ev); }; CIRCNetwork.prototype.connect = function (requireSecurity) { if ("primServ" in this && this.primServ.isConnected) { return true; } // We need to test for secure servers in the network object here, // because without them all connection attempts will fail anyway. if (requireSecurity && !this.hasSecureServer()) { // No secure server, cope. ev = new CEvent("network", "error", this, "onError"); ev.server = this; ev.debug = "No connection attempted: no secure servers in list"; ev.errorCode = JSIRC_ERR_NO_SECURE; this.eventPump.addEvent(ev); return false; } this.state = NET_CONNECTING; this.connectAttempt = 0; // actual connection attempts this.connectCandidate = 0; // incl. requireSecurity non-attempts this.nextHost = 0; this.requireSecurity = requireSecurity || false; this.immediateConnect({ password: null }); return true; }; /** * Disconnects the network with a given reason. */ CIRCNetwork.prototype.quit = function (reason) { if (this.isConnected()) { this.primServ.logout(reason); } }; /** * Cancels the network's connection (whatever its current state). */ CIRCNetwork.prototype.cancel = function () { // We're online, pull the plug on the current connection, or... if (this.state == NET_ONLINE) { this.quit(); } // We're waiting for the 001, too late to throw a reconnect, or... else if (this.state == NET_CONNECTING) { this.state = NET_CANCELLING; if ("primServ" in this && this.primServ.isConnected) { this.primServ.connection.disconnect(); var ev = new CEvent("network", "error", this, "onError"); ev.server = this.primServ; ev.debug = "Connect sequence was canceled."; ev.errorCode = JSIRC_ERR_CANCELLED; this.eventPump.addEvent(ev); } } // We're waiting for onDoConnect, so try a reconnect (which will fail us) else if (this.state == NET_WAITING) { this.state = NET_CANCELLING; // onDoConnect will throw the error events for us, as it will fail this.immediateConnect(); } else { dd("Network cancel in odd state: " + this.state); } }; CIRCNetwork.prototype.onDoConnect = function (e) { const NS_ERROR_OFFLINE = 0x804b0010; var c; // Clear the timer, if there is one. if ("reconnectTimer" in this) { clearTimeout(this.reconnectTimer); delete this.reconnectTimer; } var ev; if (this.state == NET_CANCELLING) { if ("primServ" in this && this.primServ.isConnected) { this.primServ.connection.disconnect(); } else { this.state = NET_OFFLINE; } ev = new CEvent("network", "error", this, "onError"); ev.server = this.primServ; ev.debug = "Connect sequence was canceled."; ev.errorCode = JSIRC_ERR_CANCELLED; this.eventPump.addEvent(ev); return false; } if ("primServ" in this && this.primServ.isConnected) { return true; } this.connectAttempt++; this.connectCandidate++; this.state = NET_CONNECTING; /* connection is considered "made" when server * sends a 001 message (see server.on001) */ var host = this.nextHost++; if (host >= this.serverList.length) { this.nextHost = 1; host = 0; } // If STS is enabled, check the cache for a secure port to connect to. if (this.STS_MODULE.ENABLED && !this.serverList[host].isSecure) { var newPort = this.STS_MODULE.getUpgradePolicy( this.serverList[host].hostname ); if (newPort) { // If we're a temporary network, just change the server prior to connecting. if (this.temporary) { this.serverList[host].port = newPort; this.serverList[host].isSecure = true; } // Otherwise, find or create a server with the specified host and port. else { var hostname = this.serverList[host].hostname; var matches = this.serverList.filter(function (s) { return s.hostname == hostname && s.port == newPort; }); if (matches.length > 0) { host = this.serverList.indexOf(matches[0]); } else { this.addServer( hostname, newPort, true, this.serverList[host].password ); host = this.serverList.length - 1; } } } } if (this.serverList[host].isSecure || !this.requireSecurity) { ev = new CEvent("network", "startconnect", this, "onStartConnect"); ev.debug = "Connecting to " + this.serverList[host].unicodeName + ":" + this.serverList[host].port + ", attempt " + this.connectAttempt + " of " + this.MAX_CONNECT_ATTEMPTS + "..."; ev.host = this.serverList[host].hostname; ev.port = this.serverList[host].port; ev.server = this.serverList[host]; ev.connectAttempt = this.connectAttempt; ev.reconnectDelayMs = this.getReconnectDelayMs(); this.eventPump.addEvent(ev); try { this.serverList[host].connect(); } catch (ex) { this.state = NET_OFFLINE; ev = new CEvent("network", "error", this, "onError"); ev.server = this; ev.debug = "Exception opening socket: " + ex; ev.errorCode = JSIRC_ERR_NO_SOCKET; if (typeof ex == "object" && ex.result == NS_ERROR_OFFLINE) { ev.errorCode = JSIRC_ERR_OFFLINE; } if (typeof ex == "string" && ex == JSIRC_ERR_PAC_LOADING) { ev.errorCode = JSIRC_ERR_PAC_LOADING; ev.retryDelay = CIRCNetwork.prototype.PAC_RECONNECT_DELAY; /* PAC loading is not a problem with any specific server. We'll * retry the connection in 5 seconds. */ this.nextHost--; this.state = NET_WAITING; setTimeout( function (n) { n.immediateConnect(); }, ev.retryDelay, this ); } this.eventPump.addEvent(ev); } } else { /* Server doesn't use SSL as requested, try next one. * In the meantime, correct the connection attempt counter */ this.connectAttempt--; this.immediateConnect(); } return true; }; /** * Returns |true| iff this network has a socket-level connection. */ CIRCNetwork.prototype.isConnected = function (e) { return "primServ" in this && this.primServ.isConnected; }; CIRCNetwork.prototype.ignore = function (hostmask) { var input = getHostmaskParts(hostmask); if (input.mask in this.ignoreList) { return false; } this.ignoreList[input.mask] = input; this.ignoreMaskCache = {}; return true; }; CIRCNetwork.prototype.unignore = function (hostmask) { var input = getHostmaskParts(hostmask); if (!(input.mask in this.ignoreList)) { return false; } delete this.ignoreList[input.mask]; this.ignoreMaskCache = {}; return true; }; function CIRCServer(parent, hostname, port, isSecure, password) { var serverName = hostname + ":" + port; var s; if (serverName in parent.servers) { s = parent.servers[serverName]; } else { s = this; s.channels = {}; s.users = {}; } s.unicodeName = serverName; s.viewName = serverName; s.canonicalName = serverName; s.collectionKey = ":" + serverName; s.encodedName = serverName; s.hostname = hostname; s.port = port; s.parent = parent; s.isSecure = isSecure; s.password = password; s.connection = null; s.isConnected = false; s.sendQueue = []; s.lastSend = new Date("1/1/1980"); s.lastPingSent = null; s.lastPing = null; s.savedLine = ""; s.lag = -1; s.usersStable = true; s.supports = null; s.channelTypes = null; s.channelModes = null; s.channelCount = -1; s.userModes = null; s.maxLineLength = 400; s.caps = {}; s.capvals = {}; parent.servers[s.collectionKey] = s; if ("onInit" in s) { s.onInit(); } return s; } CIRCServer.prototype.MS_BETWEEN_SENDS = 1500; CIRCServer.prototype.READ_TIMEOUT = 100; CIRCServer.prototype.VERSION_RPLY = "JS-IRC Library v0.01, " + "Copyright (C) 1999 Robert Ginda; rginda@ndcico.com"; CIRCServer.prototype.OS_RPLY = "Unknown"; CIRCServer.prototype.HOST_RPLY = "Unknown"; CIRCServer.prototype.DEFAULT_REASON = "no reason"; /* true means WHO command doesn't collect hostmask, username, etc. */ CIRCServer.prototype.LIGHTWEIGHT_WHO = false; /* Unique identifier for WHOX commands. */ CIRCServer.prototype.WHOX_TYPE = "314"; /* -1 == never, 0 == prune onQuit, >0 == prune when >X ms old */ CIRCServer.prototype.PRUNE_OLD_USERS = -1; CIRCServer.prototype.TYPE = "IRCServer"; // Define functions to set modes so they're easily readable. // name is the name used on the CIRCChanMode object // getValue is a function returning the value the canonicalmode should be set to // given a certain modifier and appropriate data. CIRCServer.prototype.canonicalChanModes = { i: { name: "invite", getValue(modifier) { return modifier == "+"; }, }, m: { name: "moderated", getValue(modifier) { return modifier == "+"; }, }, n: { name: "publicMessages", getValue(modifier) { return modifier == "-"; }, }, t: { name: "publicTopic", getValue(modifier) { return modifier == "-"; }, }, s: { name: "secret", getValue(modifier) { return modifier == "+"; }, }, p: { name: "pvt", getValue(modifier) { return modifier == "+"; }, }, k: { name: "key", getValue(modifier, data) { if (modifier == "+") { return data; } return ""; }, }, l: { name: "limit", getValue(modifier, data) { // limit is special - we return -1 if there is no limit. if (modifier == "-") { return -1; } return data; }, }, }; CIRCServer.prototype.toLowerCase = function (str) { /* This is an implementation that lower-cases strings according to the * prevailing CASEMAPPING setting for the server. Values for this are: * * o "ascii": The ASCII characters 97 to 122 (decimal) are defined as * the lower-case characters of ASCII 65 to 90 (decimal). No other * character equivalency is defined. * o "strict-rfc1459": The ASCII characters 97 to 125 (decimal) are * defined as the lower-case characters of ASCII 65 to 93 (decimal). * No other character equivalency is defined. * o "rfc1459": The ASCII characters 97 to 126 (decimal) are defined as * the lower-case characters of ASCII 65 to 94 (decimal). No other * character equivalency is defined. * */ function replaceFunction(chr) { return String.fromCharCode(chr.charCodeAt(0) + 32); } var mapping = "rfc1459"; if (this.supports) { mapping = this.supports.casemapping; } /* NOTE: There are NO breaks in this switch. This is CORRECT. * Each mapping listed is a super-set of those below, thus we only * transform the extra characters, and then fall through. */ switch (mapping) { case "rfc1459": str = str.replace(/\^/g, replaceFunction); case "strict-rfc1459": str = str.replace(/[\[\\\]]/g, replaceFunction); case "ascii": str = str.replace(/[A-Z]/g, replaceFunction); } return str; }; // Iterates through the keys in an object and, if specified, the keys of // child objects. CIRCServer.prototype.renameProperties = function (obj, child) { for (let key in obj) { let item = obj[key]; item.canonicalName = this.toLowerCase(item.encodedName); item.collectionKey = ":" + item.canonicalName; renameProperty(obj, key, item.collectionKey); if (child && child in item) { this.renameProperties(item[child], null); } } }; // Encodes tag data to send. CIRCServer.prototype.encodeTagData = function (obj) { var dict = {}; dict[";"] = ":"; dict[" "] = "s"; dict["\\"] = "\\"; dict["\r"] = "r"; dict["\n"] = "n"; // Function for escaping key values. function escapeTagValue(data) { var rv = ""; for (var i = 0; i < data.length; i++) { var ci = data[i]; var co = dict[data[i]]; if (co) { rv += "\\" + co; } else { rv += ci; } } return rv; } var str = ""; for (var key in obj) { var val = obj[key]; str += key; if (val) { str += "="; str += escapeTagValue(val); } str += ";"; } // Remove any trailing semicolons. if (str[str.length - 1] == ";") { str = str.substring(0, str.length - 1); } return str; }; // Decodes received tag data. CIRCServer.prototype.decodeTagData = function (str) { // Remove the leading '@' if we have one. if (str[0] == "@") { str = str.substring(1); } var dict = {}; dict[":"] = ";"; dict.s = " "; dict["\\"] = "\\"; dict.r = "\r"; dict.n = "\n"; // Function for unescaping key values. function unescapeTagValue(data) { var rv = ""; for (let j = 0; j < data.length; j++) { let currentItem = data[j]; if (currentItem == "\\" && j < data.length - 1) { let nextItem = data[j + 1]; if (nextItem in dict) { rv += dict[nextItem]; } else { rv += nextItem; } j++; } else if (currentItem != "\\") { rv += currentItem; } } return rv; } var obj = Object(); var tags = str.split(";"); for (var i = 0; i < tags.length; i++) { var [key, val] = tags[i].split("="); if (val) { val = unescapeTagValue(val); } else { val = ""; } obj[key] = val; } return obj; }; // Returns the IRC URL representation of this server. CIRCServer.prototype.getURL = function (target, flags) { var scheme = this.isSecure ? "ircs" : "irc"; var obj = { host: this.hostname, scheme, isserver: true, port: this.port, needpass: Boolean(this.password), }; if (target) { obj.target = target; } if (flags) { for (var i = 0; i < flags.length; i++) { obj[flags[i]] = true; } } return constructIRCURL(obj); }; CIRCServer.prototype.getUser = function (nick) { var tnick = ":" + this.toLowerCase(nick); if (tnick in this.users) { return this.users[tnick]; } tnick = ":" + this.toLowerCase(fromUnicode(nick, this)); if (tnick in this.users) { return this.users[tnick]; } return null; }; CIRCServer.prototype.getChannel = function (name) { var tname = ":" + this.toLowerCase(name); if (tname in this.channels) { return this.channels[tname]; } tname = ":" + this.toLowerCase(fromUnicode(name, this)); if (tname in this.channels) { return this.channels[tname]; } return null; }; CIRCServer.prototype.connect = function () { if (this.connection != null) { throw Components.Exception( "Server already has a connection pending or established", Cr.NS_ERROR_FAILURE ); } var config = { isSecure: this.isSecure }; if (this.parent.PROXY_TYPE_OVERRIDE) { config.proxy = this.parent.PROXY_TYPE_OVERRIDE; } this.connection = new CBSConnection(); this.connection.connect(this.hostname, this.port, config, this); }; // This may be called synchronously or asynchronously by CBSConnection.connect. CIRCServer.prototype.onSocketConnection = function ( host, port, config, exception ) { if (this.parent.state == NET_CANCELLING) { this.connection.disconnect(); this.connection = null; this.parent.state = NET_OFFLINE; var ev = new CEvent("network", "error", this.parent, "onError"); ev.server = this; ev.debug = "Connect sequence was canceled."; ev.errorCode = JSIRC_ERR_CANCELLED; this.parent.eventPump.addEvent(ev); } else if (!exception) { var ev = new CEvent("server", "connect", this, "onConnect"); ev.server = this; this.parent.eventPump.addEvent(ev); this.isConnected = true; this.connection.startAsyncRead(this); } else { var ev = new CEvent("server", "disconnect", this, "onDisconnect"); ev.server = this; ev.reason = "error"; ev.exception = exception; ev.disconnectStatus = NS_ERROR_ABORT; this.parent.eventPump.addEvent(ev); } }; /* * What to do when the client connects to it's primary server */ CIRCServer.prototype.onConnect = function (e) { this.parent.primServ = e.server; this.sendData("CAP LS 302\n"); this.pendingCapNegotiation = true; this.caps = {}; this.capvals = {}; this.login( this.parent.INITIAL_NICK, this.parent.INITIAL_NAME, this.parent.INITIAL_DESC ); return true; }; CIRCServer.prototype.onStreamDataAvailable = function ( request, inStream, sourceOffset, count ) { var ev = new CEvent("server", "data-available", this, "onDataAvailable"); ev.line = this.connection.readData(0, count); /* route data-available as we get it. the data-available handler does * not do much, so we can probably get away with this without starving * the UI even under heavy input traffic. */ this.parent.eventPump.routeEvent(ev); }; CIRCServer.prototype.onStreamClose = function (status) { var ev = new CEvent("server", "disconnect", this, "onDisconnect"); ev.server = this; ev.disconnectStatus = status; if (ev.disconnectStatus == NS_ERROR_BINDING_ABORTED) { ev.disconnectStatus = NS_ERROR_ABORT; } this.parent.eventPump.addEvent(ev); }; CIRCServer.prototype.flushSendQueue = function () { this.sendQueue.length = 0; dd("sendQueue flushed."); return true; }; CIRCServer.prototype.login = function (nick, name, desc) { nick = nick.replace(/ /g, "_"); name = name.replace(/ /g, "_"); if (!nick) { nick = "nick"; } if (!name) { name = nick; } if (!desc) { desc = nick; } this.me = new CIRCUser(this, nick, null, name); if (this.password) { this.sendData("PASS " + this.password + "\n"); } this.changeNick(this.me.unicodeName); this.sendData("USER " + name + " * * :" + fromUnicode(desc, this) + "\n"); }; CIRCServer.prototype.logout = function (reason) { if (reason == null || typeof reason == "undefined") { reason = this.DEFAULT_REASON; } this.quitting = true; this.connection.sendData("QUIT :" + fromUnicode(reason, this.parent) + "\n"); this.connection.disconnect(); }; CIRCServer.prototype.sendAuthResponse = function (resp) { // Encode the response and break into 400-byte parts. var resp = btoa(resp); var part = null; var n = 0; do { part = resp.substring(0, 400); n = part.length; resp = resp.substring(400); this.sendData("AUTHENTICATE " + part + "\n"); } while (resp.length > 0); // Send empty auth response if last part was exactly 400 bytes long. if (n == 400) { this.sendData("AUTHENTICATE +\n"); } }; CIRCServer.prototype.sendAuthAbort = function () { // Abort an in-progress SASL authentication. this.sendData("AUTHENTICATE *\n"); }; CIRCServer.prototype.sendMonitorList = function (nicks, isAdd) { if (!nicks.length) { return; } var prefix; if (isAdd) { prefix = "MONITOR + "; } else { prefix = "MONITOR - "; } /* Send monitor list updates in chunks less than maxLineLength in size. */ var nicks_string = nicks.join(","); while (nicks_string.length > this.maxLineLength) { var nicks_part = nicks_string.substring(0, this.maxLineLength); var i = nicks_part.lastIndexOf(","); nicks_part = nicks_string.substring(0, i); nicks_string = nicks_string.substring(i + 1); this.sendData(prefix + nicks_part + "\n"); } this.sendData(prefix + nicks_string + "\n"); }; CIRCServer.prototype.addTarget = function (name) { if (this.channelTypes.includes(name[0])) { return this.addChannel(name); } return this.addUser(name); }; CIRCServer.prototype.addChannel = function (unicodeName, charset) { return new CIRCChannel(this, unicodeName, fromUnicode(unicodeName, charset)); }; CIRCServer.prototype.addUser = function (unicodeName, name, host) { return new CIRCUser(this, unicodeName, null, name, host); }; CIRCServer.prototype.getChannelsLength = function () { var i = 0; for (var p in this.channels) { i++; } return i; }; CIRCServer.prototype.getUsersLength = function () { var i = 0; for (var p in this.users) { i++; } return i; }; CIRCServer.prototype.sendData = function (msg) { this.queuedSendData(msg); }; CIRCServer.prototype.queuedSendData = function (msg) { if (this.sendQueue.length == 0) { this.parent.eventPump.addEvent( new CEvent("server", "senddata", this, "onSendData") ); } this.sendQueue.unshift(new String(msg)); }; // Utility method for splitting large lines prior to sending. CIRCServer.prototype.splitLinesForSending = function (line, prettyWrap) { let lines = String(line).split("\n"); let realLines = []; for (let i = 0; i < lines.length; i++) { if (lines[i]) { while (lines[i].length > this.maxLineLength) { var extraLine = lines[i].substr(0, this.maxLineLength - 5); var pos = extraLine.lastIndexOf(" "); if (pos >= 0 && pos >= this.maxLineLength - 15) { // Smart-split. extraLine = lines[i].substr(0, pos); lines[i] = lines[i].substr(extraLine.length + 1); if (prettyWrap) { extraLine += "..."; lines[i] = "..." + lines[i]; } } else { // Dumb-split. extraLine = lines[i].substr(0, this.maxLineLength); lines[i] = lines[i].substr(extraLine.length); } realLines.push(extraLine); } realLines.push(lines[i]); } } return realLines; }; CIRCServer.prototype.messageTo = function (code, target, msg, ctcpCode) { let lines = this.splitLinesForSending(msg, true); let i = 0; let pfx = ""; let sfx = ""; if (ctcpCode) { pfx = "\01" + ctcpCode; sfx = "\01"; } // We may have no message at all with CTCP commands. if (!lines.length && ctcpCode) { lines.push(""); } for (i in lines) { if (lines[i] != "" || ctcpCode) { var line = code + " " + target + " :" + pfx; if (lines[i] != "") { if (ctcpCode) { line += " "; } line += lines[i] + sfx; } else { line += sfx; } //dd ("-*- irc sending '" + line + "'"); this.sendData(line + "\n"); } } }; CIRCServer.prototype.sayTo = function (target, msg) { this.messageTo("PRIVMSG", target, msg); }; CIRCServer.prototype.noticeTo = function (target, msg) { this.messageTo("NOTICE", target, msg); }; CIRCServer.prototype.actTo = function (target, msg) { this.messageTo("PRIVMSG", target, msg, "ACTION"); }; CIRCServer.prototype.ctcpTo = function (target, code, msg, method) { msg = msg || ""; method = method || "PRIVMSG"; code = code.toUpperCase(); if (code == "PING" && !msg) { msg = Number(new Date()); } this.messageTo(method, target, msg, code); }; CIRCServer.prototype.changeNick = function (newNick) { this.sendData("NICK " + fromUnicode(newNick, this) + "\n"); }; CIRCServer.prototype.updateLagTimer = function () { this.connection.sendData("PING :LAGTIMER\n"); this.lastPing = this.lastPingSent = new Date(); }; CIRCServer.prototype.userhost = function (target) { this.sendData("USERHOST " + fromUnicode(target, this) + "\n"); }; CIRCServer.prototype.userip = function (target) { this.sendData("USERIP " + fromUnicode(target, this) + "\n"); }; CIRCServer.prototype.who = function (target) { this.sendData("WHO " + fromUnicode(target, this) + "\n"); }; /** * Abstracts the whois command. * * @param target intended user(s). */ CIRCServer.prototype.whois = function (target) { this.sendData("WHOIS " + fromUnicode(target, this) + "\n"); }; CIRCServer.prototype.whowas = function (target, limit) { if (typeof limit == "undefined") { limit = 1; } else if (limit == 0) { limit = ""; } this.sendData("WHOWAS " + fromUnicode(target, this) + " " + limit + "\n"); }; CIRCServer.prototype.onDisconnect = function (e) { function stateChangeFn(network, state) { network.state = state; } function delayedConnectFn(network) { network.delayedConnect(); } /* If we're not connected and get this, it means we have almost certainly * encountered a read or write error on the socket post-disconnect. There's * no point propagating this any further, as we've already notified the * user of the disconnect (with the right error). */ if (!this.isConnected) { return; } let errorClass = 0; // Check if e.disconnectStatus is within the valid range for NSS Errors. if (e.disconnectStatus >= 8192 && e.disconnectStatus < 20480) { errorClass = Cc["@mozilla.org/nss_errors_service;1"] .getService(Ci.nsINSSErrorsService) .getErrorClass(e.disconnectStatus); } // Don't reconnect from a certificate error. let badCert = errorClass == Ci.nsINSSErrorsService.ERROR_CLASS_BAD_CERT; // Don't reconnect if our connection was aborted. let wasAborted = e.disconnectStatus == NS_ERROR_ABORT; let dontReconnect = badCert || wasAborted; if ( (this.parent.state == NET_CONNECTING && !dontReconnect) || /* fell off while connecting, try again */ (this.parent.primServ == this && this.parent.state == NET_ONLINE && !("quitting" in this) && this.parent.stayingPower && !dontReconnect) ) { /* fell off primary server, reconnect to any host in the serverList */ setTimeout(delayedConnectFn, 0, this.parent); } else { setTimeout(stateChangeFn, 0, this.parent, NET_OFFLINE); } e.server = this; e.set = "network"; e.destObject = this.parent; e.quitting = this.quitting; for (var c in this.channels) { this.channels[c].users = {}; this.channels[c].active = false; } if (this.isStartTLS) { this.isSecure = false; delete this.isStartTLS; } delete this.batches; this.connection = null; this.isConnected = false; delete this.quitting; }; CIRCServer.prototype.onSendData = function (e) { if (!this.isConnected || this.parent.state == NET_CANCELLING) { dd("Can't send to disconnected socket"); this.flushSendQueue(); return false; } var d = new Date(); // Wheee, some sanity checking! (there's been at least one case of lastSend // ending up in the *future* at this point, which kinda busts things) if (this.lastSend > d) { this.lastSend = 0; } if (d - this.lastSend >= this.MS_BETWEEN_SENDS && this.sendQueue.length > 0) { var s = this.sendQueue.pop(); if (s) { try { this.connection.sendData(s); } catch (ex) { dd("Exception in queued send: " + ex); this.flushSendQueue(); var ev = new CEvent("server", "disconnect", this, "onDisconnect"); ev.server = this; ev.reason = "error"; ev.exception = ex; ev.disconnectStatus = NS_ERROR_ABORT; this.parent.eventPump.addEvent(ev); return false; } this.lastSend = d; } } else { this.parent.eventPump.addEvent(new CEvent("event-pump", "yield", null, "")); } if (this.sendQueue.length > 0) { this.parent.eventPump.addEvent( new CEvent("server", "senddata", this, "onSendData") ); } return true; }; CIRCServer.prototype.onPoll = function (e) { var lines; var ex; var ev; try { if (this.parent.state != NET_CANCELLING) { line = this.connection.readData(this.READ_TIMEOUT); } } catch (ex) { dd("*** Caught exception " + ex + " reading from server " + this.hostname); ev = new CEvent("server", "disconnect", this, "onDisconnect"); ev.server = this; ev.reason = "error"; ev.exception = ex; ev.disconnectStatus = NS_ERROR_ABORT; this.parent.eventPump.addEvent(ev); return false; } this.parent.eventPump.addEvent(new CEvent("server", "poll", this, "onPoll")); if (line) { ev = new CEvent("server", "data-available", this, "onDataAvailable"); ev.line = line; this.parent.eventPump.routeEvent(ev); } return true; }; CIRCServer.prototype.onDataAvailable = function (e) { var line = e.line; if (line == "") { return false; } var incomplete = line[line.length - 1] != "\n"; var lines = line.split("\n"); if (this.savedLine) { lines[0] = this.savedLine + lines[0]; this.savedLine = ""; } if (incomplete) { this.savedLine = lines.pop(); } for (var i in lines) { var ev = new CEvent("server", "rawdata", this, "onRawData"); ev.data = lines[i].replace(/\r/g, ""); if (ev.data) { if (ev.data.match(/^(?::[^ ]+ )?(?:32[123]|352|354|315) /i)) { this.parent.eventPump.addBulkEvent(ev); } else { this.parent.eventPump.addEvent(ev); } } } return true; }; /* * onRawData begins shaping the event by parsing the IRC message at it's * simplest level. After onRawData, the event will have the following * properties: * name value * * set............"server" * type..........."parsedata" * destMethod....."onParsedData" * destObject.....server (this) * server.........server (this) * connection.....CBSConnection (this.connection) * source.........the <prefix> of the message (if it exists) * user...........user object initialized with data from the message <prefix> * params.........array containing the parameters of the message * code...........the first parameter (most messages have this) * * See Section 2.3.1 of RFC 1459 for details on <prefix>, <middle> and * <trailing> tokens. */ CIRCServer.prototype.onRawData = function (e) { var ary; var l = e.data; if (l.length == 0) { dd("empty line on onRawData?"); return false; } if (l[0] == "@") { e.tagdata = l.substring(0, l.indexOf(" ")); e.tags = this.decodeTagData(e.tagdata); l = l.substring(l.indexOf(" ") + 1); } else { e.tagdata = {}; e.tags = {}; } if (l[0] == ":") { // Must split only on REAL spaces here, not just any old whitespace. ary = l.match(/:([^ ]+) +(.*)/); e.source = ary[1]; l = ary[2]; ary = e.source.match(/([^ ]+)!([^ ]+)@(.*)/); if (ary) { e.user = new CIRCUser(this, null, ary[1], ary[2], ary[3]); } else { ary = e.source.match(/([^ ]+)@(.*)/); if (ary) { e.user = new CIRCUser(this, null, ary[1], null, ary[2]); } else { ary = e.source.match(/([^ ]+)!(.*)/); if (ary) { e.user = new CIRCUser(this, null, ary[1], ary[2], null); } } } } if ("user" in e && e.user && e.tags.account) { e.user.account = e.tags.account; } e.ignored = false; if ("user" in e && e.user && "ignoreList" in this.parent) { // Assumption: if "ignoreList" is in this.parent, we assume that: // a) it's an array. // b) ignoreMaskCache also exists, and // c) it's an array too. if (!(e.source in this.parent.ignoreMaskCache)) { for (var m in this.parent.ignoreList) { if (hostmaskMatches(e.user, this.parent.ignoreList[m])) { e.ignored = true; break; } } /* Save this exact source in the cache, with results of tests. */ this.parent.ignoreMaskCache[e.source] = e.ignored; } else { e.ignored = this.parent.ignoreMaskCache[e.source]; } } e.server = this; var sep = l.indexOf(" :"); if (sep != -1) { /* <trailing> param, if there is one */ var trail = l.substr(sep + 2, l.length); e.params = l.substr(0, sep).split(/ +/); e.params[e.params.length] = trail; } else { e.params = l.split(/ +/); } e.decodeParam = decodeParam; e.code = e.params[0].toUpperCase(); // Ignore all private (inc. channel) messages, notices and invites here. if ( e.ignored && (e.code == "PRIVMSG" || e.code == "NOTICE" || e.code == "INVITE" || e.code == "TAGMSG") ) { return true; } // If the message is part of a batch, store it for later. if (this.batches && e.tags.batch && e.code != "BATCH") { var reftag = e.tags.batch; // Check if the batch is already open. // If not, ignore the incoming message. if (this.batches[reftag]) { this.batches[reftag].messages.push(e); } return false; } e.type = "parseddata"; e.destObject = this; e.destMethod = "onParsedData"; return true; }; /* * onParsedData forwards to next event, based on |e.code| */ CIRCServer.prototype.onParsedData = function (e) { e.type = this.toLowerCase(e.code); if (!e.code[0]) { dd(dumpObjectTree(e)); return false; } e.destMethod = "on" + e.code[0].toUpperCase() + e.code.substr(1, e.code.length).toLowerCase(); if (typeof this[e.destMethod] == "function") { e.destObject = this; } else if (typeof this.onUnknown == "function") { e.destMethod = "onUnknown"; } else if (typeof this.parent[e.destMethod] == "function") { e.set = "network"; e.destObject = this.parent; } else { e.set = "network"; e.destObject = this.parent; e.destMethod = "onUnknown"; } return true; }; /* User changed topic */ CIRCServer.prototype.onTopic = function (e) { e.channel = new CIRCChannel(this, null, e.params[1]); e.channel.topicBy = e.user.unicodeName; e.channel.topicDate = new Date(); e.channel.topic = toUnicode(e.params[2], e.channel); e.destObject = e.channel; e.set = "channel"; return true; }; /* Successful login */ CIRCServer.prototype.on001 = function (e) { this.parent.connectAttempt = 0; this.parent.connectCandidate = 0; //Mark capability negotiation as finished, if we haven't already. delete this.parent.pendingCapNegotiation; this.parent.state = NET_ONLINE; // nextHost is incremented after picking a server. Push it back here. this.parent.nextHost--; /* servers won't send a nick change notification if user was forced * to change nick while logging in (eg. nick already in use.) We need * to verify here that what the server thinks our name is, matches what * we think it is. If not, the server wins. */ if (e.params[1] != e.server.me.encodedName) { renameProperty( e.server.users, e.server.me.collectionKey, ":" + this.toLowerCase(e.params[1]) ); e.server.me.changeNick(toUnicode(e.params[1], this)); } /* Set up supports defaults here. * This is so that we don't waste /huge/ amounts of RAM for the network's * servers just because we know about them. Until we connect, that is. * These defaults are taken from the draft 005 RPL_ISUPPORTS here: * http://www.ietf.org/internet-drafts/draft-brocklesby-irc-isupport-02.txt */ this.supports = {}; this.supports.modes = 3; this.supports.maxchannels = 10; this.supports.nicklen = 9; this.supports.casemapping = "rfc1459"; this.supports.channellen = 200; this.supports.chidlen = 5; /* Make sure it's possible to tell if we've actually got a 005 message. */ this.supports.rpl_isupport = false; this.channelTypes = ["#", "&"]; /* This next one isn't in the isupport draft, but instead is defaulting to * the codes we understand. It should be noted, some servers include the * mode characters (o, h, v) in the 'a' list, although the draft spec says * they should be treated as type 'b'. Luckly, in practise this doesn't * matter, since both 'a' and 'b' types always take a parameter in the * MODE message, and parsing is not affected. */ this.channelModes = { a: ["b"], b: ["k"], c: ["l"], d: ["i", "m", "n", "p", "s", "t"], }; // Default to support of v/+ and o/@ only. this.userModes = [ { mode: "o", symbol: "@" }, { mode: "v", symbol: "+" }, ]; // Assume the server supports no extra interesting commands. this.servCmds = {}; if (this.parent.INITIAL_UMODE) { e.server.sendData( "MODE " + e.server.me.encodedName + " :" + this.parent.INITIAL_UMODE + "\n" ); } this.parent.users = this.users; e.destObject = this.parent; e.set = "network"; }; /* server features */ CIRCServer.prototype.on005 = function (e) { var oldCaseMapping = this.supports.casemapping; /* Drop params 0 and 1. */ for (var i = 2; i < e.params.length; i++) { var itemStr = e.params[i]; /* Items may be of the forms: * NAME * -NAME * NAME=value * Value may be empty on occasion. * No value allowed for -NAME items. */ var item = itemStr.match(/^(-?)([A-Z]+)(=(.*))?$/i); if (!item) { continue; } var name = item[2].toLowerCase(); if ("3" in item && item[3]) { // And other items are stored as-is, though numeric items // get special treatment to make our life easier later. if ("4" in item && item[4].match(/^\d+$/)) { this.supports[name] = Number(item[4]); } else { this.supports[name] = item[4]; } } else { // Boolean-type items stored as 'true'. this.supports[name] = !("1" in item && item[1] == "-"); } } // Update all users and channels if the casemapping changed. if (this.supports.casemapping != oldCaseMapping) { this.renameProperties(this.users, null); this.renameProperties(this.channels, "users"); } // Supported 'special' items: // CHANTYPES (--> channelTypes{}), // PREFIX (--> userModes[{mode,symbol}]), // CHANMODES (--> channelModes{a:[], b:[], c:[], d:[]}). var m; if ("chantypes" in this.supports) { this.channelTypes = []; for (m = 0; m < this.supports.chantypes.length; m++) { this.channelTypes.push(this.supports.chantypes[m]); } } if ("prefix" in this.supports) { var mlist = this.supports.prefix.match(/^\((.*)\)(.*)$/i); if (!mlist || mlist[1].length != mlist[2].length) { dd("** Malformed PREFIX entry in 005 SUPPORTS message **"); } else { this.userModes = []; for (m = 0; m < mlist[1].length; m++) { this.userModes.push({ mode: mlist[1][m], symbol: mlist[2][m] }); } } } if ("chanmodes" in this.supports) { var cmlist = this.supports.chanmodes.split(/,/); if (!cmlist || cmlist.length < 4) { dd("** Malformed CHANMODES entry in 005 SUPPORTS message **"); } else { // 4 types - list, set-unset-param, set-only-param, flag. this.channelModes = { a: cmlist[0].split(""), b: cmlist[1].split(""), c: cmlist[2].split(""), d: cmlist[3].split(""), }; } } if ("cmds" in this.supports) { // Map this.supports.cmds [comma-list] into this.servCmds [props]. var cmdlist = this.supports.cmds.split(/,/); for (var i = 0; i < cmdlist.length; i++) { this.servCmds[cmdlist[i].toLowerCase()] = true; } } this.supports.rpl_isupport = true; e.destObject = this.parent; e.set = "network"; return true; }; /* users */ CIRCServer.prototype.on251 = function (e) { // 251 is the first message we get after 005, so it's now safe to do // things that might depend upon server features. if ("namesx" in this.supports && this.supports.namesx) { // "multi-prefix" is the same as "namesx" but PROTOCTL doesn't reply. this.caps["multi-prefix"] = true; this.sendData("PROTOCTL NAMESX\n"); } if (this.parent.INITIAL_CHANNEL) { this.parent.primChan = this.addChannel(this.parent.INITIAL_CHANNEL); this.parent.primChan.join(); } e.destObject = this.parent; e.set = "network"; }; /* channels */ CIRCServer.prototype.on254 = function (e) { this.channelCount = e.params[2]; e.destObject = this.parent; e.set = "network"; }; /* user away message */ CIRCServer.prototype.on301 = function (e) { e.user = new CIRCUser(this, null, e.params[2]); e.user.awayMessage = e.decodeParam(3, e.user); e.destObject = this.parent; e.set = "network"; }; /* whois name */ CIRCServer.prototype.on311 = function (e) { e.user = new CIRCUser(this, null, e.params[2], e.params[3], e.params[4]); e.user.desc = e.decodeParam(6, e.user); e.destObject = this.parent; e.set = "network"; this.pendingWhoisLines = e.user; }; /* whois server */ CIRCServer.prototype.on312 = function (e) { e.user = new CIRCUser(this, null, e.params[2]); e.user.connectionHost = e.params[3]; e.destObject = this.parent; e.set = "network"; }; /* whois idle time */ CIRCServer.prototype.on317 = function (e) { e.user = new CIRCUser(this, null, e.params[2]); e.user.idleSeconds = e.params[3]; e.destObject = this.parent; e.set = "network"; }; /* whois channel list */ CIRCServer.prototype.on319 = function (e) { e.user = new CIRCUser(this, null, e.params[2]); e.destObject = this.parent; e.set = "network"; }; /* end of whois */ CIRCServer.prototype.on318 = function (e) { e.user = new CIRCUser(this, null, e.params[2]); if ("pendingWhoisLines" in this) { delete this.pendingWhoisLines; } e.destObject = this.parent; e.set = "network"; }; /* ircu's 330 numeric ("X is logged in as Y") */ CIRCServer.prototype.on330 = function (e) { e.user = new CIRCUser(this, null, e.params[2]); var account = e.params[3] == "*" ? null : e.params[3]; this.users[e.user.collectionKey].account = account; e.destObject = this.parent; e.set = "network"; }; /* TOPIC reply - no topic set */ CIRCServer.prototype.on331 = function (e) { e.channel = new CIRCChannel(this, null, e.params[2]); e.channel.topic = ""; e.destObject = e.channel; e.set = "channel"; return true; }; /* TOPIC reply - topic set */ CIRCServer.prototype.on332 = function (e) { e.channel = new CIRCChannel(this, null, e.params[2]); e.channel.topic = toUnicode(e.params[3], e.channel); e.destObject = e.channel; e.set = "channel"; return true; }; /* topic information */ CIRCServer.prototype.on333 = function (e) { e.channel = new CIRCChannel(this, null, e.params[2]); e.channel.topicBy = toUnicode(e.params[3], this); e.channel.topicDate = new Date(Number(e.params[4]) * 1000); e.destObject = e.channel; e.set = "channel"; return true; }; /* who reply */ CIRCServer.prototype.on352 = function (e) { e.userHasChanges = false; if (this.LIGHTWEIGHT_WHO) { e.user = new CIRCUser(this, null, e.params[6]); } else { e.user = new CIRCUser(this, null, e.params[6], e.params[3], e.params[4]); e.user.connectionHost = e.params[5]; if (8 in e.params) { var ary = e.params[8].match(/(?:(\d+)\s)?(.*)/); e.user.hops = ary[1]; var desc = fromUnicode(ary[2], e.user); if (e.user.desc != desc) { e.userHasChanges = true; e.user.desc = desc; } } } var away = e.params[7][0] == "G"; if (e.user.isAway != away) { e.userHasChanges = true; e.user.isAway = away; } e.destObject = this.parent; e.set = "network"; return true; }; /* extended who reply */ CIRCServer.prototype.on354 = function (e) { // Discard if the type is not ours. if (e.params[2] != this.WHOX_TYPE) { return; } e.userHasChanges = false; if (this.LIGHTWEIGHT_WHO) { e.user = new CIRCUser(this, null, e.params[7]); } else { e.user = new CIRCUser(this, null, e.params[7], e.params[4], e.params[5]); e.user.connectionHost = e.params[6]; // Hops is a separate parameter in WHOX. e.user.hops = e.params[9]; var account = e.params[10] == "0" ? null : e.params[10]; e.user.account = account; if (11 in e.params) { var desc = e.decodeParam(11, e.user); if (e.user.desc != desc) { e.userHasChanges = true; e.user.desc = desc; } } } var away = e.params[8][0] == "G"; if (e.user.isAway != away) { e.userHasChanges = true; e.user.isAway = away; } e.destObject = this.parent; e.set = "network"; return true; }; /* end of who */ CIRCServer.prototype.on315 = function (e) { e.user = new CIRCUser(this, null, e.params[1]); e.destObject = this.parent; e.set = "network"; return true; }; /* names reply */ CIRCServer.prototype.on353 = function (e) { e.channel = new CIRCChannel(this, null, e.params[3]); if (e.channel.usersStable) { e.channel.users = {}; e.channel.usersStable = false; } e.destObject = e.channel; e.set = "channel"; var nicks = e.params[4].split(" "); var mList = this.userModes; for (var n in nicks) { var nick = nicks[n]; if (nick == "") { break; } var modes = []; var multiPrefix = ("namesx" in this.supports && this.supports.namesx) || ("multi-prefix" in this.caps && this.caps["multi-prefix"]); do { var found = false; for (var m in mList) { if (nick[0] == mList[m].symbol) { nick = nick.substr(1); modes.push(mList[m].mode); found = true; break; } } } while (found && multiPrefix); var ary = nick.match(/([^ ]+)!([^ ]+)@(.*)/); var user = null; var host = null; if (this.caps["userhost-in-names"] && ary) { nick = ary[1]; user = ary[2]; host = ary[3]; } new CIRCChanUser(e.channel, null, nick, modes, true, user, host); } return true; }; /* end of names */ CIRCServer.prototype.on366 = function (e) { e.channel = new CIRCChannel(this, null, e.params[2]); e.destObject = e.channel; e.set = "channel"; e.channel.usersStable = true; return true; }; /* channel time stamp? */ CIRCServer.prototype.on329 = function (e) { e.channel = new CIRCChannel(this, null, e.params[2]); e.destObject = e.channel; e.set = "channel"; e.channel.timeStamp = new Date(Number(e.params[3]) * 1000); return true; }; /* channel mode reply */ CIRCServer.prototype.on324 = function (e) { e.channel = new CIRCChannel(this, null, e.params[2]); e.destObject = this; e.type = "chanmode"; e.destMethod = "onChanMode"; return true; }; /* channel ban entry */ CIRCServer.prototype.on367 = function (e) { e.channel = new CIRCChannel(this, null, e.params[2]); e.destObject = e.channel; e.set = "channel"; e.ban = e.params[3]; e.user = new CIRCUser(this, null, e.params[4]); e.banTime = new Date(Number(e.params[5]) * 1000); if (typeof e.channel.bans[e.ban] == "undefined") { e.channel.bans[e.ban] = { host: e.ban, user: e.user, time: e.banTime }; var ban_evt = new CEvent("channel", "ban", e.channel, "onBan"); ban_evt.tags = e.tags; ban_evt.channel = e.channel; ban_evt.ban = e.ban; ban_evt.source = e.user; this.parent.eventPump.addEvent(ban_evt); } return true; }; /* channel ban list end */ CIRCServer.prototype.on368 = function (e) { e.channel = new CIRCChannel(this, null, e.params[2]); e.destObject = e.channel; e.set = "channel"; /* This flag is cleared in a timeout (which occurs right after the current * message has been processed) so that the new event target (the channel) * will still have the flag set when it executes. */ if ("pendingBanList" in e.channel) { setTimeout(function () { delete e.channel.pendingBanList; }, 0); } return true; }; /* channel except entry */ CIRCServer.prototype.on348 = function (e) { e.channel = new CIRCChannel(this, null, e.params[2]); e.destObject = e.channel; e.set = "channel"; e.except = e.params[3]; e.user = new CIRCUser(this, null, e.params[4]); e.exceptTime = new Date(Number(e.params[5]) * 1000); if (typeof e.channel.excepts[e.except] == "undefined") { e.channel.excepts[e.except] = { host: e.except, user: e.user, time: e.exceptTime, }; } return true; }; /* channel except list end */ CIRCServer.prototype.on349 = function (e) { e.channel = new CIRCChannel(this, null, e.params[2]); e.destObject = e.channel; e.set = "channel"; if ("pendingExceptList" in e.channel) { setTimeout(function () { delete e.channel.pendingExceptList; }, 0); } return true; }; /* don't have operator perms */ CIRCServer.prototype.on482 = function (e) { e.channel = new CIRCChannel(this, null, e.params[2]); e.destObject = e.channel; e.set = "channel"; /* Some servers (e.g. Hybrid) don't let you get the except list without ops, * so we might be waiting for this list forever otherwise. */ if ("pendingExceptList" in e.channel) { setTimeout(function () { delete e.channel.pendingExceptList; }, 0); } return true; }; /* userhost reply */ CIRCServer.prototype.on302 = function (e) { var list = e.params[2].split(/\s+/); for (var i = 0; i < list.length; i++) { // <reply> ::= <nick>['*'] '=' <'+'|'-'><hostname> // '*' == IRCop. '+' == here, '-' == away. var data = list[i].match(/^(.*)(\*?)=([-+])(.*)@(.*)$/); if (data) { this.addUser(data[1], data[4], data[5]); } } e.destObject = this.parent; e.set = "network"; return true; }; /* CAP response */ CIRCServer.prototype.onCap = function (e) { // We expect some sort of identifier. if (e.params.length < 2) { return; } if (e.params[2] == "LS") { /* We're getting a list of all server capabilities. Set them all to * null (if they don't exist) to indicate we don't know if they're * enabled or not (but this will evaluate to false which matches that * capabilities are only enabled on request). */ var caps = e.params[3].split(/\s+/); var multiline = e.params[3] == "*"; if (multiline) { caps = e.params[4].split(/\s+/); } for (var i = 0; i < caps.length; i++) { var [cap, value] = caps[i].split(/=(.+)/); cap = cap.replace(/^-/, "").trim(); if (!(cap in this.caps)) { this.caps[cap] = null; } if (value) { this.capvals[cap] = value; } } // Don't do anything until the end of the response. if (multiline) { return true; } //Only request capabilities we support if we are connecting. if (this.pendingCapNegotiation) { // If we have an STS upgrade policy, immediately disconnect // and reconnect on the secure port. if ( this.parent.STS_MODULE.ENABLED && "sts" in this.caps && !this.isSecure ) { var policy = this.parent.STS_MODULE.parseParameters(this.capvals.sts); if (policy && policy.port) { e.stsUpgradePort = policy.port; e.destObject = this.parent; e.set = "network"; return false; } } // Request STARTTLS if we are configured to do so. if ( !this.isSecure && "tls" in this.caps && this.parent.UPGRADE_INSECURE ) { this.sendData("STARTTLS\n"); } var caps_req = JSIRCV3_SUPPORTED_CAPS.filter(i => i in this.caps); // Don't send requests for these caps. let caps_noreq = ["tls", "sts", "echo-message"]; if (!this.parent.USE_SASL) { caps_noreq.push("sasl"); } caps_req = caps_req.filter(i => !caps_noreq.includes(i)); if (caps_req.length > 0) { caps_req = caps_req.join(" "); e.server.sendData("CAP REQ :" + caps_req + "\n"); } else { e.server.sendData("CAP END\n"); delete this.pendingCapNegotiation; } } } else if (e.params[2] == "LIST") { /* Received list of enabled capabilities. Just use this as a sanity * check. */ var caps = e.params[3].trim().split(/\s+/); var multiline = e.params[3] == "*"; if (multiline) { caps = e.params[4].trim().split(/\s+/); } for (var i = 0; i < caps.length; i++) { this.caps[caps[i]] = true; } // Don't do anything until the end of the response. if (multiline) { return true; } } else if (e.params[2] == "ACK") { /* One or more capability changes have been successfully applied. An enabled * capability is just "cap" whilst a disabled capability is "-cap". */ var caps = e.params[3].trim().split(/\s+/); e.capsOn = []; e.capsOff = []; for (var i = 0; i < caps.length; i++) { var cap = caps[i].replace(/^-/, "").trim(); var enabled = caps[i][0] != "-"; if (enabled) { e.capsOn.push(cap); } else { e.capsOff.push(cap); } this.caps[cap] = enabled; } // Try SASL authentication if we are configured to do so. if (caps.includes("sasl")) { var ev = new CEvent("server", "sasl-start", this, "onSASLStart"); ev.server = this; if (this.capvals.sasl) { ev.mechs = this.capvals.sasl.toLowerCase().split(/,/); } ev.destObject = this.parent; this.parent.eventPump.routeEvent(ev); if (this.pendingCapNegotiation) { return true; } } if (this.pendingCapNegotiation) { e.server.sendData("CAP END\n"); delete this.pendingCapNegotiation; //Don't show the raw message while connecting. return true; } } else if (e.params[2] == "NAK") { // A capability change has failed. var caps = e.params[3].trim().split(/\s+/); e.caps = []; for (var i = 0; i < caps.length; i++) { var cap = caps[i].replace(/^-/, "").trim(); e.caps.push(cap); } if (this.pendingCapNegotiation) { e.server.sendData("CAP END\n"); delete this.pendingCapNegotiation; //Don't show the raw message while connecting. return true; } } else if (e.params[2] == "NEW") { // A capability is now available, so request it if we can. var caps = e.params[3].split(/\s+/); e.newcaps = []; for (var i = 0; i < caps.length; i++) { var [cap, value] = caps[i].split(/=(.+)/); cap = cap.trim(); this.caps[cap] = null; e.newcaps.push(cap); if (value) { this.capvals[cap] = value; } } var caps_req = JSIRCV3_SUPPORTED_CAPS.filter(i => i in e.newcaps); // Don't send requests for these caps. caps_noreq = ["tls", "sts", "sasl", "echo-message"]; caps_req = caps_req.filter(i => !caps_noreq.includes(i)); if (caps_req.length > 0) { caps_req = caps_req.join(" "); e.server.sendData("CAP REQ :" + caps_req + "\n"); } } else if (e.params[2] == "DEL") { // A capability is no longer available. var caps = e.params[3].split(/\s+/); var caps_nodel = ["sts"]; for (var i = 0; i < caps.length; i++) { var cap = caps[i].split(/=(.+)/)[0]; cap = cap.trim(); if (caps_nodel.includes(cap)) { continue; } this.caps[cap] = null; } } else { dd("Unknown CAP reply " + e.params[2]); } e.destObject = this.parent; e.set = "network"; }; /* BATCH start or end */ CIRCServer.prototype.onBatch = function (e) { // We should at least get a ref tag. if (e.params.length < 2) { return false; } e.reftag = e.params[1].substring(1); switch (e.params[1][0]) { case "+": e.starting = true; break; case "-": e.starting = false; break; default: // Invalid reference tag. return false; } var isPlayback = this.batches && this.batches[e.reftag] && this.batches[e.reftag].playback; if (!isPlayback) { if (e.starting) { // We're starting a batch, so we also need a type. if (e.params.length < 3) { return false; } if (!this.batches) { this.batches = {}; } // The batch object holds the messages queued up as part // of this batch, and a boolean value indicating whether // it is being played back. var newBatch = {}; newBatch.messages = [e]; newBatch.type = e.params[2].toUpperCase(); if (e.params[3] && e.params[3] in this.channels) { newBatch.destObject = this.channels[e.params[3]]; } else if (e.params[3] && e.params[3] in this.users) { newBatch.destObject = this.users[e.params[3]]; } else { newBatch.destObject = this.parent; } newBatch.playback = false; this.batches[e.reftag] = newBatch; } else { if (!this.batches[e.reftag]) { // Got a close tag without an open tag, so ignore it. return false; } var batch = this.batches[e.reftag]; // Closing the batch, prepare for playback. batch.messages.push(e); batch.playback = true; if (e.tags.batch) { // We are an inner batch. Append the message queue // to the outer batch's message queue. var parentRef = e.tags.batch; var parentMsgs = this.batches[parentRef].messages; parentMsgs = parentMsgs.concat(batch.messages); } else { // We are an outer batch. Playback! for (var i = 0; i < batch.messages.length; i++) { var ev = batch.messages[i]; ev.type = "parseddata"; ev.destObject = this; ev.destMethod = "onParsedData"; this.parent.eventPump.routeEvent(ev); } } } return false; } // Batch command is ready for handling. e.batchtype = this.batches[e.reftag].type; e.destObject = this.batches[e.reftag].destObject; if (e.destObject.TYPE == "CIRCChannel") { e.set = "channel"; } else { e.set = "network"; } if (!e.starting) { // If we've reached the end of a batch in playback, // do some cleanup. delete this.batches[e.reftag]; if (Object.entries(this.batches).length == 0) { delete this.batches; } } // Massage the batchtype into a method name for handlers: // netsplit - onNetsplitBatch // some-batch-type - onSomeBatchTypeBatch // example.com/example - onExampleComExampleBatch var batchCode = e.batchtype .split(/[\.\/-]/) .map(function (s) { return s[0].toUpperCase() + s.substr(1).toLowerCase(); }) .join(""); e.destMethod = "on" + batchCode + "Batch"; if (!e.destObject[e.destMethod]) { e.destMethod = "onUnknownBatch"; } }; /* SASL authentication responses */ /* Nick locked */ CIRCServer.prototype.on902 = /* Auth success */ CIRCServer.prototype.on903 = /* Auth failed */ CIRCServer.prototype.on904 = /* Command too long */ CIRCServer.prototype.on905 = /* Aborted */ CIRCServer.prototype.on906 = /* Already authenticated */ CIRCServer.prototype.on907 = /* Mechanisms */ CIRCServer.prototype.on908 = function (e) { if (this.pendingCapNegotiation) { delete this.pendingCapNegotiation; this.sendData("CAP END\n"); } if (e.code == "908") { // Update our list of SASL mechanics. this.capvals.sasl = e.params[2]; } e.destObject = this.parent; e.set = "network"; }; /* STARTTLS responses */ /* Success */ CIRCServer.prototype.on670 = function (e) { this.caps.tls = true; e.server.connection.startTLS(); e.server.isSecure = true; e.server.isStartTLS = true; e.destObject = this.parent; e.set = "network"; }; /* Failure */ CIRCServer.prototype.on691 = function (e) { this.caps.tls = false; e.destObject = this.parent; e.set = "network"; }; /* User away status changed */ CIRCServer.prototype.onAway = function (e) { e.user.isAway = !!e.params[1]; e.destObject = this.parent; e.set = "network"; }; /* User host changed */ CIRCServer.prototype.onChghost = function (e) { this.users[e.user.collectionKey].name = e.params[1]; this.users[e.user.collectionKey].host = e.params[2]; e.destObject = this.parent; e.set = "network"; }; /* user changed the mode */ CIRCServer.prototype.onMode = function (e) { e.destObject = this; /* modes are not allowed in +channels -> no need to test that here.. */ if (this.channelTypes.includes(e.params[1][0])) { e.channel = new CIRCChannel(this, null, e.params[1]); if ("user" in e && e.user) { e.user = new CIRCChanUser(e.channel, e.user.unicodeName); } e.type = "chanmode"; e.destMethod = "onChanMode"; } else { e.type = "usermode"; e.destMethod = "onUserMode"; } return true; }; CIRCServer.prototype.onUserMode = function (e) { e.user = new CIRCUser(this, null, e.params[1]); e.user.modestr = e.params[2]; e.destObject = this.parent; e.set = "network"; // usermode usually happens on connect, after the MOTD, so it's a good // place to kick off the lag timer. this.updateLagTimer(); return true; }; CIRCServer.prototype.onChanMode = function (e) { var modifier = ""; var params_eaten = 0; var BASE_PARAM; if (e.code.toUpperCase() == "MODE") { BASE_PARAM = 2; } else if (e.code == "324") { BASE_PARAM = 3; } else { dd("** INVALID CODE in ChanMode event **"); return false; } var mode_str = e.params[BASE_PARAM]; params_eaten++; e.modeStr = mode_str; e.usersAffected = []; var nick; var user; var umList = this.userModes; var cmList = this.channelModes; var modeMap = this.canonicalChanModes; var canonicalModeValue; for (var i = 0; i < mode_str.length; i++) { /* Take care of modifier first. */ if (mode_str[i] == "+" || mode_str[i] == "-") { modifier = mode_str[i]; continue; } var done = false; for (var m in umList) { if (mode_str[i] == umList[m].mode && modifier != "") { nick = e.params[BASE_PARAM + params_eaten]; user = new CIRCChanUser(e.channel, null, nick, [ modifier + umList[m].mode, ]); params_eaten++; e.usersAffected.push(user); done = true; break; } } if (done) { continue; } // Update legacy canonical modes if necessary. if (mode_str[i] in modeMap) { // Get the data in case we need it, but don't increment the counter. var datacounter = BASE_PARAM + params_eaten; var data = datacounter in e.params ? e.params[datacounter] : null; canonicalModeValue = modeMap[mode_str[i]].getValue(modifier, data); e.channel.mode[modeMap[mode_str[i]].name] = canonicalModeValue; } if (cmList.a.includes(mode_str[i])) { var data = e.params[BASE_PARAM + params_eaten++]; if (modifier == "+") { e.channel.mode.modeA[data] = true; } else if (data in e.channel.mode.modeA) { delete e.channel.mode.modeA[data]; } else { dd( "** Trying to remove channel mode '" + mode_str[i] + "'/'" + data + "' which does not exist in list." ); } } else if (cmList.b.includes(mode_str[i])) { var data = e.params[BASE_PARAM + params_eaten++]; if (modifier == "+") { e.channel.mode.modeB[mode_str[i]] = data; } else { // Save 'null' even though we have some data. e.channel.mode.modeB[mode_str[i]] = null; } } else if (cmList.c.includes(mode_str[i])) { if (modifier == "+") { var data = e.params[BASE_PARAM + params_eaten++]; e.channel.mode.modeC[mode_str[i]] = data; } else { e.channel.mode.modeC[mode_str[i]] = null; } } else if (cmList.d.includes(mode_str[i])) { e.channel.mode.modeD[mode_str[i]] = modifier == "+"; } else { dd("** UNKNOWN mode symbol '" + mode_str[i] + "' in ChanMode event **"); } } e.destObject = e.channel; e.set = "channel"; return true; }; CIRCServer.prototype.onNick = function (e) { var newNick = e.params[1]; var newKey = ":" + this.toLowerCase(newNick); var oldKey = e.user.collectionKey; var ev; renameProperty(this.users, oldKey, newKey); e.oldNick = e.user.unicodeName; e.user.changeNick(toUnicode(newNick, this)); for (var c in this.channels) { if ( this.channels[c].active && (oldKey in this.channels[c].users || e.user == this.me) ) { var cuser = this.channels[c].users[oldKey]; renameProperty(this.channels[c].users, oldKey, newKey); // User must be a channel user, update sort name for userlist, // before we route the event further: cuser.updateSortName(); ev = new CEvent("channel", "nick", this.channels[c], "onNick"); ev.tags = e.tags; ev.channel = this.channels[c]; ev.user = cuser; ev.server = this; ev.oldNick = e.oldNick; this.parent.eventPump.routeEvent(ev); } } if (e.user == this.me) { /* if it was me, tell the network about the nick change as well */ ev = new CEvent("network", "nick", this.parent, "onNick"); ev.tags = e.tags; ev.user = e.user; ev.server = this; ev.oldNick = e.oldNick; this.parent.eventPump.routeEvent(ev); } e.destObject = e.user; e.set = "user"; return true; }; CIRCServer.prototype.onQuit = function (e) { var reason = e.decodeParam(1); for (var c in e.server.channels) { if ( e.server.channels[c].active && e.user.collectionKey in e.server.channels[c].users ) { var ev = new CEvent("channel", "quit", e.server.channels[c], "onQuit"); ev.tags = e.tags; ev.user = e.server.channels[c].users[e.user.collectionKey]; ev.channel = e.server.channels[c]; ev.server = ev.channel.parent; ev.reason = reason; this.parent.eventPump.routeEvent(ev); delete e.server.channels[c].users[e.user.collectionKey]; } } this.users[e.user.collectionKey].lastQuitMessage = reason; this.users[e.user.collectionKey].lastQuitDate = new Date(); // 0 == prune onQuit. if (this.PRUNE_OLD_USERS == 0) { delete this.users[e.user.collectionKey]; } e.reason = reason; e.destObject = e.user; e.set = "user"; return true; }; CIRCServer.prototype.onPart = function (e) { e.channel = new CIRCChannel(this, null, e.params[1]); e.reason = e.params.length > 2 ? e.decodeParam(2, e.channel) : ""; e.user = new CIRCChanUser(e.channel, e.user.unicodeName); if (userIsMe(e.user)) { e.channel.active = false; e.channel.joined = false; } e.channel.removeUser(e.user.encodedName); e.destObject = e.channel; e.set = "channel"; return true; }; CIRCServer.prototype.onKick = function (e) { e.channel = new CIRCChannel(this, null, e.params[1]); e.lamer = new CIRCChanUser(e.channel, null, e.params[2]); delete e.channel.users[e.lamer.collectionKey]; if (userIsMe(e.lamer)) { e.channel.active = false; e.channel.joined = false; } e.reason = e.decodeParam(3, e.channel); e.destObject = e.channel; e.set = "channel"; return true; }; CIRCServer.prototype.onJoin = function (e) { e.channel = new CIRCChannel(this, null, e.params[1]); // Passing undefined here because CIRCChanUser doesn't like "null" e.user = new CIRCChanUser( e.channel, e.user.unicodeName, null, undefined, true ); if (e.params[2] && e.params[3]) { var account = e.params[2] == "*" ? null : e.params[2]; var desc = e.decodeParam([3], e.user); this.users[e.user.collectionKey].account = account; this.users[e.user.collectionKey].desc = desc; } if (userIsMe(e.user)) { var delayFn1 = function (t) { if (!e.channel.active) { return; } // Give us the channel mode! e.server.sendData("MODE " + e.channel.encodedName + "\n"); }; // Between 1s - 3s. setTimeout(delayFn1, 1000 + 2000 * Math.random(), this); var delayFn2 = function (t) { if (!e.channel.active) { return; } // Get a full list of bans and exceptions, if supported. if (t.channelModes.a.includes("b")) { e.server.sendData("MODE " + e.channel.encodedName + " +b\n"); e.channel.pendingBanList = true; } if (t.channelModes.a.includes("e")) { e.server.sendData("MODE " + e.channel.encodedName + " +e\n"); e.channel.pendingExceptList = true; } //If away-notify is active, query the list of users for away status. if (e.server.caps["away-notify"]) { // If the server supports extended who, use it. // This lets us initialize the account property. if (e.server.supports.whox) { e.server.who( e.channel.unicodeName + " %acdfhnrstu," + e.server.WHOX_TYPE ); } else { e.server.who(e.channel.unicodeName); } } }; // Between 10s - 20s. setTimeout(delayFn2, 10000 + 10000 * Math.random(), this); /* Clean up the topic, since servers don't always send RPL_NOTOPIC * (no topic set) when joining a channel without a topic. In fact, * the RFC even fails to mention sending a RPL_NOTOPIC after a join! */ e.channel.topic = ""; e.channel.topicBy = null; e.channel.topicDate = null; // And we're in! e.channel.active = true; e.channel.joined = true; } e.destObject = e.channel; e.set = "channel"; return true; }; CIRCServer.prototype.onAccount = function (e) { var account = e.params[1] == "*" ? null : e.params[1]; this.users[e.user.collectionKey].account = account; return true; }; CIRCServer.prototype.onPing = function (e) { /* non-queued send, so we can calcualte lag */ this.connection.sendData("PONG :" + e.params[1] + "\n"); this.updateLagTimer(); e.destObject = this.parent; e.set = "network"; return true; }; CIRCServer.prototype.onPong = function (e) { if (e.params[2] != "LAGTIMER") { return true; } if (this.lastPingSent) { this.lag = (new Date() - this.lastPingSent) / 1000; } this.lastPingSent = null; e.destObject = this.parent; e.set = "network"; return true; }; CIRCServer.prototype.onInvite = function (e) { e.channel = new CIRCChannel(this, null, e.params[2]); e.destObject = this.parent; e.set = "network"; }; CIRCServer.prototype.onNotice = CIRCServer.prototype.onPrivmsg = CIRCServer.prototype.onTagmsg = function (e) { var targetName = e.params[1]; if (this.userModes) { // Strip off one (and only one) user mode prefix. for (var i = 0; i < this.userModes.length; i++) { if (targetName[0] == this.userModes[i].symbol) { e.msgPrefix = this.userModes[i]; targetName = targetName.substr(1); break; } } } /* setting replyTo provides a standard place to find the target for */ /* replies associated with this event. */ if (this.channelTypes && this.channelTypes.includes(targetName[0])) { e.channel = new CIRCChannel(this, null, targetName); if ("user" in e) { e.user = new CIRCChanUser(e.channel, e.user.unicodeName); } e.replyTo = e.channel; e.set = "channel"; } else if (!("user" in e)) { e.set = "network"; e.destObject = this.parent; return true; } else { e.set = "user"; e.replyTo = e.user; /* send replies to the user who sent the message */ } /* The capability identify-msg adds a + or - in front the message to * indicate their network registration status. */ if ("identify-msg" in this.caps && this.caps["identify-msg"]) { e.identifyMsg = false; var flag = e.params[2].substring(0, 1); if (flag == "+") { e.identifyMsg = true; e.params[2] = e.params[2].substring(1); } else if (flag == "-") { e.params[2] = e.params[2].substring(1); } else { // Just print to console on failure - or we'd spam the user dd("Warning: IDENTIFY-MSG is on, but there's no message flags"); } } // TAGMSG doesn't have a message parameter, so just pass it on. if (e.code == "TAGMSG") { e.destObject = e.replyTo; return true; } if (e.params[2].search(/^\x01[^ ]+.*\x01$/) != -1) { if (e.code == "NOTICE") { e.type = "ctcp-reply"; e.destMethod = "onCTCPReply"; } // e.code == "PRIVMSG" else { e.type = "ctcp"; e.destMethod = "onCTCP"; } e.set = "server"; e.destObject = this; } else { e.msg = e.decodeParam(2, e.replyTo); e.destObject = e.replyTo; } return true; }; CIRCServer.prototype.onWallops = function (e) { if ("user" in e && e.user) { e.msg = e.decodeParam(1, e.user); e.replyTo = e.user; } else { e.msg = e.decodeParam(1); e.replyTo = this; } e.destObject = this.parent; e.set = "network"; return true; }; CIRCServer.prototype.onCTCPReply = function (e) { var ary = e.params[2].match(/^\x01([^ ]+) ?(.*)\x01$/i); if (ary == null) { return false; } e.CTCPData = ary[2] ? ary[2] : ""; e.CTCPCode = ary[1].toLowerCase(); e.type = "ctcp-reply-" + e.CTCPCode; e.destMethod = "onCTCPReply" + ary[1][0].toUpperCase() + ary[1].substr(1, ary[1].length).toLowerCase(); if (typeof this[e.destMethod] != "function") { /* if there's no place to land the event here, try to forward it */ e.destObject = this.parent; e.set = "network"; if (typeof e.destObject[e.destMethod] != "function") { /* if there's no place to forward it, send it to unknownCTCP */ e.type = "unk-ctcp-reply"; e.destMethod = "onUnknownCTCPReply"; if (e.destMethod in this) { e.set = "server"; e.destObject = this; } else { e.set = "network"; e.destObject = this.parent; } } } else { e.destObject = this; } return true; }; CIRCServer.prototype.onCTCP = function (e) { var ary = e.params[2].match(/^\x01([^ ]+) ?(.*)\x01$/i); if (ary == null) { return false; } e.CTCPData = ary[2] ? ary[2] : ""; e.CTCPCode = ary[1].toLowerCase(); if (e.CTCPCode.search(/^reply/i) == 0) { dd("dropping spoofed reply."); return false; } e.CTCPCode = toUnicode(e.CTCPCode, e.replyTo); e.CTCPData = toUnicode(e.CTCPData, e.replyTo); e.type = "ctcp-" + e.CTCPCode; e.destMethod = "onCTCP" + ary[1][0].toUpperCase() + ary[1].substr(1, ary[1].length).toLowerCase(); if (typeof this[e.destMethod] != "function") { /* if there's no place to land the event here, try to forward it */ e.destObject = e.replyTo; e.set = e.replyTo == e.user ? "user" : "channel"; if (typeof e.replyTo[e.destMethod] != "function") { /* if there's no place to forward it, send it to unknownCTCP */ e.type = "unk-ctcp"; e.destMethod = "onUnknownCTCP"; } } else { e.destObject = this; } var ev = new CEvent("server", "ctcp-receive", this, "onReceiveCTCP"); ev.tags = e.tags; ev.server = this; ev.CTCPCode = e.CTCPCode; ev.CTCPData = e.CTCPData; ev.type = e.type; ev.user = e.user; ev.destObject = this.parent; this.parent.eventPump.addEvent(ev); return true; }; CIRCServer.prototype.onCTCPClientinfo = function (e) { var clientinfo = []; if (e.CTCPData) { var cmdName = "onCTCP" + e.CTCPData[0].toUpperCase() + e.CTCPData.substr(1, e.CTCPData.length).toLowerCase(); var helpName = cmdName.replace(/^onCTCP/, "CTCPHelp"); // Check we support the command. if (cmdName in this) { // Do we have help for it? if (helpName in this) { var msg; if (typeof this[helpName] == "function") { msg = this[helpName](); } else { msg = this[helpName]; } e.user.ctcp("CLIENTINFO", msg, "NOTICE"); } else { e.user.ctcp( "CLIENTINFO", getMsg(MSG_ERR_NO_CTCP_HELP, e.CTCPData), "NOTICE" ); } } else { e.user.ctcp( "CLIENTINFO", getMsg(MSG_ERR_NO_CTCP_CMD, e.CTCPData), "NOTICE" ); } return true; } for (var fname in this) { var ary = fname.match(/^onCTCP(.+)/); if (ary && ary[1].search(/^Reply/) == -1) { clientinfo.push(ary[1].toUpperCase()); } } e.user.ctcp("CLIENTINFO", clientinfo.join(" "), "NOTICE"); return true; }; CIRCServer.prototype.onCTCPAction = function (e) { e.destObject = e.replyTo; e.set = e.replyTo == e.user ? "user" : "channel"; }; CIRCServer.prototype.onCTCPFinger = function (e) { e.user.ctcp("FINGER", this.parent.INITIAL_DESC, "NOTICE"); return true; }; CIRCServer.prototype.onCTCPTime = function (e) { e.user.ctcp("TIME", new Date(), "NOTICE"); return true; }; CIRCServer.prototype.onCTCPVersion = function (e) { var lines = e.server.VERSION_RPLY.split("\n"); for (var i in lines) { e.user.ctcp("VERSION", lines[i], "NOTICE"); } e.destObject = e.replyTo; e.set = e.replyTo == e.user ? "user" : "channel"; return true; }; CIRCServer.prototype.onCTCPSource = function (e) { e.user.ctcp("SOURCE", this.SOURCE_RPLY, "NOTICE"); return true; }; CIRCServer.prototype.onCTCPOs = function (e) { e.user.ctcp("OS", this.OS_RPLY, "NOTICE"); return true; }; CIRCServer.prototype.onCTCPHost = function (e) { e.user.ctcp("HOST", this.HOST_RPLY, "NOTICE"); return true; }; CIRCServer.prototype.onCTCPPing = function (e) { /* non-queued send */ this.connection.sendData( "NOTICE " + e.user.encodedName + " :\01PING " + e.CTCPData + "\01\n" ); e.destObject = e.replyTo; e.set = e.replyTo == e.user ? "user" : "channel"; return true; }; CIRCServer.prototype.onCTCPDcc = function (e) { var ary = e.CTCPData.match(/([^ ]+)? ?(.*)/); e.DCCData = ary[2]; e.type = "dcc-" + ary[1].toLowerCase(); e.destMethod = "onDCC" + ary[1][0].toUpperCase() + ary[1].substr(1, ary[1].length).toLowerCase(); if (typeof this[e.destMethod] != "function") { /* if there's no place to land the event here, try to forward it */ e.destObject = e.replyTo; e.set = e.replyTo == e.user ? "user" : "channel"; } else { e.destObject = this; } return true; }; CIRCServer.prototype.onDCCChat = function (e) { var ary = e.DCCData.match(/(chat) (\d+) (\d+)/i); if (ary == null) { return false; } e.id = ary[2]; // Longword --> dotted IP conversion. var host = Number(e.id); e.host = ((host >> 24) & 0xff) + "." + ((host >> 16) & 0xff) + "." + ((host >> 8) & 0xff) + "." + (host & 0xff); e.port = Number(ary[3]); e.destObject = e.replyTo; e.set = e.replyTo == e.user ? "user" : "channel"; return true; }; CIRCServer.prototype.onDCCSend = function (e) { var ary = e.DCCData.match(/([^ ]+) (\d+) (\d+) (\d+)/); /* Just for mIRC: filenames with spaces may be enclosed in double-quotes. * (though by default it replaces spaces with underscores, but we might as * well cope). */ if (ary[1][0] == '"' || ary[1][ary[1].length - 1] == '"') { ary = e.DCCData.match(/"(.+)" (\d+) (\d+) (\d+)/); } if (ary == null) { return false; } e.file = ary[1]; e.id = ary[2]; // Longword --> dotted IP conversion. var host = Number(e.id); e.host = ((host >> 24) & 0xff) + "." + ((host >> 16) & 0xff) + "." + ((host >> 8) & 0xff) + "." + (host & 0xff); e.port = Number(ary[3]); e.size = Number(ary[4]); e.destObject = e.replyTo; e.set = e.replyTo == e.user ? "user" : "channel"; return true; }; function CIRCChannel(parent, unicodeName, encodedName) { // Both unicodeName and encodedName are optional, but at least one must be // present. if (!encodedName && !unicodeName) { throw Components.Exception( "Hey! Come on, I need either an encoded or a Unicode name.", Cr.NS_ERROR_INVALID_ARG ); } if (!encodedName) { encodedName = fromUnicode(unicodeName, parent); } let collectionKey = ":" + parent.toLowerCase(encodedName); if (collectionKey in parent.channels) { return parent.channels[collectionKey]; } this.parent = parent; this.encodedName = encodedName; this.canonicalName = collectionKey.substr(1); this.collectionKey = collectionKey; this.unicodeName = unicodeName || toUnicode(encodedName, this); this.viewName = this.unicodeName; this.users = {}; this.bans = {}; this.excepts = {}; this.mode = new CIRCChanMode(this); this.usersStable = true; /* These next two flags represent a subtle difference in state: * active - in the channel, from the server's point of view. * joined - in the channel, from the user's point of view. * e.g. parting the channel clears both, but being disconnected only * clears |active| - the user still wants to be in the channel, even * though they aren't physically able to until we've reconnected. */ this.active = false; this.joined = false; this.parent.channels[this.collectionKey] = this; if ("onInit" in this) { this.onInit(); } return this; } CIRCChannel.prototype.TYPE = "IRCChannel"; CIRCChannel.prototype.topic = ""; // Returns the IRC URL representation of this channel. CIRCChannel.prototype.getURL = function () { var target = this.encodedName; var flags = this.mode.key ? ["needkey"] : []; if ( target[0] == "#" && target.length > 1 && !this.parent.channelTypes.includes(target[1]) ) { /* First character is "#" (which we're allowed to omit), and the * following character is NOT a valid prefix, so it's safe to remove. */ target = target.substr(1); } return this.parent.parent.getURL(target, flags); }; CIRCChannel.prototype.rehome = function (newParent) { delete this.parent.channels[this.collectionKey]; this.parent = newParent; this.parent.channels[this.collectionKey] = this; }; CIRCChannel.prototype.addUser = function (unicodeName, modes) { return new CIRCChanUser(this, unicodeName, null, modes); }; CIRCChannel.prototype.getUser = function (nick) { // Try assuming it's an encodedName first. let tnick = ":" + this.parent.toLowerCase(nick); if (tnick in this.users) { return this.users[tnick]; } // Ok, failed, so try assuming it's a unicodeName. tnick = ":" + this.parent.toLowerCase(fromUnicode(nick, this.parent)); if (tnick in this.users) { return this.users[tnick]; } return null; }; CIRCChannel.prototype.removeUser = function (nick) { // Try assuming it's an encodedName first. let key = ":" + this.parent.toLowerCase(nick); if (key in this.users) { delete this.users[key]; } // see ya // Ok, failed, so try assuming it's a unicodeName. key = ":" + this.parent.toLowerCase(fromUnicode(nick, this.parent)); if (key in this.users) { delete this.users[key]; } }; CIRCChannel.prototype.getUsersLength = function (mode) { var i = 0; var p; this.opCount = 0; this.halfopCount = 0; this.voiceCount = 0; if (typeof mode == "undefined") { for (p in this.users) { if (this.users[p].isOp) { this.opCount++; } if (this.users[p].isHalfOp) { this.halfopCount++; } if (this.users[p].isVoice) { this.voiceCount++; } i++; } } else { for (p in this.users) { if (this.users[p].modes.includes(mode)) { i++; } } } return i; }; CIRCChannel.prototype.iAmOp = function () { return this.active && this.users[this.parent.me.collectionKey].isOp; }; CIRCChannel.prototype.iAmHalfOp = function () { return this.active && this.users[this.parent.me.collectionKey].isHalfOp; }; CIRCChannel.prototype.iAmVoice = function () { return this.active && this.users[this.parent.me.collectionKey].isVoice; }; CIRCChannel.prototype.setTopic = function (str) { this.parent.sendData( "TOPIC " + this.encodedName + " :" + fromUnicode(str, this) + "\n" ); }; CIRCChannel.prototype.say = function (msg) { this.parent.sayTo(this.encodedName, fromUnicode(msg, this)); }; CIRCChannel.prototype.act = function (msg) { this.parent.actTo(this.encodedName, fromUnicode(msg, this)); }; CIRCChannel.prototype.notice = function (msg) { this.parent.noticeTo(this.encodedName, fromUnicode(msg, this)); }; CIRCChannel.prototype.ctcp = function (code, msg, type) { msg = msg || ""; type = type || "PRIVMSG"; this.parent.ctcpTo( this.encodedName, fromUnicode(code, this), fromUnicode(msg, this), type ); }; CIRCChannel.prototype.join = function (key) { if (!key) { key = ""; } this.parent.sendData("JOIN " + this.encodedName + " " + key + "\n"); return true; }; CIRCChannel.prototype.part = function (reason) { if (!reason) { reason = ""; } this.parent.sendData( "PART " + this.encodedName + " :" + fromUnicode(reason, this) + "\n" ); this.users = {}; return true; }; /** * Invites a user to a channel. * * @param nick the user name to invite. */ CIRCChannel.prototype.invite = function (nick) { var rawNick = fromUnicode(nick, this.parent); this.parent.sendData("INVITE " + rawNick + " " + this.encodedName + "\n"); return true; }; CIRCChannel.prototype.findUsers = function (mask) { var ary = []; var unchecked = 0; mask = getHostmaskParts(mask); for (var nick in this.users) { var user = this.users[nick]; if (!user.host || !user.name) { unchecked++; } else if (hostmaskMatches(user, mask)) { ary.push(user); } } return { users: ary, unchecked }; }; /** * Stores a channel's current mode settings. * * You should never need to create an instance of this prototype; access the * channel mode information through the |CIRCChannel.mode| property. * * @param parent The |CIRCChannel| to which this mode belongs. */ function CIRCChanMode(parent) { this.parent = parent; this.modeA = {}; this.modeB = {}; this.modeC = {}; this.modeD = {}; this.invite = false; this.moderated = false; this.publicMessages = true; this.publicTopic = true; this.secret = false; this.pvt = false; this.key = ""; this.limit = -1; } CIRCChanMode.prototype.TYPE = "IRCChanMode"; // Returns the complete mode string, as constructed from its component parts. CIRCChanMode.prototype.getModeStr = function (f) { var str = ""; var modeCparams = ""; /* modeA are 'list' ones, and so should not be shown. * modeB are 'param' ones, like +k key, so we wont show them either. * modeC are 'on-param' ones, like +l limit, which we will show. * modeD are 'boolean' ones, which we will definitely show. */ // Add modeD: for (var m in this.modeD) { if (this.modeD[m]) { str += m; } } // Add modeC, save parameters for adding all the way at the end: for (var m in this.modeC) { if (this.modeC[m]) { str += m; modeCparams += " " + this.modeC[m]; } } // Add parameters: if (str) { str = "+" + str + modeCparams; } return str; }; // Sends the given mode string to the server with the channel pre-filled. CIRCChanMode.prototype.setMode = function (modestr) { this.parent.parent.sendData( "MODE " + this.parent.encodedName + " " + modestr + "\n" ); return true; }; // Sets (|n| > 0) or clears (|n| <= 0) the user count limit. CIRCChanMode.prototype.setLimit = function (n) { if (typeof n == "undefined" || n <= 0) { this.parent.parent.sendData("MODE " + this.parent.encodedName + " -l\n"); } else { this.parent.parent.sendData( "MODE " + this.parent.encodedName + " +l " + Number(n) + "\n" ); } return true; }; // Locks the channel with a given key. CIRCChanMode.prototype.lock = function (k) { this.parent.parent.sendData( "MODE " + this.parent.encodedName + " +k " + k + "\n" ); return true; }; // Unlocks the channel with a given key. CIRCChanMode.prototype.unlock = function (k) { this.parent.parent.sendData( "MODE " + this.parent.encodedName + " -k " + k + "\n" ); return true; }; // Sets or clears the moderation mode. CIRCChanMode.prototype.setModerated = function (f) { var modifier = f ? "+" : "-"; this.parent.parent.sendData( "MODE " + this.parent.encodedName + " " + modifier + "m\n" ); return true; }; // Sets or clears the allow public messages mode. CIRCChanMode.prototype.setPublicMessages = function (f) { var modifier = f ? "-" : "+"; this.parent.parent.sendData( "MODE " + this.parent.encodedName + " " + modifier + "n\n" ); return true; }; // Sets or clears the public topic mode. CIRCChanMode.prototype.setPublicTopic = function (f) { var modifier = f ? "-" : "+"; this.parent.parent.sendData( "MODE " + this.parent.encodedName + " " + modifier + "t\n" ); return true; }; // Sets or clears the invite-only mode. CIRCChanMode.prototype.setInvite = function (f) { var modifier = f ? "+" : "-"; this.parent.parent.sendData( "MODE " + this.parent.encodedName + " " + modifier + "i\n" ); return true; }; // Sets or clears the private channel mode. CIRCChanMode.prototype.setPvt = function (f) { var modifier = f ? "+" : "-"; this.parent.parent.sendData( "MODE " + this.parent.encodedName + " " + modifier + "p\n" ); return true; }; // Sets or clears the secret channel mode. CIRCChanMode.prototype.setSecret = function (f) { var modifier = f ? "+" : "-"; this.parent.parent.sendData( "MODE " + this.parent.encodedName + " " + modifier + "s\n" ); return true; }; function CIRCUser(parent, unicodeName, encodedName, name, host) { // Both unicodeName and encodedName are optional, but at least one must be // present. if (!encodedName && !unicodeName) { throw Components.Exception( "Hey! Come on, I need either an encoded or a Unicode name.", Cr.NS_ERROR_INVALID_ARG ); } if (!encodedName) { encodedName = fromUnicode(unicodeName, parent); } let collectionKey = ":" + parent.toLowerCase(encodedName); if (collectionKey in parent.users) { let existingUser = parent.users[collectionKey]; if (name) { existingUser.name = name; } if (host) { existingUser.host = host; } return existingUser; } this.parent = parent; this.encodedName = encodedName; this.canonicalName = collectionKey.substr(1); this.collectionKey = collectionKey; this.unicodeName = unicodeName || toUnicode(encodedName, this.parent); this.viewName = this.unicodeName; this.name = name; this.host = host; this.desc = ""; this.account = null; this.connectionHost = null; this.isAway = false; this.modestr = this.parent.parent.INITIAL_UMODE; this.parent.users[this.collectionKey] = this; if ("onInit" in this) { this.onInit(); } return this; } CIRCUser.prototype.TYPE = "IRCUser"; // Returns the IRC URL representation of this user. CIRCUser.prototype.getURL = function () { return this.parent.parent.getURL(this.encodedName, ["isnick"]); }; CIRCUser.prototype.rehome = function (newParent) { delete this.parent.users[this.collectionKey]; this.parent = newParent; this.parent.users[this.collectionKey] = this; }; CIRCUser.prototype.changeNick = function (unicodeName) { this.unicodeName = unicodeName; this.viewName = this.unicodeName; this.encodedName = fromUnicode(this.unicodeName, this.parent); this.canonicalName = this.parent.toLowerCase(this.encodedName); this.collectionKey = ":" + this.canonicalName; }; CIRCUser.prototype.getHostMask = function (pfx) { pfx = typeof pfx != "undefined" ? pfx : "*!" + this.name + "@*."; var idx = this.host.indexOf("."); if (idx == -1) { return pfx + this.host; } return pfx + this.host.substr(idx + 1, this.host.length); }; CIRCUser.prototype.getBanMask = function () { if (!this.host) { return this.unicodeName + "!*@*"; } return "*!*@" + this.host; }; CIRCUser.prototype.say = function (msg) { this.parent.sayTo(this.encodedName, fromUnicode(msg, this)); }; CIRCUser.prototype.notice = function (msg) { this.parent.noticeTo(this.encodedName, fromUnicode(msg, this)); }; CIRCUser.prototype.act = function (msg) { this.parent.actTo(this.encodedName, fromUnicode(msg, this)); }; CIRCUser.prototype.ctcp = function (code, msg, type) { msg = msg || ""; type = type || "PRIVMSG"; this.parent.ctcpTo( this.encodedName, fromUnicode(code, this), fromUnicode(msg, this), type ); }; CIRCUser.prototype.whois = function () { this.parent.whois(this.unicodeName); }; /* * channel user */ function CIRCChanUser( parent, unicodeName, encodedName, modes, userInChannel, name, host ) { // Both unicodeName and encodedName are optional, but at least one must be // present. if (!encodedName && !unicodeName) { throw Components.Exception( "Hey! Come on, I need either an encoded or a Unicode name.", Cr.NS_ERROR_INVALID_ARG ); } else if (encodedName && !unicodeName) { unicodeName = toUnicode(encodedName, parent); } else if (!encodedName && unicodeName) { encodedName = fromUnicode(unicodeName, parent); } // We should have both unicode and encoded names by now. let collectionKey = ":" + parent.parent.toLowerCase(encodedName); if (collectionKey in parent.users) { let existingUser = parent.users[collectionKey]; if (modes) { // If we start with a single character mode, assume we're replacing // the list. (i.e. the list is either all +/- modes, or all normal) if (modes.length >= 1 && modes[0].search(/^[-+]/) == -1) { // Modes, but no +/- prefixes, so *replace* mode list. existingUser.modes = modes; } else { // We have a +/- mode list, so carefully update the mode list. for (var m in modes) { // This will remove '-' modes, and all other modes will be // added. var mode = modes[m][1]; if (modes[m][0] == "-") { let idx = existingUser.modes.indexOf(mode); if (idx >= 0) { existingUser.modes.splice(idx, 1); } } else if (!existingUser.modes.includes(mode)) { existingUser.modes.push(mode); } } } } existingUser.isFounder = existingUser.modes.includes("q"); existingUser.isAdmin = existingUser.modes.includes("a"); existingUser.isOp = existingUser.modes.includes("o"); existingUser.isHalfOp = existingUser.modes.includes("h"); existingUser.isVoice = existingUser.modes.includes("v"); existingUser.updateSortName(); return existingUser; } var protoUser = new CIRCUser( parent.parent, unicodeName, encodedName, name, host ); this.__proto__ = protoUser; this.getURL = cusr_geturl; this.setOp = cusr_setop; this.setHalfOp = cusr_sethalfop; this.setVoice = cusr_setvoice; this.setBan = cusr_setban; this.kick = cusr_kick; this.kickBan = cusr_kban; this.say = cusr_say; this.notice = cusr_notice; this.act = cusr_act; this.whois = cusr_whois; this.updateSortName = cusr_updatesortname; this.parent = parent; this.TYPE = "IRCChanUser"; this.modes = []; if (typeof modes != "undefined") { this.modes = modes; } this.isFounder = this.modes.includes("q"); this.isAdmin = this.modes.includes("a"); this.isOp = this.modes.includes("o"); this.isHalfOp = this.modes.includes("h"); this.isVoice = this.modes.includes("v"); this.updateSortName(); if (userInChannel) { parent.users[this.collectionKey] = this; } return this; } function cusr_updatesortname() { // Check for the highest mode the user has (for sorting the userlist) const userModes = this.parent.parent.userModes; var modeLevel = 0; var mode; for (var i = 0; i < this.modes.length; i++) { for (var j = 0; j < userModes.length; j++) { if (userModes[j].mode == this.modes[i]) { if (userModes.length - j > modeLevel) { modeLevel = userModes.length - j; mode = userModes[j]; } break; } } } // Counts numerically down from 9. this.sortName = 9 - modeLevel + "-" + this.unicodeName; } function cusr_geturl() { // Don't ask. return this.parent.parent.parent.getURL(this.encodedName, ["isnick"]); } function cusr_setop(f) { var server = this.parent.parent; var me = server.me; var modifier = f ? " +o " : " -o "; server.sendData( "MODE " + this.parent.encodedName + modifier + this.encodedName + "\n" ); return true; } function cusr_sethalfop(f) { var server = this.parent.parent; var me = server.me; var modifier = f ? " +h " : " -h "; server.sendData( "MODE " + this.parent.encodedName + modifier + this.encodedName + "\n" ); return true; } function cusr_setvoice(f) { var server = this.parent.parent; var me = server.me; var modifier = f ? " +v " : " -v "; server.sendData( "MODE " + this.parent.encodedName + modifier + this.encodedName + "\n" ); return true; } function cusr_kick(reason) { var server = this.parent.parent; var me = server.me; reason = typeof reason == "string" ? reason : ""; server.sendData( "KICK " + this.parent.encodedName + " " + this.encodedName + " :" + fromUnicode(reason, this) + "\n" ); return true; } function cusr_setban(f) { var server = this.parent.parent; var me = server.me; if (!this.host) { return false; } var modifier = f ? " +b " : " -b "; modifier += fromUnicode(this.getBanMask(), server) + " "; server.sendData("MODE " + this.parent.encodedName + modifier + "\n"); return true; } function cusr_kban(reason) { var server = this.parent.parent; var me = server.me; if (!this.host) { return false; } reason = typeof reason != "undefined" ? reason : this.encodedName; var modifier = " -o+b " + this.encodedName + " " + fromUnicode(this.getBanMask(), server) + " "; server.sendData( "MODE " + this.parent.encodedName + modifier + "\n" + "KICK " + this.parent.encodedName + " " + this.encodedName + " :" + reason + "\n" ); return true; } function cusr_say(msg) { this.__proto__.say(msg); } function cusr_notice(msg) { this.__proto__.notice(msg); } function cusr_act(msg) { this.__proto__.act(msg); } function cusr_whois() { this.__proto__.whois(); } // IRC URL parsing and generating function parseIRCURL(url) { var specifiedHost = ""; var rv = {}; rv.spec = url; rv.scheme = url.split(":")[0]; rv.host = null; rv.target = ""; rv.port = rv.scheme == "ircs" ? 6697 : 6667; rv.msg = ""; rv.pass = null; rv.key = null; rv.charset = null; rv.needpass = false; rv.needkey = false; rv.isnick = false; rv.isserver = false; if (url.search(/^(ircs?:\/?\/?)$/i) != -1) { return rv; } /* split url into <host>/<everything-else> pieces */ var ary = url.match(/^ircs?:\/\/([^\/\s]+)?(\/[^\s]*)?$/i); if (!ary || !ary[1]) { dd("parseIRCURL: initial split failed"); return null; } var host = ary[1]; var rest = arrayHasElementAt(ary, 2) ? ary[2] : ""; /* split <host> into server (or network) / port */ ary = host.match(/^([^\:]+|\[[^\]]+\])(\:\d+)?$/i); if (!ary) { dd("parseIRCURL: host/port split failed"); return null; } // 1 = hostname or IPv4 address, 2 = port. specifiedHost = rv.host = ary[1].toLowerCase(); rv.isserver = arrayHasElementAt(ary, 2) || /\.|:/.test(specifiedHost); if (arrayHasElementAt(ary, 2)) { rv.port = parseInt(ary[2].substr(1)); } if (rest) { ary = rest.match(/^\/([^\?\s\/,]*)?\/?(,[^\?]*)?(\?.*)?$/); if (!ary) { dd("parseIRCURL: rest split failed ``" + rest + "''"); return null; } rv.target = arrayHasElementAt(ary, 1) ? ecmaUnescape(ary[1]) : ""; if (rv.target.search(/[\x07,\s]/) != -1) { dd("parseIRCURL: invalid characters in channel name"); return null; } var params = arrayHasElementAt(ary, 2) ? ary[2].toLowerCase() : ""; var query = arrayHasElementAt(ary, 3) ? ary[3] : ""; if (params) { params = params.split(","); while (params.length) { var param = params.pop(); // split doesn't take out empty bits: if (param == "") { continue; } switch (param) { case "isnick": rv.isnick = true; if (!rv.target) { dd("parseIRCURL: isnick w/o target"); /* isnick w/o a target is bogus */ return null; } break; case "isserver": rv.isserver = true; if (!specifiedHost) { dd("parseIRCURL: isserver w/o host"); /* isserver w/o a host is bogus */ return null; } break; case "needpass": case "needkey": rv[param] = true; break; default: /* If we didn't understand it, ignore but warn: */ dd("parseIRCURL: Unrecognized param '" + param + "' in URL!"); } } } if (query) { ary = query.substr(1).split("&"); while (ary.length) { var arg = ary.pop().split("="); /* * we don't want to accept *any* query, or folks could * say things like "target=foo", and overwrite what we've * already parsed, so we only use query args we know about. */ switch (arg[0].toLowerCase()) { case "msg": rv.msg = ecmaUnescape(arg[1]).replace("\n", "\\n"); break; case "pass": rv.needpass = true; rv.pass = ecmaUnescape(arg[1]).replace("\n", "\\n"); break; case "key": rv.needkey = true; rv.key = ecmaUnescape(arg[1]).replace("\n", "\\n"); break; case "charset": rv.charset = ecmaUnescape(arg[1]).replace("\n", "\\n"); break; } } } } return rv; } function constructIRCURL(obj) { function parseQuery(obj) { var rv = []; if ("msg" in obj) { rv.push("msg=" + ecmaEscape(obj.msg.replace("\\n", "\n"))); } if ("pass" in obj) { rv.push("pass=" + ecmaEscape(obj.pass.replace("\\n", "\n"))); } if ("key" in obj) { rv.push("key=" + ecmaEscape(obj.key.replace("\\n", "\n"))); } if ("charset" in obj) { rv.push("charset=" + ecmaEscape(obj.charset.replace("\\n", "\n"))); } return rv.length ? "?" + rv.join("&") : ""; } function parseFlags(obj) { var rv = []; var haveTarget = "target" in obj && obj.target; if ("needpass" in obj && obj.needpass) { rv.push(",needpass"); } if ("needkey" in obj && obj.needkey && haveTarget) { rv.push(",needkey"); } if ("isnick" in obj && obj.isnick && haveTarget) { rv.push(",isnick"); } return rv.join(""); } var flags = ""; var scheme = "scheme" in obj ? obj.scheme : "irc"; if (!("host" in obj) || !obj.host) { return scheme + "://"; } var url = scheme + "://" + obj.host; // Add port if non-standard: if ( "port" in obj && ((scheme == "ircs" && obj.port != 6697) || (scheme == "irc" && obj.port != 6667)) ) { url += ":" + obj.port; } // Need to add ",isserver" if there's no port and no dots in the hostname: else if ("isserver" in obj && obj.isserver && !obj.host.includes(".")) { flags += ",isserver"; } url += "/"; if ("target" in obj && obj.target) { if (obj.target.search(/[\x07,\s]/) != -1) { dd("parseIRCObject: invalid characters in channel/nick name"); return null; } url += ecmaEscape(obj.target).replace(/\//g, "%2f"); } return url + flags + parseFlags(obj) + parseQuery(obj); } /* Canonicalizing an IRC URL removes all items which aren't necessary to * identify the target. For example, an IRC URL with ?pass=password and one * without (but otherwise identical) are refering to the same target, so * ?pass= is removed. */ function makeCanonicalIRCURL(url) { var canonicalProps = { scheme: true, host: true, port: true, target: true, isserver: true, isnick: true, }; var urlObject = parseIRCURL(url); if (!urlObject) { return ""; } // Input wasn't a valid IRC URL. for (var prop in urlObject) { if (!(prop in canonicalProps)) { delete urlObject[prop]; } } return constructIRCURL(urlObject); }