/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*- * * ***** BEGIN LICENSE BLOCK ***** * Version: MPL 1.1/GPL 2.0/LGPL 2.1 * * The contents of this file are subject to the Mozilla Public License Version * 1.1 (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * http://www.mozilla.org/MPL/ * * Software distributed under the License is distributed on an "AS IS" basis, * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License * for the specific language governing rights and limitations under the * License. * * The Original Code is JSIRC Library. * * The Initial Developer of the Original Code is * New Dimensions Consulting, Inc. * Portions created by the Initial Developer are Copyright (C) 1999 * the Initial Developer. All Rights Reserved. * * Contributor(s): * Robert Ginda, rginda@ndcico.com, original author * * Alternatively, the contents of this file may be used under the terms of * either the GNU General Public License Version 2 or later (the "GPL"), or * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), * in which case the provisions of the GPL or the LGPL are applicable instead * of those above. If you wish to allow use of your version of this file only * under the terms of either the GPL or the LGPL, and not to allow others to * use your version of this file under the terms of the MPL, indicate your * decision by deleting the provisions above and replace them with the notice * and other provisions required by the GPL or the LGPL. If you do not delete * the provisions above, a recipient may use your version of this file under * the terms of any one of the MPL, the GPL or the LGPL. * * ***** END LICENSE BLOCK ***** */ 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"; 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; } var 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 i; /* * irc network */ function CIRCNetwork (name, serverList, eventPump, temporary) { this.unicodeName = name; this.viewName = name; this.canonicalName = name; this.encodedName = name; this.servers = new Object(); this.serverList = new Array(); this.ignoreList = new Object(); this.ignoreMaskCache = new Object(); 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"; /* 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.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"; CIRCNetwork.prototype.getURL = function net_geturl(target) { if (this.temporary) return this.serverList[0].getURL(target); if (!target) target = ""; /* Determine whether to use the irc:// or ircs:// scheme */ if ("primServ" in this && this.primServ.isConnected) { if (this.primServ.isSecure) return "ircs://" + ecmaEscape(this.unicodeName) + "/" + target; } else { /* No current connections, so let's see if every server has SSL */ var isSecure = true; for (var s in this.serverList) { if (!this.serverList[s].isSecure) { isSecure = false; break; } } if (isSecure) return "ircs://" + ecmaEscape(this.unicodeName) + "/" + target; } return "irc://" + ecmaEscape(this.unicodeName) + "/" + target; } CIRCNetwork.prototype.getUser = function net_getuser (nick) { if ("primServ" in this && this.primServ) return this.primServ.getUser(nick); return null; } CIRCNetwork.prototype.addServer = function net_addsrv(host, port, isSecure, password) { this.serverList.push(new CIRCServer(this, host, port, isSecure, password)); } // Checks if a network has a secure server in its list. CIRCNetwork.prototype.hasSecureServer = function net_hasSecure() { for (var i = 0; i < this.serverList.length; i++) { if (this.serverList[i].isSecure) return true; } return false; } CIRCNetwork.prototype.clearServerList = function net_clearserverlist() { /* 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 = new Object(); this.serverList = new Array(); } /** Trigger an onDoConnect event after a delay. */ CIRCNetwork.prototype.delayedConnect = function net_delayedConnect(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 net_immediateConnect(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 net_connect(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; } CIRCNetwork.prototype.quit = function net_quit (reason) { if (this.isConnected()) this.primServ.logout(reason); } CIRCNetwork.prototype.cancel = function net_cancel() { // 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; this.primServ.connection.disconnect(); // Throw the necessary error events: ev = new CEvent ("network", "error", this, "onError"); ev.server = this; ev.debug = "Connect sequence was cancelled."; 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); } } /* * Handles a request to connect to a primary server. */ CIRCNetwork.prototype.onDoConnect = function net_doconnect(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.connection) this.primServ.connection.disconnect(); else this.state = NET_OFFLINE; ev = new CEvent ("network", "error", this, "onError"); ev.server = this; ev.debug = "Connect sequence was cancelled."; 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 (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 { if (!this.serverList[host].connect(null)) { /* connect failed, try again */ this.delayedConnect(); } } 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; 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; } CIRCNetwork.prototype.isConnected = function net_connected (e) { return ("primServ" in this && this.primServ.isConnected); } CIRCNetwork.prototype.ignore = function net_ignore (hostmask) { var input = getHostmaskParts(hostmask); if (input.mask in this.ignoreList) return false; this.ignoreList[input.mask] = input; this.ignoreMaskCache = new Object(); return true; } CIRCNetwork.prototype.unignore = function net_ignore (hostmask) { var input = getHostmaskParts(hostmask); if (!(input.mask in this.ignoreList)) return false; delete this.ignoreList[input.mask]; this.ignoreMaskCache = new Object(); return true; } /* * irc server */ 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 = new Object(); s.users = new Object(); } s.unicodeName = serverName; s.viewName = serverName; s.canonicalName = 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 = new Array(); s.lastSend = new Date("1/1/1980"); s.lastPingSent = null; s.lastPing = null; s.sendsThisRound = 0; s.savedLine = ""; s.lag = -1; s.usersStable = true; s.supports = null; s.channelTypes = null; s.channelModes = null; s.userModes = null; s.maxLineLength = 400; parent.servers[s.canonicalName] = s; if ("onInit" in s) s.onInit(); return s; } CIRCServer.prototype.MAX_LINES_PER_SEND = 0; /* unlimited */ CIRCServer.prototype.MS_BETWEEN_SENDS = 1500; CIRCServer.prototype.READ_TIMEOUT = 100; CIRCServer.prototype.TOO_MANY_LINES_MSG = "\01ACTION has said too much\01"; 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 on352 code doesn't collect hostmask, username, etc. */ CIRCServer.prototype.LIGHTWEIGHT_WHO = false; /* -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: function (modifier) { return (modifier == "+"); } }, m: { name: "moderated", getValue: function (modifier) { return (modifier == "+"); } }, n: { name: "publicMessages", getValue: function (modifier) { return (modifier == "-"); } }, t: { name: "publicTopic", getValue: function (modifier) { return (modifier == "-"); } }, s: { name: "secret", getValue: function (modifier) { return (modifier == "+"); } }, p: { name: "pvt", getValue: function (modifier) { return (modifier == "+"); } }, k: { name: "key", getValue: function (modifier, data) { if (modifier == "+") return data; else return ""; } }, l: { name: "limit", getValue: function (modifier, data) { // limit is special - we return -1 if there is no limit. if (modifier == "-") return -1; else return data; } } }; CIRCServer.prototype.toLowerCase = function serv_tolowercase(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; } CIRCServer.prototype.getURL = function serv_geturl(target) { if (!target) target = ""; var url = (this.isSecure ? "ircs://" : "irc://") + this.hostname; if (this.port != 6667) url += ":" + this.port; url += "/" + target; if (url.indexOf(".") == -1) url += ",isserver"; if (this.password) url += ",needpass"; return url; } CIRCServer.prototype.getUser = function chan_getuser(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 chan_getchannel(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 serv_connect (password) { try { this.connection = new CBSConnection(); } catch (ex) { ev = new CEvent ("server", "error", this, "onError"); ev.server = this; ev.debug = "Couldn't create socket :" + ex; ev.errorCode = JSIRC_ERR_NO_SOCKET; ev.exception = ex; this.parent.eventPump.addEvent (ev); return false; } var config = { isSecure: this.isSecure }; if (this.parent.PROXY_TYPE_OVERRIDE) config.proxy = this.parent.PROXY_TYPE_OVERRIDE; if (this.connection.connect(this.hostname, this.port, config)) { var ev = new CEvent("server", "connect", this, "onConnect"); if (password) this.password = password; ev.server = this; this.parent.eventPump.addEvent (ev); this.isConnected = true; if (jsenv.HAS_NSPR_EVENTQ) this.connection.startAsyncRead(this); else this.parent.eventPump.addEvent(new CEvent("server", "poll", this, "onPoll")); } return true; } /* * What to do when the client connects to it's primary server */ CIRCServer.prototype.onConnect = function serv_onconnect (e) { this.parent.primServ = e.server; this.login(this.parent.INITIAL_NICK, this.parent.INITIAL_NAME, this.parent.INITIAL_DESC); return true; } CIRCServer.prototype.onStreamDataAvailable = function serv_sda (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 serv_sockdiscon(status) { var ev = new CEvent ("server", "disconnect", this, "onDisconnect"); ev.server = this; ev.disconnectStatus = status; this.parent.eventPump.addEvent (ev); } CIRCServer.prototype.flushSendQueue = function serv_flush() { this.sendQueue.length = 0; dd("sendQueue flushed."); return true; } CIRCServer.prototype.login = function serv_login(nick, name, desc) { nick = nick.replace(" ", "_"); name = name.replace(" ", "_"); 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.sendData("NICK " + this.me.encodedName + "\n"); this.sendData("USER " + name + " * * :" + fromUnicode(desc, this) + "\n"); } CIRCServer.prototype.logout = function serv_logout(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.addTarget = function serv_addtarget(name) { if (arrayIndexOf(this.channelTypes, name[0]) != -1) { return this.addChannel(name); } else { return this.addUser(name); } } CIRCServer.prototype.addChannel = function serv_addchan(unicodeName, charset) { return new CIRCChannel(this, unicodeName, fromUnicode(unicodeName, charset)); } CIRCServer.prototype.addUser = function serv_addusr(unicodeName, name, host) { return new CIRCUser(this, unicodeName, null, name, host); } CIRCServer.prototype.getChannelsLength = function serv_chanlen() { var i = 0; for (var p in this.channels) i++; return i; } CIRCServer.prototype.getUsersLength = function serv_chanlen() { var i = 0; for (var p in this.users) i++; return i; } CIRCServer.prototype.sendData = function serv_senddata (msg) { this.queuedSendData (msg); } CIRCServer.prototype.queuedSendData = function serv_senddata (msg) { if (this.sendQueue.length == 0) this.parent.eventPump.addEvent (new CEvent ("server", "senddata", this, "onSendData")); arrayInsertAt (this.sendQueue, 0, new String(msg)); } /* * Takes care not to let more than MAX_LINES_PER_SEND lines out per * cycle. Cycle's are defined as the time between onPoll calls. */ CIRCServer.prototype.messageTo = function serv_messto (code, target, msg, ctcpCode) { var lines = String(msg).split ("\n"); var sendable = 0, i; var pfx = "", sfx = ""; if (this.MAX_LINES_PER_SEND && this.sendsThisRound > this.MAX_LINES_PER_SEND) return false; if (ctcpCode) { pfx = "\01" + ctcpCode; sfx = "\01"; } for (i in lines) { 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 - 2); } else { // Dump-split. extraLine = lines[i].substr(0, this.maxLineLength); lines[i] = lines[i].substr(extraLine.length); } if (!this.messageTo(code, target, extraLine, ctcpCode)) return false; } } } // Check this again, since we may have actually sent stuff in the loop above. if (this.MAX_LINES_PER_SEND && this.sendsThisRound > this.MAX_LINES_PER_SEND) return false; for (i in lines) if ((lines[i] != "") || ctcpCode) sendable++; for (i in lines) { if (this.MAX_LINES_PER_SEND && ( ((this.sendsThisRound == this.MAX_LINES_PER_SEND - 1) && (sendable > this.MAX_LINES_PER_SEND)) || this.sendsThisRound == this.MAX_LINES_PER_SEND)) { this.sendData ("PRIVMSG " + target + " :" + this.TOO_MANY_LINES_MSG + "\n"); this.sendsThisRound++; return true; } if ((lines[i] != "") || ctcpCode) { var line = code + " " + target + " :" + pfx; this.sendsThisRound++; if (lines[i] != "") { if (ctcpCode) line += " "; line += lines[i] + sfx; } else line += sfx; //dd ("-*- irc sending '" + line + "'"); this.sendData(line + "\n"); } } return true; } CIRCServer.prototype.sayTo = function serv_sayto (target, msg) { this.messageTo("PRIVMSG", target, msg); } CIRCServer.prototype.noticeTo = function serv_noticeto (target, msg) { this.messageTo("NOTICE", target, msg); } CIRCServer.prototype.actTo = function serv_actto (target, msg) { this.messageTo("PRIVMSG", target, msg, "ACTION"); } CIRCServer.prototype.ctcpTo = function serv_ctcpto (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 serv_changenick(newNick) { this.sendData("NICK " + fromUnicode(newNick, this) + "\n"); } CIRCServer.prototype.updateLagTimer = function serv_uptimer() { this.connection.sendData("PING :LAGTIMER\n"); this.lastPing = this.lastPingSent = new Date(); } CIRCServer.prototype.userhost = function serv_userhost(target) { this.sendData("USERHOST " + fromUnicode(target, this) + "\n"); } CIRCServer.prototype.userip = function serv_userip(target) { this.sendData("USERIP " + fromUnicode(target, this) + "\n"); } CIRCServer.prototype.who = function serv_who(target) { this.sendData("WHO " + fromUnicode(target, this) + "\n"); } /** * Abstracts the whois command. * * @param target intended user(s). */ CIRCServer.prototype.whois = function serv_whois (target) { this.sendData("WHOIS " + fromUnicode(target, this) + "\n"); } CIRCServer.prototype.whowas = function serv_whowas(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 serv_disconnect(e) { function stateChangeFn(network, state) { network.state = state; }; function delayedConnectFn(network) { network.delayedConnect(); }; if ((this.parent.state == NET_CONNECTING) || /* fell off while connecting, try again */ (this.parent.primServ == this) && (this.parent.state == NET_ONLINE) && (!("quitting" in this) && this.parent.stayingPower)) { /* 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 = new Object(); this.channels[c].active = false; } this.connection = null; this.isConnected = false; delete this.quitting; return true; } CIRCServer.prototype.onSendData = function serv_onsenddata (e) { if (!this.isConnected || (this.parent.state == NET_CANCELLING)) { dd ("Can't send to disconnected socket"); this.flushSendQueue(); return false; } var d = new Date(); this.sendsThisRound = 0; // 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; 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 serv_poll(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); if (jsenv.HAS_RHINO && (ex instanceof java.lang.ThreadDeath)) { dd("### catching a ThreadDeath"); throw(ex); } else { ev = new CEvent ("server", "disconnect", this, "onDisconnect"); ev.server = this; ev.reason = "error"; ev.exception = ex; 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 serv_ppline(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 (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|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 of the message (if it exists) * user...........user object initialized with data from the message * 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 , and * tokens. */ CIRCServer.prototype.onRawData = function serv_onRawData(e) { var ary; var l = e.data; if (l.length == 0) { dd ("empty line on onRawData?"); return false; } if (l[0] == ":") { // Must split only on a REAL space here, not just any old whitespace. ary = l.match(/:(\S+) (.*)/); e.source = ary[1]; l = ary[2]; ary = e.source.match(/(\S+)!(\S+)@(.*)/); if (ary) { e.user = new CIRCUser(this, null, ary[1], ary[2], ary[3]); } else { ary = e.source.match(/(\S+)@(.*)/); if (ary) { e.user = new CIRCUser(this, null, ary[1], null, ary[2]); } else { ary = e.source.match(/(\S+)!(.*)/); if (ary) e.user = new CIRCUser(this, null, ary[1], ary[2], null); } } } 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) /* 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 Privmsg and Notice messages here. if (e.ignored && ((e.code == "PRIVMSG") || (e.code == "NOTICE"))) return true; e.type = "parseddata"; e.destObject = this; e.destMethod = "onParsedData"; return true; } /* * onParsedData forwards to next event, based on |e.code| */ CIRCServer.prototype.onParsedData = function serv_onParsedData(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 serv_topic (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 serv_001 (e) { this.parent.connectAttempt = 0; this.parent.connectCandidate = 0; 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.canonicalName, 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 = new Object(); 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"); } if (this.parent.INITIAL_CHANNEL) { this.parent.primChan = this.addChannel (this.parent.INITIAL_CHANNEL); this.parent.primChan.join(); } this.parent.users = this.users; e.destObject = this.parent; e.set = "network"; } /* server features */ CIRCServer.prototype.on005 = function serv_005 (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) { var newName, encodedName; for (var user in this.users) { newName = this.toLowerCase(this.users[user].encodedName); renameProperty(this.users, user, newName); this.users[newName].canonicalName = newName; } for (var channel in this.channels) { newName = this.toLowerCase(this.channels[channel].encodedName); renameProperty(this.channels, this.channels[channel].canonicalName, newName); this.channels[channel].canonicalName = newName; for (user in this.channels[channel].users) { encodedName = this.channels[channel].users[user].encodedName; newName = this.toLowerCase(encodedName); renameProperty(this.channels[channel].users, user, newName); } } } // 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; } /* whois name */ CIRCServer.prototype.on311 = function serv_311 (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 serv_312 (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 serv_317 (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 serv_319(e) { e.user = new CIRCUser(this, null, e.params[2]); e.destObject = this.parent; e.set = "network"; } /* end of whois */ CIRCServer.prototype.on318 = function serv_318(e) { e.user = new CIRCUser(this, null, e.params[2]); if ("pendingWhoisLines" in this) delete this.pendingWhoisLines; e.destObject = this.parent; e.set = "network"; } /* TOPIC reply - no topic set */ CIRCServer.prototype.on331 = function serv_331 (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 serv_332 (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 serv_333 (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 serv_352 (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; } /* end of who */ CIRCServer.prototype.on315 = function serv_315 (e) { e.user = new CIRCUser(this, null, e.params[2]); e.destObject = this.parent; e.set = "network"; return true; } /* name reply */ CIRCServer.prototype.on353 = function serv_353 (e) { e.channel = new CIRCChannel(this, null, e.params[3]); if (e.channel.usersStable) { e.channel.users = new Object(); 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 found = false; for (var m in mList) { if (nick[0] == mList[m].symbol) { e.user = new CIRCChanUser(e.channel, null, nick.substr(1, nick.length), [ mList[m].mode ]); found = true; break; } } if (!found) e.user = new CIRCChanUser(e.channel, null, nick, [ ]); } return true; } /* end of names */ CIRCServer.prototype.on366 = function serv_366 (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 serv_329 (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 serv_324 (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 serv_367(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.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 serv_368(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 serv_348(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 serv_349(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 serv_482(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 serv_302(e) { var list = e.params[2].split(/\s+/); for (var i = 0; i < list.length; i++) { // ::= ['*'] '=' <'+'|'-'> // '*' == 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; } /* user changed the mode */ CIRCServer.prototype.onMode = function serv_mode (e) { e.destObject = this; /* modes are not allowed in +channels -> no need to test that here.. */ if (arrayIndexOf(this.channelTypes, e.params[1][0]) != -1) { 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 serv_usermode (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 serv_chanmode (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 = new Array(); 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 (arrayContains(cmList.a, 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 (arrayContains(cmList.b, 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 (arrayContains(cmList.c, 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 (arrayContains(cmList.d, 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 serv_nick (e) { var newNick = e.params[1]; var newKey = this.toLowerCase(newNick); var oldKey = e.user.canonicalName; 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); ev = new CEvent ("channel", "nick", this.channels[c], "onNick"); ev.channel = this.channels[c]; ev.user = cuser; ev.server = this; ev.oldNick = e.oldNick; this.parent.eventPump.addEvent(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.user = e.user; ev.server = this; ev.oldNick = e.oldNick; this.parent.eventPump.addEvent(ev); } e.destObject = e.user; e.set = "user"; return true; } CIRCServer.prototype.onQuit = function serv_quit (e) { var reason = e.decodeParam(1); for (var c in e.server.channels) { if (e.server.channels[c].active && e.user.canonicalName in e.server.channels[c].users) { var ev = new CEvent ("channel", "quit", e.server.channels[c], "onQuit"); ev.user = e.server.channels[c].users[e.user.canonicalName]; ev.channel = e.server.channels[c]; ev.server = ev.channel.parent; ev.reason = reason; this.parent.eventPump.addEvent(ev); delete e.server.channels[c].users[e.user.canonicalName]; } } this.users[e.user.canonicalName].lastQuitMessage = reason; this.users[e.user.canonicalName].lastQuitDate = new Date(); // 0 == prune onQuit. if (this.PRUNE_OLD_USERS == 0) delete this.users[e.user.canonicalName]; e.reason = reason; e.destObject = e.user; e.set = "user"; return true; } CIRCServer.prototype.onPart = function serv_part (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 serv_kick (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.canonicalName]; 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 serv_join(e) { e.channel = new CIRCChannel(this, null, e.params[1]); e.user = new CIRCChanUser(e.channel, e.user.unicodeName); 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 (arrayContains(t.channelModes.a, "b")) { e.server.sendData("MODE " + e.channel.encodedName + " +b\n"); e.channel.pendingBanList = true; } if (arrayContains(t.channelModes.a, "e")) { e.server.sendData("MODE " + e.channel.encodedName + " +e\n"); e.channel.pendingExceptList = true; } }; // 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.onPing = function serv_ping (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 serv_pong (e) { if (e.params[2] != "LAGTIMER") return true; if (this.lastPingSent) this.lag = roundTo ((new Date() - this.lastPingSent) / 1000, 2); this.lastPingSent = null; e.destObject = this.parent; e.set = "network"; return true; } CIRCServer.prototype.onInvite = function serv_invite(e) { e.channel = new CIRCChannel(this, null, e.params[2]); e.destObject = this.parent; e.set = "network"; } CIRCServer.prototype.onNotice = function serv_notice (e) { var targetName = e.params[1]; // 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) { targetName = targetName.substr(1); break; } } if (arrayIndexOf(this.channelTypes, targetName[0]) != -1) { 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 */ } if (e.params[2].search (/\x01.*\x01/i) != -1) { e.type = "ctcp-reply"; e.destMethod = "onCTCPReply"; e.set = "server"; e.destObject = this; } else { e.msg = e.decodeParam(2, e.replyTo); e.destObject = e.replyTo; } return true; } CIRCServer.prototype.onPrivmsg = function serv_privmsg (e) { var targetName = e.params[1]; // 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) { targetName = targetName.substr(1); break; } } /* setting replyTo provides a standard place to find the target for */ /* replies associated with this event. */ if (arrayIndexOf(this.channelTypes, targetName[0]) != -1) { e.channel = new CIRCChannel(this, null, targetName); e.user = new CIRCChanUser(e.channel, e.user.unicodeName); e.replyTo = e.channel; e.set = "channel"; } else { e.set = "user"; e.replyTo = e.user; /* send replys to the user who sent the message */ } if (e.params[2].search (/\x01.*\x01/i) != -1) { e.type = "ctcp"; e.destMethod = "onCTCP"; e.set = "server"; e.destObject = this; } else { e.destObject = e.replyTo; e.msg = e.decodeParam(2, e.replyTo); } return true; } CIRCServer.prototype.onCTCPReply = function serv_ctcpr (e) { var ary = e.params[2].match (/^\x01(\S+) ?(.*)\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 serv_ctcp (e) { var ary = e.params[2].match (/^\x01(\S+) ?(.*)\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; return true; } CIRCServer.prototype.onCTCPClientinfo = function serv_ccinfo (e) { var clientinfo = new Array(); 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 serv_cact (e) { e.destObject = e.replyTo; e.set = (e.replyTo == e.user) ? "user" : "channel"; } CIRCServer.prototype.onCTCPFinger = function serv_cfinger (e) { e.user.ctcp("FINGER", this.parent.INITIAL_DESC, "NOTICE"); return true; } CIRCServer.prototype.onCTCPTime = function serv_cping (e) { e.user.ctcp("TIME", new Date(), "NOTICE"); return true; } CIRCServer.prototype.onCTCPVersion = function serv_cver (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 serv_csrc (e) { e.user.ctcp("SOURCE", this.SOURCE_RPLY, "NOTICE"); return true; } CIRCServer.prototype.onCTCPOs = function serv_os(e) { e.user.ctcp("OS", this.OS_RPLY, "NOTICE"); return true; } CIRCServer.prototype.onCTCPHost = function serv_host(e) { e.user.ctcp("HOST", this.HOST_RPLY, "NOTICE"); return true; } CIRCServer.prototype.onCTCPPing = function serv_cping (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 serv_dcc (e) { var ary = e.CTCPData.match (/(\S+)? ?(.*)/); 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 serv_dccchat (e) { var ary = e.DCCData.match (/(chat) (\d+) (\d+)/i); if (ary == null) return false; e.id = ary[2]; // Checky longword --> dotted IP conversion. var host = Number(e.id).toString(16); e.host = Number("0x" + host.substr(0, 2)) + "." + Number("0x" + host.substr(2, 2)) + "." + Number("0x" + host.substr(4, 2)) + "." + Number("0x" + host.substr(6, 2)); e.port = Number(ary[3]); e.destObject = e.replyTo; e.set = (e.replyTo == e.user) ? "user" : "channel"; return true; } CIRCServer.prototype.onDCCSend = function serv_dccsend (e) { var ary = e.DCCData.match(/(\S+) (\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]; // Cheeky longword --> dotted IP conversion. var host = Number(e.id).toString(16); e.host = Number("0x" + host.substr(0, 2)) + "." + Number("0x" + host.substr(2, 2)) + "." + Number("0x" + host.substr(4, 2)) + "." + Number("0x" + host.substr(6, 2)); e.port = Number(ary[3]); e.size = Number(ary[4]); e.destObject = e.replyTo; e.set = (e.replyTo == e.user) ? "user" : "channel"; return true; } /* * channel */ function CIRCChannel(parent, unicodeName, encodedName) { // Both unicodeName and encodedName are optional, but at least one must be // present. if (!encodedName && !unicodeName) throw "Hey! Come on, I need either an encoded or a Unicode name."; if (!encodedName) encodedName = fromUnicode(unicodeName, parent); var canonicalName = parent.toLowerCase(encodedName); if (canonicalName in parent.channels) return parent.channels[canonicalName]; this.parent = parent; this.encodedName = encodedName; this.canonicalName = canonicalName; this.unicodeName = unicodeName || toUnicode(encodedName, this); this.viewName = this.unicodeName; this.users = new Object(); this.bans = new Object(); this.excepts = new Object(); 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.canonicalName] = this; if ("onInit" in this) this.onInit(); return this; } CIRCChannel.prototype.TYPE = "IRCChannel"; CIRCChannel.prototype.topic = ""; CIRCChannel.prototype.getURL = function chan_geturl () { var target = this.encodedName; if ((target[0] == "#") && arrayIndexOf(this.parent.channelTypes, target[1]) == -1) { /* First character is "#" (which we're allowed to ommit), and the * following character is NOT a valid prefix, so it's safe to remove. */ target = ecmaEscape(target.substr(1)); } else { target = ecmaEscape(target); } target = target.replace("/", "%2f"); return this.parent.parent.getURL(target); } CIRCChannel.prototype.rehome = function chan_rehome(newParent) { delete this.parent.channels[this.canonicalName]; this.parent = newParent; this.parent.channels[this.canonicalName] = this; } CIRCChannel.prototype.addUser = function chan_adduser (unicodeName, modes) { return new CIRCChanUser(this, unicodeName, null, modes); } CIRCChannel.prototype.getUser = function chan_getuser(nick) { // Try assuming it's an encodedName first. nick = this.parent.toLowerCase(nick); if (nick in this.users) return this.users[nick]; // Ok, failed, so try assuming it's a unicodeName. nick = this.parent.toLowerCase(fromUnicode(nick, this.parent)); if (nick in this.users) return this.users[nick]; return null; } CIRCChannel.prototype.removeUser = function chan_removeuser(nick) { // Try assuming it's an encodedName first. nick = this.parent.toLowerCase(nick); if (nick in this.users) delete this.users[nick]; // see ya // Ok, failed, so try assuming it's a unicodeName. nick = this.parent.toLowerCase(fromUnicode(nick, this.parent)); if (nick in this.users) delete this.users[nick]; } CIRCChannel.prototype.getUsersLength = function chan_userslen (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 (arrayContains(this.users[p].modes, mode)) i++; } return i; } CIRCChannel.prototype.iAmOp = function chan_amop() { return this.active && this.users[this.parent.me.canonicalName].isOp; } CIRCChannel.prototype.iAmHalfOp = function chan_amhalfop() { return this.active && this.users[this.parent.me.canonicalName].isHalfOp; } CIRCChannel.prototype.iAmVoice = function chan_amvoice() { return this.active && this.users[this.parent.me.canonicalName].isVoice; } CIRCChannel.prototype.setTopic = function chan_topic (str) { this.parent.sendData ("TOPIC " + this.encodedName + " :" + fromUnicode(str, this) + "\n"); } CIRCChannel.prototype.say = function chan_say (msg) { this.parent.sayTo(this.encodedName, fromUnicode(msg, this)); } CIRCChannel.prototype.act = function chan_say (msg) { this.parent.actTo(this.encodedName, fromUnicode(msg, this)); } CIRCChannel.prototype.notice = function chan_notice (msg) { this.parent.noticeTo(this.encodedName, fromUnicode(msg, this)); } CIRCChannel.prototype.ctcp = function chan_ctcpto (code, msg, type) { msg = msg || ""; type = type || "PRIVMSG"; this.parent.ctcpTo(this.encodedName, fromUnicode(code, this), fromUnicode(msg, this), type); } CIRCChannel.prototype.join = function chan_join (key) { if (!key) key = ""; this.parent.sendData ("JOIN " + this.encodedName + " " + key + "\n"); return true; } CIRCChannel.prototype.part = function chan_part (reason) { if (!reason) reason = ""; this.parent.sendData ("PART " + this.encodedName + " :" + fromUnicode(reason, this) + "\n"); this.users = new Object(); return true; } /** * Invites a user to a channel. * * @param nick the user name to invite. */ CIRCChannel.prototype.invite = function chan_inviteuser (nick) { this.parent.sendData("INVITE " + nick + " " + this.encodedName + "\n"); return true; } /* * channel mode */ function CIRCChanMode (parent) { this.parent = parent; this.modeA = new Object(); this.modeB = new Object(); this.modeC = new Object(); this.modeD = new Object(); 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"; CIRCChanMode.prototype.getModeStr = function chan_modestr (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; } CIRCChanMode.prototype.setMode = function chanm_mode (modestr) { this.parent.parent.sendData ("MODE " + this.parent.encodedName + " " + modestr + "\n"); return true; } CIRCChanMode.prototype.setLimit = function chanm_limit (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; } CIRCChanMode.prototype.lock = function chanm_lock (k) { this.parent.parent.sendData("MODE " + this.parent.encodedName + " +k " + k + "\n"); return true; } CIRCChanMode.prototype.unlock = function chan_unlock (k) { this.parent.parent.sendData("MODE " + this.parent.encodedName + " -k " + k + "\n"); return true; } CIRCChanMode.prototype.setModerated = function chan_moderate (f) { var modifier = (f) ? "+" : "-"; this.parent.parent.sendData("MODE " + this.parent.encodedName + " " + modifier + "m\n"); return true; } CIRCChanMode.prototype.setPublicMessages = function chan_pmessages (f) { var modifier = (f) ? "-" : "+"; this.parent.parent.sendData("MODE " + this.parent.encodedName + " " + modifier + "n\n"); return true; } CIRCChanMode.prototype.setPublicTopic = function chan_ptopic (f) { var modifier = (f) ? "-" : "+"; this.parent.parent.sendData("MODE " + this.parent.encodedName + " " + modifier + "t\n"); return true; } CIRCChanMode.prototype.setInvite = function chan_invite (f) { var modifier = (f) ? "+" : "-"; this.parent.parent.sendData("MODE " + this.parent.encodedName + " " + modifier + "i\n"); return true; } CIRCChanMode.prototype.setPvt = function chan_pvt (f) { var modifier = (f) ? "+" : "-"; this.parent.parent.sendData("MODE " + this.parent.encodedName + " " + modifier + "p\n"); return true; } CIRCChanMode.prototype.setSecret = function chan_secret (f) { var modifier = (f) ? "+" : "-"; this.parent.parent.sendData("MODE " + this.parent.encodedName + " " + modifier + "s\n"); return true; } /* * user */ function CIRCUser(parent, unicodeName, encodedName, name, host) { // Both unicodeName and encodedName are optional, but at least one must be // present. if (!encodedName && !unicodeName) throw "Hey! Come on, I need either an encoded or a Unicode name."; if (!encodedName) encodedName = fromUnicode(unicodeName, parent); var canonicalName = parent.toLowerCase(encodedName); if (canonicalName in parent.users) { var existingUser = parent.users[canonicalName]; if (name) existingUser.name = name; if (host) existingUser.host = host; return existingUser; } this.parent = parent; this.encodedName = encodedName; this.canonicalName = canonicalName; this.unicodeName = unicodeName || toUnicode(encodedName, this.parent); this.viewName = this.unicodeName; this.name = name; this.host = host; this.desc = ""; this.connectionHost = null; this.isAway = false; this.modestr = this.parent.parent.INITIAL_UMODE; this.parent.users[this.canonicalName] = this; if ("onInit" in this) this.onInit(); return this; } CIRCUser.prototype.TYPE = "IRCUser"; CIRCUser.prototype.getURL = function usr_geturl () { var target = ecmaEscape(this.encodedName); target = target.replace("/", "%2f"); //target += "?charset=" + this.prefs["charset"]; return this.parent.parent.getURL(target) + ",isnick"; } CIRCUser.prototype.rehome = function usr_rehome(newParent) { delete this.parent.users[this.canonicalName]; this.parent = newParent; this.parent.users[this.canonicalName] = this; } CIRCUser.prototype.changeNick = function usr_changenick(unicodeName) { this.unicodeName = unicodeName; this.viewName = this.unicodeName; this.encodedName = fromUnicode(this.unicodeName, this.parent); this.canonicalName = this.parent.toLowerCase(this.encodedName); } CIRCUser.prototype.getHostMask = function usr_hostmask (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 usr_banmask() { if (!this.host) return this.unicodeName + "!*@*"; var hostmask = this.host; if (!/^\d+\.\d+\.\d+\.\d+$/.test(hostmask)) hostmask = hostmask.replace(/^[^.]+/, "*"); else hostmask = hostmask.replace(/[^.]+$/, "*"); return "*!" + this.name + "@" + hostmask; } CIRCUser.prototype.say = function usr_say (msg) { this.parent.sayTo(this.encodedName, fromUnicode(msg, this)); } CIRCUser.prototype.notice = function usr_notice (msg) { this.parent.noticeTo(this.encodedName, fromUnicode(msg, this)); } CIRCUser.prototype.act = function usr_act (msg) { this.parent.actTo(this.encodedName, fromUnicode(msg, this)); } CIRCUser.prototype.ctcp = function usr_ctcp (code, msg, type) { msg = msg || ""; type = type || "PRIVMSG"; this.parent.ctcpTo(this.encodedName, fromUnicode(code, this), fromUnicode(msg, this), type); } CIRCUser.prototype.whois = function usr_whois () { this.parent.whois(this.unicodeName); } /* * channel user */ function CIRCChanUser(parent, unicodeName, encodedName, modes) { // Both unicodeName and encodedName are optional, but at least one must be // present. if (!encodedName && !unicodeName) throw "Hey! Come on, I need either an encoded or a Unicode name."; 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. var canonicalName = parent.parent.toLowerCase(encodedName); if (canonicalName in parent.users) { var existingUser = parent.users[canonicalName]; 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] == "-") { if (arrayContains(existingUser.modes, mode)) { var i = arrayIndexOf(existingUser.modes, mode); arrayRemoveAt(existingUser.modes, i); } } else { if (!arrayContains(existingUser.modes, mode)) existingUser.modes.push(mode); } } } } existingUser.isFounder = (arrayContains(existingUser.modes, "q")) ? true : false; existingUser.isAdmin = (arrayContains(existingUser.modes, "a")) ? true : false; existingUser.isOp = (arrayContains(existingUser.modes, "o")) ? true : false; existingUser.isHalfOp = (arrayContains(existingUser.modes, "h")) ? true : false; existingUser.isVoice = (arrayContains(existingUser.modes, "v")) ? true : false; return existingUser; } var protoUser = new CIRCUser(parent.parent, unicodeName, encodedName); 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.parent = parent; this.TYPE = "IRCChanUser"; this.modes = new Array(); if (typeof modes != "undefined") this.modes = modes; this.isFounder = (arrayContains(this.modes, "q")) ? true : false; this.isAdmin = (arrayContains(this.modes, "a")) ? true : false; this.isOp = (arrayContains(this.modes, "o")) ? true : false; this.isHalfOp = (arrayContains(this.modes, "h")) ? true : false; this.isVoice = (arrayContains(this.modes, "v")) ? true : false; parent.users[this.canonicalName] = this; return this; } function cusr_geturl () { return this.parent.parent.getURL(ecmaEscape(this.unicodeName)) + ",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 (); }