diff options
-rw-r--r-- | irc.py | 945 |
1 files changed, 624 insertions, 321 deletions
@@ -12,6 +12,8 @@ import ssl import glob from collections import deque import iqueue as Queue +import chardet +import codecs class InvalidName(BaseException): @@ -118,7 +120,7 @@ class RejoinDelay(BaseException): pass _rfc1459casemapping = string.maketrans(string.ascii_uppercase + r'\[]~', - string.ascii_lowercase + r'|{}^') + string.ascii_lowercase + r'|{}^').decode("ISO-8859-2") # The IRC RFC does not permit the first character in a nickname to be a # numeral. However, this is not always adhered to. @@ -129,8 +131,7 @@ _chanmatch = r"^[%%s][^%s\s\n]*$" % re.escape("\x07,") _targchanmatch = r"^([%%s]?)([%%s][^%s\s\n]*)$" % re.escape("\x07,") _usermatch = r"^[A-Za-z0-9\-\^\`\\\|\_\{\}\[\]]+$" _realnamematch = r"^[^\n]*$" -_ircrecvmatch = r"^:(.+?)(?:!(.+?)@(.+?))?\s+(.+?)(?:\s+(.+?)(?:\s+(.+?))??)??(?:\s+:(.*))?$" -_ircsendmatch = r"^(.+?)(?:\s+(.+?)(?:\s+(.+?))??)??(?:\s+:(.*))?$" +_ircmatch = r"^(?::(.+?)(?:!(.+?)@(.+?))?\s+)?([A-Za-z0-9]+?)\s*(?:\s+(.+?)(?:\s+(.+?))??)??(?:\s+:(.*))?$" _ctcpmatch = "^\x01(.*?)(?:\\s+(.*?)\\s*)?\x01$" _prefixmatch = r"\((.*)\)(.*)" @@ -154,11 +155,11 @@ def timestamp(): class Connection(object): - def __init__(self, server, nick="ircbot", username="python", realname="Python IRC Library", passwd=None, port=None, ipv6=False, ssl=False, autoreconnect=True, log=sys.stderr, timeout=300, retrysleep=5, maxretries=15, onlogin=None): + def __init__(self, server, nick="ircbot", username="python", realname="Python IRC Library", passwd=None, port=None, ipv6=False, ssl=False, autoreconnect=True, log=sys.stderr, timeout=300, retrysleep=5, maxretries=15, onlogin=None, quietpingpong=True, pinginterval=60): self.__name__ = "pyIRC" - self.__version__ = "1.2" + self.__version__ = "1.3" self.__author__ = "Brian Sherson" - self.__date__ = "December 28, 2013" + self.__date__ = "February 8, 2014" if port == None: self.port = 6667 if not ssl else 6697 @@ -199,6 +200,8 @@ class Connection(object): self.maxretries = maxretries self.timeout = timeout self.retrysleep = retrysleep + self.quietpingpong = quietpingpong + self.pinginterval = pinginterval self._quitexpected = False self.log = log @@ -217,6 +220,8 @@ class Connection(object): self._recvhandlerthread = None # Initialize IRC environment variables + self.users = UserList(context=self) + self.channels = ChanList(context=self) self._init() def _init(self): @@ -226,8 +231,6 @@ class Connection(object): self.trynick = 0 self.identity = None - self.users = [] - self.channels = [] self.motdgreet = None self.motd = None @@ -258,10 +261,10 @@ class Connection(object): print >>self.log, "%s %s" % (ts, line) self.log.flush() - def logopen(self, filename): + def logopen(self, filename, encoding="utf8"): with self._loglock: ts = timestamp() - newlog = open(filename, "a") + newlog = codecs.open(filename, "a", encoding=encoding) if type(self.log) == file and not self.log.closed: if self.log not in (sys.stdout, sys.stderr): print >>self.log, "%s ### Log file closed" % (ts) @@ -270,7 +273,7 @@ class Connection(object): print >>self.log, "%s ### Log file opened" % (ts) self.log.flush() - def _event(self, method, modlist, exceptions=False, **params): + def _event(self, method, modlist, exceptions=False, data=None, **params): # Used to call event handlers on all attached addons, when applicable. handled = [] unhandled = [] @@ -280,25 +283,39 @@ class Connection(object): # Duplicate continue if method in dir(addon) and callable(getattr(addon, method)): - try: - getattr(addon, method)(self, **params) - except: - exc, excmsg, tb = sys.exc_info() - errors.append((addon, exc, excmsg, tb)) - - # Print to log AND stderr - self.logwrite(*["!!! Exception in addon %(addon)s" % vars()] + [ - "!!! %s" % line for line in traceback.format_exc().split("\n")]) - print >>sys.stderr, "Exception in addon %(addon)s" % vars() - print >>sys.stderr, traceback.format_exc() - if exceptions: # If set to true, we raise the exception. - raise - else: - handled.append(addon) + f = getattr(addon, method) + args = params + elif "onOther" in dir(addon) and callable(addon.onOther) and data: + f = addon.onOther + args = data + elif "onUnhandled" in dir(addon) and callable(addon.onUnhandled) and data: + # Backwards compatability for addons that still use + # onUnhandled. Use onOther in future development. + f = addon.onUnhandled + args = data else: unhandled.append(addon) + continue + try: + f(self, **args) + except: + exc, excmsg, tb = sys.exc_info() + errors.append((addon, exc, excmsg, tb)) + + # Print to log AND stderr + self.logwrite(*["!!! Exception in addon %(addon)s" % vars()] + [ + "!!! %s" % line for line in traceback.format_exc().split("\n")]) + print >>sys.stderr, "Exception in addon %(addon)s" % vars() + print >>sys.stderr, traceback.format_exc() + if exceptions: # If set to true, we raise the exception. + raise + else: + handled.append(addon) return (handled, unhandled, errors) + # TODO: Build method validation into the next two addons, Complain when a + # method is not callable or does not take in the expected arguments. + def addAddon(self, addon, trusted=False, **params): if addon in self.addons: raise BaseException, "Addon already added." @@ -328,12 +345,16 @@ class Connection(object): if addon in self.trusted: self.trusted.remove(addon) - def connect(self): + def connect(self, server=None, port=None, ssl=None, ipv6=None): if self.isAlive(): raise AlreadyConnected + with self._sendline: + self._outgoing.clear() with self.lock: - self._recvhandlerthread = Thread(target=self._recvhandler) - self._sendhandlerthread = Thread(target=self._sendhandler) + self._recvhandlerthread = Thread( + target=self._recvhandler, name="Receive Handler", kwargs=dict(server=None, port=None, ssl=None, ipv6=None)) + self._sendhandlerthread = Thread( + target=self._sendhandler, name="Send Handler") self._recvhandlerthread.start() self._sendhandlerthread.start() @@ -382,18 +403,19 @@ class Connection(object): self._connected = True def _procrecvline(self, line): - # If received PING, then just pong back transparently, bypassing - # _outgoingthread. - ping = re.findall("^PING :?(.*)$", line) - if len(ping): - with self.lock: - self._connection.send("PONG :%s\n" % ping[0]) - return - - self.logwrite("<<< %s" % line) + # If received PING, then just pong back transparently, bypassing _outgoingthread. + #ping=re.findall("^PING :?(.*)$", line) + # if len(ping): + # if not self.quietpingpong: + #self.logwrite("<<< %s" % line) + # with self.lock: + #self._connection.send("PONG :%s\n" % ping[0]) + # if not self.quietpingpong: + #self.logwrite(">>> %s" % "PONG :%s" % ping[0]) + # return # Attempts to match against pattern ":src cmd target params :extinfo" - matches = re.findall(_ircrecvmatch, line) + matches = re.findall(_ircmatch, line) # We have a match! if len(matches): @@ -406,10 +428,16 @@ class Connection(object): else: cmd = cmd.upper() + if cmd not in ("PING", "PONG") or not self.quietpingpong: + self.logwrite("<<< %s" % line) + + if origin == "" and cmd == "PING": + self._send(u"PONG :%s" % extinfo) + with self.lock: if not self._registered: if type(cmd) == int and cmd != 451 and target != "*": # Registration complete! - self.identity = self.user(target) + self.identity = self.user(target, init=True) self.serv = origin self._event("onRegistered", self.addons + reduce( lambda x, y: x + y, [chan.addons for chan in self.channels], [])) @@ -447,12 +475,13 @@ class Connection(object): else: targetprefix = "" - data = dict(origin=origin, cmd=cmd, target=target, + data = dict(line=line, origin=origin, cmd=cmd, target=target, targetprefix=targetprefix, params=params, extinfo=extinfo) # Major codeblock here! Track IRC state. # Send line to addons having onRecv method first - self._event("onRecv", self.addons, line=line, **data) + if cmd not in ("PING", "PONG") or not self.quietpingpong: + self._event("onRecv", self.addons, **data) # Support for further addon events is taken care of here. Each invocation of self._event will return (handled, unhandled, exceptions), # where handled is the list of addons that have an event handler, and was executed without error, unhandled gives the list of addons @@ -461,20 +490,20 @@ class Connection(object): # deadlock. if cmd == 1: - (handled, unhandled, exceptions) = self._event("onWelcome", self.addons + reduce( - lambda x, y: x + y, [chan.addons for chan in self.channels], []), origin=origin, msg=extinfo) + self._event("onWelcome", self.addons + reduce( + lambda x, y: x + y, [chan.addons for chan in self.channels], []), origin=origin, msg=extinfo, data=data) self.welcome = extinfo # Welcome message elif cmd == 2: - (handled, unhandled, exceptions) = self._event("onYourHost", self.addons + reduce( - lambda x, y: x + y, [chan.addons for chan in self.channels], []), origin=origin, msg=extinfo) + self._event("onYourHost", self.addons + reduce( + lambda x, y: x + y, [chan.addons for chan in self.channels], []), origin=origin, msg=extinfo, data=data) self.hostinfo = extinfo # Your Host elif cmd == 3: - (handled, unhandled, exceptions) = self._event("onServerCreated", self.addons + reduce( - lambda x, y: x + y, [chan.addons for chan in self.channels], []), origin=origin, msg=extinfo) + self._event("onServerCreated", self.addons + reduce( + lambda x, y: x + y, [chan.addons for chan in self.channels], []), origin=origin, msg=extinfo, data=data) self.servcreated = extinfo # Server Created elif cmd == 4: - (handled, unhandled, exceptions) = self._event("onServInfo", self.addons + reduce( - lambda x, y: x + y, [chan.addons for chan in self.channels], []), origin=origin, servinfo=params) + self._event("onServInfo", self.addons + reduce( + lambda x, y: x + y, [chan.addons for chan in self.channels], []), origin=origin, servinfo=params, data=data) self.servinfo = params # What is this code? elif cmd == 5: # Server Supports support = dict( @@ -488,8 +517,8 @@ class Connection(object): else: del support[ "PREFIX"] # Might as well delete the info if it doesn't match expected pattern - (handled, unhandled, exceptions) = self._event("onSupports", self.addons + reduce( - lambda x, y: x + y, [chan.addons for chan in self.channels], []), origin=origin, supports=support) + self._event("onSupports", self.addons + reduce( + lambda x, y: x + y, [chan.addons for chan in self.channels], []), origin=origin, supports=support, data=data) self.supports.update(support) if "serv005" in dir(self) and type(self.serv005) == list: self.serv005.append(params) @@ -497,129 +526,140 @@ class Connection(object): self.serv005 = [params] elif cmd == 8: # Snomask snomask = params.lstrip("+") - (handled, unhandled, exceptions) = self._event("onSnoMask", self.addons + reduce( - lambda x, y: x + y, [chan.addons for chan in self.channels], []), origin=origin, snomask=snomask) + self._event("onSnoMask", self.addons + reduce( + lambda x, y: x + y, [chan.addons for chan in self.channels], []), origin=origin, snomask=snomask, data=data) self.identity.snomask = snomask if "s" not in self.identity.modes: self.snomask = "" elif cmd == 221: # User Modes modes = (params if params else extinfo).lstrip("+") - (handled, unhandled, exceptions) = self._event("onUserModes", self.addons + reduce( - lambda x, y: x + y, [chan.addons for chan in self.channels], []), origin=origin, snomask=modes) + self._event("onUserModes", self.addons + reduce( + lambda x, y: x + y, [chan.addons for chan in self.channels], []), origin=origin, snomask=modes, data=data) self.identity.modes = modes if "s" not in self.identity.modes: self.snomask = "" elif cmd == 251: # Net Stats - (handled, unhandled, exceptions) = self._event( - "onNetStats", self.addons, origin=origin, netstats=extinfo) + self._event( + "onNetStats", self.addons, origin=origin, netstats=extinfo, data=data) self.netstats = extinfo elif cmd == 252: opcount = int(params) - (handled, unhandled, exceptions) = self._event( - "onOpCount", self.addons, origin=origin, opcount=opcount) + self._event( + "onOpCount", self.addons, origin=origin, opcount=opcount, data=data) self.opcount = opcount elif cmd == 254: chancount = int(params) - (handled, unhandled, exceptions) = self._event( - "onChanCount", self.addons, origin=origin, chancount=chancount) + self._event( + "onChanCount", self.addons, origin=origin, chancount=chancount, data=data) self.chancount = chancount elif cmd == 305: # Returned from away status - (handled, unhandled, exceptions) = self._event( - "onReturn", self.addons, origin=origin, msg=extinfo) + self._event( + "onReturn", self.addons, origin=origin, msg=extinfo, data=data) self.identity.away = False elif cmd == 306: # Entered away status - (handled, unhandled, exceptions) = self._event( - "onAway", self.addons, origin=origin, msg=extinfo) + self._event( + "onAway", self.addons, origin=origin, msg=extinfo, data=data) self.identity.away = True elif cmd == 311: # Start of WHOIS data nickname, username, host, star = params.split() user = self.user(nickname) - (handled, unhandled, exceptions) = self._event("onWhoisStart", self.addons, origin=origin, - user=user, nickname=nickname, username=username, host=host, realname=extinfo) + self._event( + "onWhoisStart", self.addons, origin=origin, user=user, + nickname=nickname, username=username, host=host, realname=extinfo, data=data) user.nick = nickname user.username = username user.host = host elif cmd == 301: # Away Message user = self.user(params) - (handled, unhandled, exceptions) = self._event( - "onWhoisAway", self.addons, origin=origin, user=user, nickname=params, awaymsg=extinfo) + self._event("onWhoisAway", self.addons, origin=origin, + user=user, nickname=params, awaymsg=extinfo, data=data) user.away = True user.awaymsg = extinfo elif cmd == 303: # ISON Reply users = [self.user(user) for user in extinfo.split(" ")] - (handled, unhandled, exceptions) = self._event( - "onIsonReply", self.addons, origin=origin, isonusers=users) + self._event( + "onIsonReply", self.addons, origin=origin, isonusers=users, data=data) elif cmd == 307: # Is a registered nick - (handled, unhandled, exceptions) = self._event("onWhoisRegisteredNick", - self.addons, origin=origin, user=self.user(params), nickname=params, msg=extinfo) + self._event( + "onWhoisRegisteredNick", self.addons, origin=origin, + user=self.user(params), nickname=params, msg=extinfo, data=data) elif cmd == 378: # Connecting From - (handled, unhandled, exceptions) = self._event("onWhoisConnectingFrom", - self.addons, origin=origin, user=self.user(params), nickname=params, msg=extinfo) + self._event( + "onWhoisConnectingFrom", self.addons, origin=origin, + user=self.user(params), nickname=params, msg=extinfo, data=data) elif cmd == 319: # Channels - (handled, unhandled, exceptions) = self._event("onWhoisChannels", self.addons, - origin=origin, user=self.user(params), nickname=params, chanlist=extinfo.split(" ")) + self._event("onWhoisChannels", self.addons, origin=origin, user=self.user( + params), nickname=params, chanlist=extinfo.split(" "), data=data) elif cmd == 310: # Availability - (handled, unhandled, exceptions) = self._event("onWhoisAvailability", - self.addons, origin=origin, user=self.user(params), nickname=params, msg=extinfo) + self._event( + "onWhoisAvailability", self.addons, origin=origin, + user=self.user(params), nickname=params, msg=extinfo, data=data) elif cmd == 312: # Server nickname, server = params.split(" ") user = self.user(nickname) - (handled, unhandled, exceptions) = self._event("onWhoisServer", self.addons, - origin=origin, user=user, nickname=nickname, server=server, servername=extinfo) + self._event( + "onWhoisServer", self.addons, origin=origin, user=user, + nickname=nickname, server=server, servername=extinfo, data=data) user.server = server elif cmd == 313: # IRC Op user = self.user(params) - (handled, unhandled, exceptions) = self._event( - "onWhoisOp", self.addons, origin=origin, user=user, nickname=params, msg=extinfo) + self._event("onWhoisOp", self.addons, origin=origin, + user=user, nickname=params, msg=extinfo, data=data) user.ircop = True user.ircopmsg = extinfo elif cmd == 317: # Idle and Signon times nickname, idletime, signontime = params.split(" ") user = self.user(nickname) - (handled, unhandled, exceptions) = self._event("onWhoisTimes", self.addons, origin=origin, - user=user, nickname=nickname, idletime=int(idletime), signontime=int(signontime), msg=extinfo) + self._event( + "onWhoisTimes", self.addons, origin=origin, user=user, nickname=nickname, + idletime=int(idletime), signontime=int(signontime), msg=extinfo, data=data) user.idlesince = int(time.time()) - int(idletime) user.signontime = int(signontime) elif cmd == 671: # SSL user = self.user(params) - (handled, unhandled, exceptions) = self._event( - "onWhoisSSL", self.addons, origin=origin, user=user, nickname=params, msg=extinfo) + self._event("onWhoisSSL", self.addons, origin=origin, + user=user, nickname=params, msg=extinfo, data=data) user.ssl = True elif cmd == 379: # User modes - (handled, unhandled, exceptions) = self._event("onWhoisModes", self.addons, - origin=origin, user=self.user(params), nickname=params, msg=extinfo) + self._event("onWhoisModes", self.addons, origin=origin, user=self.user( + params), nickname=params, msg=extinfo, data=data) elif cmd == 330: # Logged in as nickname, loggedinas = params.split(" ") user = self.user(nickname) - (handled, unhandled, exceptions) = self._event("onWhoisLoggedInAs", self.addons, - origin=origin, user=user, nickname=nickname, loggedinas=loggedinas, msg=extinfo) + self._event( + "onWhoisLoggedInAs", self.addons, origin=origin, user=user, + nickname=nickname, loggedinas=loggedinas, msg=extinfo, data=data) user.loggedinas = loggedinas elif cmd == 318: # End of WHOIS - (handled, unhandled, exceptions) = self._event("onWhoisEnd", self.addons, - origin=origin, user=self.user(params), nickname=params, msg=extinfo) + try: + user = self.user(params) + except InvalidName: + user = params + self._event("onWhoisEnd", self.addons, origin=origin, + user=user, nickname=params, msg=extinfo, data=data) elif cmd == 321: # Start LIST - (handled, unhandled, exceptions) = self._event( - "onListStart", self.addons, origin=origin, params=params, extinfo=extinfo) + self._event( + "onListStart", self.addons, origin=origin, params=params, extinfo=extinfo, data=data) elif cmd == 322: # LIST item (chan, pop) = params.split(" ", 1) - (handled, unhandled, exceptions) = self._event("onListEntry", self.addons, - origin=origin, channel=self.channel(chan), population=int(pop), extinfo=extinfo) + self._event("onListEntry", self.addons, origin=origin, channel=self.channel( + chan), population=int(pop), extinfo=extinfo, data=data) elif cmd == 323: # End of LIST - (handled, unhandled, exceptions) = self._event( - "onListEnd", self.addons, origin=origin, endmsg=extinfo) + self._event( + "onListEnd", self.addons, origin=origin, endmsg=extinfo, data=data) elif cmd == 324: # Channel Modes modeparams = params.split() channame = modeparams.pop(0) channel = self.channel(channame) - self._event("onRecv", channel.addons, line=line, **data) + self._event("onRecv", channel.addons, **data) if channel.name != channame: channel.name = channame # Server seems to have changed the idea of the case of the channel name setmodes = modeparams.pop(0) @@ -632,8 +672,8 @@ class Connection(object): modedelta.append(("+%s" % mode, param)) elif mode in self.supports["CHANMODES"][3]: modedelta.append(("+%s" % mode, None)) - (handled, unhandled, exceptions) = self._event( - "onChannelModes", self.addons + channel.addons, channel=channel, modedelta=modedelta) + self._event("onChannelModes", self.addons + channel.addons, + channel=channel, modedelta=modedelta, data=data) for ((modeset, mode), param) in modedelta: if mode in self.supports["CHANMODES"][2]: channel.modes[mode] = param @@ -644,25 +684,26 @@ class Connection(object): channame, created = params.split() created = int(created) channel = self.channel(channame) - self._event("onRecv", channel.addons, line=line, **data) - (handled, unhandled, exceptions) = self._event( - "onChanCreated", self.addons + channel.addons, channel=channel, created=created) + self._event("onRecv", channel.addons, **data) + self._event("onChanCreated", self.addons + channel.addons, + channel=channel, created=created, data=data) channel.created = int(created) elif cmd == 332: # Channel Topic channame = params channel = self.channel(channame) - self._event("onRecv", channel.addons, line=line, **data) - (handled, unhandled, exceptions) = self._event( - "onTopic", self.addons + channel.addons, origin=origin, channel=channel, topic=extinfo) + self._event("onRecv", channel.addons, **data) + self._event("onTopic", self.addons + channel.addons, + origin=origin, channel=channel, topic=extinfo, data=data) channel.topic = extinfo elif cmd == 333: # Channel Topic info (channame, nick, dt) = params.split() channel = self.channel(channame) - self._event("onRecv", channel.addons, line=line, **data) - (handled, unhandled, exceptions) = self._event("onTopicInfo", self.addons + - channel.addons, origin=origin, channel=channel, topicsetby=nick, topictime=int(dt)) + self._event("onRecv", channel.addons, **data) + self._event( + "onTopicInfo", self.addons + channel.addons, origin=origin, + channel=channel, topicsetby=nick, topictime=int(dt), data=data) channel.topicsetby = nick channel.topictime = int(dt) @@ -683,9 +724,15 @@ class Connection(object): user = self.user(nick) - self._event("onRecv", channel.addons, line=line, **data) - (handled, unhandled, exceptions) = self._event("onWhoEntry", self.addons + channel.addons, origin=origin, channel=channel, - user=user, channame=channame, username=username, host=host, serv=serv, nick=nick, flags=flags, hops=int(hops), realname=realname) + if type(channel) == Channel: + self._event("onRecv", channel.addons, **data) + self._event( + "onWhoEntry", self.addons + channel.addons, origin=origin, channel=channel, user=user, channame=channame, + username=username, host=host, serv=serv, nick=nick, flags=flags, hops=int(hops), realname=realname, data=data) + else: + self._event( + "onWhoEntry", self.addons, origin=origin, channel=channel, user=user, channame=channame, + username=username, host=host, serv=serv, nick=nick, flags=flags, hops=int(hops), realname=realname, data=data) user.hops = hops user.realname = realname user.username = username @@ -709,16 +756,16 @@ class Connection(object): chantypes = self.supports.get("CHANTYPES", "&#+!") if re.match(_chanmatch % re.escape(chantypes), params): channel = self.channel(params) - (handled, unhandled, exceptions) = self._event( - "onWhoEnd", self.addons + channel.addons, origin=origin, param=params, endmsg=extinfo) + self._event("onWhoEnd", self.addons + channel.addons, + origin=origin, param=params, endmsg=extinfo, data=data) else: - (handled, unhandled, exceptions) = self._event( - "onWhoEnd", self.addons, origin=origin, param=params, endmsg=extinfo) + self._event( + "onWhoEnd", self.addons, origin=origin, param=params, endmsg=extinfo, data=data) elif cmd == 353: # NAMES reply (flag, channame) = params.split() channel = self.channel(channame) - self._event("onRecv", channel.addons, line=line, **data) + self._event("onRecv", channel.addons, **data) if self.supports.has_key("PREFIX"): names = re.findall( @@ -730,8 +777,9 @@ class Connection(object): # Still put it into tuple form for # compatibility in the next # structure - (handled, unhandled, exceptions) = self._event("onNames", self.addons + channel.addons, - origin=origin, channel=channel, flag=flag, channame=channame, nameslist=names) + self._event( + "onNames", self.addons + channel.addons, origin=origin, + channel=channel, flag=flag, channame=channame, nameslist=names, data=data) for (symbs, nick, username, host) in names: user = self.user(nick) @@ -757,27 +805,28 @@ class Connection(object): elif cmd == 366: # End of NAMES reply channel = self.channel(params) - (handled, unhandled, exceptions) = self._event("onNamesEnd", self.addons + - channel.addons, origin=origin, channel=channel, channame=params, endmsg=extinfo) + self._event( + "onNamesEnd", self.addons + channel.addons, origin=origin, + channel=channel, channame=params, endmsg=extinfo, data=data) elif cmd == 372: # MOTD line - (handled, unhandled, exceptions) = self._event( - "onMOTDLine", self.addons, origin=origin, motdline=extinfo) + self._event( + "onMOTDLine", self.addons, origin=origin, motdline=extinfo, data=data) self.motd.append(extinfo) elif cmd == 375: # Begin MOTD - (handled, unhandled, exceptions) = self._event( - "onMOTDStart", self.addons, origin=origin, motdgreet=extinfo) + self._event( + "onMOTDStart", self.addons, origin=origin, motdgreet=extinfo, data=data) self.motdgreet = extinfo self.motd = [] elif cmd == 376: - (handled, unhandled, exceptions) = self._event( - "onMOTDEnd", self.addons, origin=origin, motdend=extinfo) + self._event( + "onMOTDEnd", self.addons, origin=origin, motdend=extinfo, data=data) self.motdend = extinfo # End of MOTD elif cmd == 386 and "q" in self.supports["PREFIX"][0]: # Channel Owner (Unreal) (channame, owner) = params.split() channel = self.channel(channame) - self._event("onRecv", channel.addons, line=line, **data) + self._event("onRecv", channel.addons, **data) if channel.name != channame: channel.name = channame # Server seems to have changed the idea of the case of the channel name user = self.user(owner) @@ -792,7 +841,7 @@ class Connection(object): elif cmd == 388 and "a" in self.supports["PREFIX"][0]: # Channel Admin (Unreal) (channame, admin) = params.split() channel = self.channel(channame) - self._event("onRecv", channel.addons, line=line, **data) + self._event("onRecv", channel.addons, **data) if channel.name != channame: channel.name = channame # Server seems to have changed the idea of the case of the channel name user = self.user(admin) @@ -809,11 +858,11 @@ class Connection(object): addons = reduce( lambda x, y: x + y, [chan.addons for chan in origin.channels], []) - self._event("onRecv", addons, line=line, **data) - (handled, unhandled, exceptions) = self._event( - "onNickChange", self.addons + addons, user=origin, newnick=newnick) + self._event("onRecv", addons, **data) + self._event( + "onNickChange", self.addons + addons, user=origin, newnick=newnick, data=data) if origin == self.identity: - (handled, unhandled, exceptions) = self._event( + self._event( "onMeNickChange", self.addons + addons, newnick=newnick) for u in self.users: @@ -831,9 +880,9 @@ class Connection(object): elif cmd == "JOIN": channel = target if type( target) == Channel else self.channel(extinfo) - self._event("onRecv", channel.addons, line=line, **data) - (handled, unhandled, exceptions) = self._event( - "onJoin", self.addons + channel.addons, user=origin, channel=channel) + self._event("onRecv", channel.addons, **data) + self._event( + "onJoin", self.addons + channel.addons, user=origin, channel=channel, data=data) if origin == self.identity: # This means the bot is entering the room, # and will reset all the channel data, on the assumption that such data may have changed. @@ -842,17 +891,14 @@ class Connection(object): if channel._joinrequested: channel._joinreply = cmd channel._joining.notify() - channel.topic = "" - channel.topicmod = "" - channel.modes = {} - channel.users = [] + channel._init() self._event( "onMeJoin", self.addons + channel.addons, channel=channel) - self.raw("MODE %s" % channel.name) - self.raw("WHO %s" % channel.name) + self._send(u"MODE %s" % channel.name) + self._send(u"WHO %s" % channel.name) if "CHANMODES" in self.supports.keys(): - self.raw( - "MODE %s :%s" % (channel.name, self.supports["CHANMODES"][0])) + self._send( + u"MODE %s :%s" % (channel.name, self.supports["CHANMODES"][0])) if channel not in origin.channels: origin.channels.append(channel) @@ -864,15 +910,16 @@ class Connection(object): if kicked.nick != params: kicked.nick = params - self._event("onRecv", target.addons, line=line, **data) + self._event("onRecv", target.addons, **data) if origin == self.identity: self._event( "onMeKick", self.addons + target.addons, channel=target, kicked=kicked, kickmsg=extinfo) if kicked == self.identity: self._event("onMeKicked", self.addons + target.addons, kicker=origin, channel=target, kickmsg=extinfo) - (handled, unhandled, exceptions) = self._event("onKick", self.addons + - target.addons, kicker=origin, channel=target, kicked=kicked, kickmsg=extinfo) + self._event( + "onKick", self.addons + target.addons, kicker=origin, + channel=target, kicked=kicked, kickmsg=extinfo, data=data) if target in kicked.channels: kicked.channels.remove(target) @@ -885,16 +932,16 @@ class Connection(object): elif cmd == "PART": try: - self._event("onRecv", target.addons, line=line, **data) + self._event("onRecv", target.addons, **data) if origin == self.identity: - with target ._parting: + with target._parting: if target._partrequested: target._partreply = cmd target._parting.notify() self._event( "onMePart", self.addons + target.addons, channel=target, partmsg=extinfo) - (handled, unhandled, exceptions) = self._event( - "onPart", self.addons + target.addons, user=origin, channel=target, partmsg=extinfo) + self._event("onPart", self.addons + target.addons, + user=origin, channel=target, partmsg=extinfo, data=data) if target in origin.channels: origin.channels.remove(target) @@ -911,9 +958,9 @@ class Connection(object): channels = list(origin.channels) addons = reduce( lambda x, y: x + y, [chan.addons for chan in origin.channels], []) - self._event("onRecv", addons, line=line, **data) - (handled, unhandled, exceptions) = self._event( - "onQuit", self.addons + addons, user=origin, quitmsg=extinfo) + self._event("onRecv", addons, **data) + self._event( + "onQuit", self.addons + addons, user=origin, quitmsg=extinfo, data=data) for channel in origin.channels: with channel.lock: if origin in channel.users: @@ -926,7 +973,7 @@ class Connection(object): elif cmd == "MODE": if type(target) == Channel: - self._event("onRecv", target.addons, line=line, **data) + self._event("onRecv", target.addons, **data) modedelta = [] modeparams = params.split() setmodes = modeparams.pop(0) @@ -943,9 +990,13 @@ class Connection(object): if modeset == "+": eventname = _maskmodeeventnames[ mode][0] + if mode == "k": + target.key = param if modeset == "-": eventname = _maskmodeeventnames[ mode][1] + if mode == "k": + target.key = None matchesbot = glob.fnmatch.fnmatch( "%s!%s@%s".lower() % (self.identity.nick, self.identity.username, self.identity.host), param.lower()) self._event( @@ -981,8 +1032,9 @@ class Connection(object): "onMe%s" % eventname, self.addons + target.addons, user=origin, channel=target) modedelta.append( ("%s%s" % (modeset, mode), modeuser)) - (handled, unhandled, exceptions) = self._event( - "onChanModeSet", self.addons + target.addons, user=origin, channel=target, modedelta=modedelta) + self._event( + "onChanModeSet", self.addons + target.addons, + user=origin, channel=target, modedelta=modedelta, data=data) with target.lock: for ((modeset, mode), param) in modedelta: if mode in self.supports["CHANMODES"][0]: @@ -1035,23 +1087,42 @@ class Connection(object): target.modes[mode] = [param] elif target.modes.has_key(mode) and param in target.modes[mode]: target.modes[mode].remove(param) - elif type(target) == User: + elif target == self.identity: modeparams = (params if params else extinfo).split() setmodes = modeparams.pop(0) + modedelta = [] modeset = "+" for mode in setmodes: if mode in "+-": modeset = mode continue if modeset == "+": + if mode == "s": + if len(modeparams): + snomask = modeparams.pop(0) + snomaskdelta = [] + snomodeset = "+" + for snomode in snomask: + if snomode in "+-": + snomodeset = snomode + continue + snomaskdelta.append( + "%s%s" % (snomodeset, snomode)) + modedelta.append(("+s", snomaskdelta)) + else: + modedelta.append(("+s", [])) + else: + modedelta.append(("+%s" % mode, None)) + if modeset == "-": + modedelta.append(("-%s" % mode, None)) + self._event( + "onUserModeSet", self.addons, origin=origin, modedelta=modedelta, data=data) + for ((modeset, mode), param) in modedelta: + if modeset == "+": if mode not in target.modes: target.modes += mode - if mode == "s" and len(modeparams): - snomask = modeparams.pop(0) - for snomode in snomask: - if snomode in "+-": - snomodeset = snomode - continue + if mode == "s": + for snomodeset, snomode in param: if snomodeset == "+" and snomode not in target.snomask: target.snomask += snomode if snomodeset == "-" and snomode in target.snomask: @@ -1065,9 +1136,9 @@ class Connection(object): target.snomask = "" elif cmd == "TOPIC": - self._event("onRecv", target.addons, line=line, **data) - (handled, unhandled, exceptions) = self._event( - "onTopicSet", self.addons + target.addons, user=origin, channel=target, topic=extinfo) + self._event("onRecv", target.addons, **data) + self._event("onTopicSet", self.addons + target.addons, + user=origin, channel=target, topic=extinfo, data=data) with target.lock: target.topic = extinfo @@ -1076,13 +1147,13 @@ class Connection(object): elif cmd == "INVITE": channel = self.channel(extinfo if extinfo else params) - self._event("onRecv", channel.addons, line=line, **data) - (handled, unhandled, exceptions) = self._event( - "onInvite", self.addons + channel.addons, user=origin, channel=channel) + self._event("onRecv", channel.addons, **data) + self._event( + "onInvite", self.addons + channel.addons, user=origin, channel=channel, data=data) elif cmd == "PRIVMSG": if type(target) == Channel: - self._event("onRecv", target.addons, line=line, **data) + self._event("onRecv", target.addons, **data) # CTCP handling ctcp = re.findall(_ctcpmatch, extinfo) @@ -1090,18 +1161,20 @@ class Connection(object): (ctcptype, ext) = ctcp[0] if ctcptype.upper() == "ACTION": if type(target) == Channel: - (handled, unhandled, exceptions) = self._event( - "onChanAction", self.addons + target.addons, user=origin, channel=target, targetprefix=targetprefix, action=ext) + self._event( + "onChanAction", self.addons + target.addons, user=origin, + channel=target, targetprefix=targetprefix, action=ext, data=data) elif target == self.identity: - (handled, unhandled, exceptions) = self._event( - "onPrivAction", self.addons, user=origin, action=ext) + self._event( + "onPrivAction", self.addons, user=origin, action=ext, data=data) else: if type(target) == Channel: - (handled, unhandled, exceptions) = self._event("onChanCTCP", self.addons + target.addons, - user=origin, channel=target, targetprefix=targetprefix, ctcptype=ctcptype, params=ext) + self._event( + "onChanCTCP", self.addons + target.addons, user=origin, channel=target, + targetprefix=targetprefix, ctcptype=ctcptype, params=ext, data=data) elif target == self.identity: - (handled, unhandled, exceptions) = self._event( - "onPrivCTCP", self.addons, user=origin, ctcptype=ctcptype, params=ext) + self._event( + "onPrivCTCP", self.addons, user=origin, ctcptype=ctcptype, params=ext, data=data) if ctcptype.upper() == "VERSION": origin.ctcpreply("VERSION", self.ctcpversion()) if ctcptype.upper() == "TIME": @@ -1115,36 +1188,39 @@ class Connection(object): origin.ctcpreply("FINGER", "%(ext)s" % vars()) else: if type(target) == Channel: - (handled, unhandled, exceptions) = self._event( - "onChanMsg", self.addons + target.addons, user=origin, channel=target, targetprefix=targetprefix, msg=extinfo) + self._event( + "onChanMsg", self.addons + target.addons, user=origin, + channel=target, targetprefix=targetprefix, msg=extinfo, data=data) elif target == self.identity: - (handled, unhandled, exceptions) = self._event( - "onPrivMsg", self.addons, user=origin, msg=extinfo) + self._event( + "onPrivMsg", self.addons, user=origin, msg=extinfo, data=data) elif cmd == "NOTICE": if type(target) == Channel: - self._event("onRecv", target.addons, line=line, **data) + self._event("onRecv", target.addons, **data) # CTCP handling ctcp = re.findall(_ctcpmatch, extinfo) if ctcp and target == self.identity: (ctcptype, ext) = ctcp[0] - (handled, unhandled, exceptions) = self._event( - "onCTCPReply", self.addons, origin=origin, ctcptype=ctcptype, params=ext) + self._event( + "onCTCPReply", self.addons, origin=origin, ctcptype=ctcptype, params=ext, data=data) else: if type(target) == Channel: - (handled, unhandled, exceptions) = self._event( - "onChanNotice", self.addons + target.addons, origin=origin, channel=target, targetprefix=targetprefix, msg=extinfo) + self._event( + "onChanNotice", self.addons + target.addons, origin=origin, + channel=target, targetprefix=targetprefix, msg=extinfo, data=data) elif target == self.identity: - (handled, unhandled, exceptions) = self._event( - "onPrivNotice", self.addons, origin=origin, msg=extinfo) + self._event( + "onPrivNotice", self.addons, origin=origin, msg=extinfo, data=data) elif cmd == 367: # Channel Ban list (channame, mask, setby, settime) = params.split() channel = self.channel(channame) - self._event("onRecv", channel.addons, line=line, **data) - (handled, unhandled, exceptions) = self._event("onBanListEntry", self.addons + channel.addons, - origin=origin, channel=channel, mask=mask, setby=setby, settime=int(settime)) + self._event("onRecv", channel.addons, **data) + self._event( + "onBanListEntry", self.addons + channel.addons, origin=origin, + channel=channel, mask=mask, setby=setby, settime=int(settime), data=data) if "b" in channel.modes.keys(): if mask.lower() not in [m.lower() for (m, s, t) in channel.modes["b"]]: channel.modes["b"].append( @@ -1153,16 +1229,17 @@ class Connection(object): channel.modes["b"] = [(mask, setby, int(settime))] elif cmd == 368: channel = self.channel(params) - self._event("onRecv", channel.addons, line=line, **data) - (handled, unhandled, exceptions) = self._event( - "onBanListEnd", self.addons + channel.addons, origin=origin, channel=channel, endmsg=extinfo) + self._event("onRecv", channel.addons, **data) + self._event("onBanListEnd", self.addons + channel.addons, + origin=origin, channel=channel, endmsg=extinfo, data=data) elif cmd == 346: # Channel Invite list (channame, mask, setby, settime) = params.split() channel = self.channel(channame) - self._event("onRecv", channel.addons, line=line, **data) - (handled, unhandled, exceptions) = self._event("onInviteListEntry", self.addons + - channel.addons, origin=origin, channel=channel, mask=mask, setby=setby, settime=int(settime)) + self._event("onRecv", channel.addons, **data) + self._event( + "onInviteListEntry", self.addons + channel.addons, origin=origin, + channel=channel, mask=mask, setby=setby, settime=int(settime), data=data) if "I" in channel.modes.keys(): if mask.lower() not in [m.lower() for (m, s, t) in channel.modes["I"]]: channel.modes["I"].append( @@ -1171,16 +1248,18 @@ class Connection(object): channel.modes["I"] = [(mask, setby, int(settime))] elif cmd == 347: channel = self.channel(params) - self._event("onRecv", channel.addons, line=line, **data) - (handled, unhandled, exceptions) = self._event( - "onInviteListEnd", self.addons + channel.addons, origin=origin, channel=channel, endmsg=extinfo) + self._event("onRecv", channel.addons, **data) + self._event( + "onInviteListEnd", self.addons + channel.addons, + origin=origin, channel=channel, endmsg=extinfo, data=data) elif cmd == 348: # Channel Ban Exception list (channame, mask, setby, settime) = params.split() channel = self.channel(channame) - self._event("onRecv", channel.addons, line=line, **data) - (handled, unhandled, exceptions) = self._event("onBanExceptListEntry", self.addons + - channel.addons, origin=origin, channel=channel, mask=mask, setby=setby, settime=int(settime)) + self._event("onRecv", channel.addons, **data) + self._event( + "onBanExceptListEntry", self.addons + channel.addons, origin=origin, + channel=channel, mask=mask, setby=setby, settime=int(settime), data=data) if "e" in channel.modes.keys(): if mask.lower() not in [m.lower() for (m, s, t) in channel.modes["e"]]: channel.modes["e"].append( @@ -1189,16 +1268,18 @@ class Connection(object): channel.modes["e"] = [(mask, setby, int(settime))] elif cmd == 349: channel = self.channel(params) - self._event("onRecv", channel.addons, line=line, **data) - (handled, unhandled, exceptions) = self._event("onBanExceptListEnd", - self.addons + channel.addons, origin=origin, channel=channel, endmsg=extinfo) + self._event("onRecv", channel.addons, **data) + self._event( + "onBanExceptListEnd", self.addons + channel.addons, + origin=origin, channel=channel, endmsg=extinfo, data=data) elif cmd == 910: # Channel Access List (channame, mask, setby, settime) = params.split() channel = self.channel(channame) - self._event("onRecv", channel.addons, line=line, **data) - (handled, unhandled, exceptions) = self._event("onAccessListEntry", self.addons + - channel.addons, origin=origin, channel=channel, mask=mask, setby=setby, settime=int(settime)) + self._event("onRecv", channel.addons, **data) + self._event( + "onAccessListEntry", self.addons + channel.addons, origin=origin, + channel=channel, mask=mask, setby=setby, settime=int(settime), data=data) if "w" in channel.modes.keys(): if mask.lower() not in [m.lower() for (m, s, t) in channel.modes["b"]]: channel.modes["w"].append( @@ -1207,16 +1288,18 @@ class Connection(object): channel.modes["w"] = [(mask, setby, int(settime))] elif cmd == 911: channel = self.channel(params) - self._event("onRecv", channel.addons, line=line, **data) - (handled, unhandled, exceptions) = self._event( - "onAccessListEnd", self.addons + channel.addons, origin=origin, channel=channel, endmsg=extinfo) + self._event("onRecv", channel.addons, **data) + self._event( + "onAccessListEnd", self.addons + channel.addons, + origin=origin, channel=channel, endmsg=extinfo, data=data) elif cmd == 941: # Spam Filter list (channame, mask, setby, settime) = params.split() channel = self.channel(channame) - self._event("onRecv", channel.addons, line=line, **data) - (handled, unhandled, exceptions) = self._event("onSpamfilterListEntry", self.addons + - channel.addons, origin=origin, channel=channel, mask=mask, setby=setby, settime=int(settime)) + self._event("onRecv", channel.addons, **data) + self._event( + "onSpamfilterListEntry", self.addons + channel.addons, origin=origin, + channel=channel, mask=mask, setby=setby, settime=int(settime), data=data) if "g" in channel.modes.keys(): if mask.lower() not in [m.lower() for (m, s, t) in channel.modes["g"]]: channel.modes["g"].append( @@ -1225,16 +1308,18 @@ class Connection(object): channel.modes["g"] = [(mask, setby, int(settime))] elif cmd == 940: channel = self.channel(params) - self._event("onRecv", channel.addons, line=line, **data) - (handled, unhandled, exceptions) = self._event("onSpamfilterListEnd", - self.addons + channel.addons, origin=origin, channel=channel, endmsg=extinfo) + self._event("onRecv", channel.addons, **data) + self._event( + "onSpamfilterListEnd", self.addons + channel.addons, + origin=origin, channel=channel, endmsg=extinfo, data=data) elif cmd == 954: # Channel exemptchanops list (channame, mask, setby, settime) = params.split() channel = self.channel(channame) - self._event("onRecv", channel.addons, line=line, **data) - (handled, unhandled, exceptions) = self._event("onExemptChanOpsListEntry", self.addons + - channel.addons, origin=origin, channel=channel, mask=mask, setby=setby, settime=int(settime)) + self._event("onRecv", channel.addons, **data) + self._event( + "onExemptChanOpsListEntry", self.addons + channel.addons, origin=origin, + channel=channel, mask=mask, setby=setby, settime=int(settime), data=data) if "X" in channel.modes.keys(): if mask.lower() not in [m.lower() for (m, s, t) in channel.modes["X"]]: channel.modes["X"].append( @@ -1243,16 +1328,18 @@ class Connection(object): channel.modes["X"] = [(mask, setby, int(settime))] elif cmd == 953: channel = self.channel(params) - self._event("onRecv", channel.addons, line=line, **data) - (handled, unhandled, exceptions) = self._event("onExemptChanOpsListEnd", - self.addons + channel.addons, origin=origin, channel=channel, endmsg=extinfo) + self._event("onRecv", channel.addons, **data) + self._event( + "onExemptChanOpsListEnd", self.addons + channel.addons, + origin=origin, channel=channel, endmsg=extinfo, data=data) elif cmd == 728: # Channel quiet list (channame, modechar, mask, setby, settime) = params.split() channel = self.channel(channame) - self._event("onRecv", channel.addons, line=line, **data) - (handled, unhandled, exceptions) = self._event("onQuietListEntry", self.addons + channel.addons, - origin=origin, channel=channel, modechar=modechar, mask=mask, setby=setby, settime=int(settime)) + self._event("onRecv", channel.addons, **data) + self._event( + "onQuietListEntry", self.addons + channel.addons, origin=origin, channel=channel, + modechar=modechar, mask=mask, setby=setby, settime=int(settime), data=data) if "q" in channel.modes.keys(): if mask.lower() not in [m.lower() for (m, s, t) in channel.modes["q"]]: channel.modes["q"].append( @@ -1262,23 +1349,24 @@ class Connection(object): elif cmd == 729: channame, modechar = params.split() channel = self.channel(channame) - self._event("onRecv", channel.addons, line=line, **data) - (handled, unhandled, exceptions) = self._event( - "onQuietListEnd", self.addons + channel.addons, channel=channel, endmsg=extinfo) + self._event("onRecv", channel.addons, **data) + self._event( + "onQuietListEnd", self.addons + channel.addons, channel=channel, endmsg=extinfo, data=data) elif cmd in (495, 384, 385, 386, 468, 470, 366, 315, 482, 484, 953, 368, 482, 349, 940, 911, 489, 490, 492, 520, 530): # Channels which appear in params for param in params.split(): if len(param) and param[0] in self.supports["CHANTYPES"]: channel = self.channel(param) - self._event( - "onRecv", channel.addons, line=line, **data) + self._event("onRecv", channel.addons, **data) elif type(cmd) == int: - (handled, unhandled, exceptions) = self._event("on%03d" % - cmd, self.addons, line=line, origin=origin, target=target, params=params, extinfo=extinfo) - else: - (handled, unhandled, exceptions) = self._event("on%s" % - cmd, self.addons, line=line, origin=origin, cmd=cmd, target=target, params=params, extinfo=extinfo) + self._event( + "on%03d" % cmd, self.addons, line=line, origin=origin, + target=target, params=params, extinfo=extinfo, data=data) + elif not (cmd in ("PING", "PONG") and self.quietpingpong): + self._event( + "on%s" % cmd, self.addons, line=line, origin=origin, + cmd=cmd, target=target, params=params, extinfo=extinfo, data=data) if cmd in (384, 403, 405, 471, 473, 474, 475, 476, 520, 477, 489, 495): # Channel Join denied try: @@ -1305,18 +1393,19 @@ class Connection(object): channel._joining.notify() # Handle events that were not handled. - self._event("onUnhandled", unhandled, line=line, origin=origin, - cmd=cmd, target=target, params=params, extinfo=extinfo) + # if not (cmd in ("PING", "PONG") and self.quietpingpong): + # self._event("onUnhandled", unhandled, line=line, origin=origin, cmd=cmd, target=target, params=params, extinfo=extinfo) def _trynick(self): (q, s) = divmod(self.trynick, len(self.nick)) nick = self.nick[s] if q > 0: nick = "%s%d" % (nick, q) - self._send("NICK %s" % nick) + self._send(u"NICK %s" % nick) self.trynick += 1 - def _recvhandler(self): + def _recvhandler(self, server=None, port=None, ssl=None, ipv6=None): + pingreq = None # Enforce that this function must only be run from within # self._sendhandlerthread. if currentThread() != self._recvhandlerthread: @@ -1360,9 +1449,9 @@ class Connection(object): # Attempt initial registration. nick = self.nick[0] if self.passwd: - self._send("PASS %s" % self.passwd) + self._send(u"PASS %s" % self.passwd) self._trynick() - self._send("USER %s * * :%s" % + self._send(u"USER %s * * :%s" % (self.username.split("\n")[0].rstrip(), self.realname.split("\n")[0].rstrip())) # Initialize buffers @@ -1372,6 +1461,13 @@ class Connection(object): while True: # Main loop of IRC connection. while len(linebuf) == 0: # Need Moar Data read = self._connection.recv(512) + with self._sendline: + if pingreq and pingreq in self._outgoing: + self._outgoing.remove(pingreq) + pingreq = (time.time() + self.pinginterval, u"PING %s %s" % ( + self.identity.nick if self.identity else "*", self.serv), self) + self._outgoing.append(pingreq) + self._sendline.notify() # If read was empty, connection is terminated. if read == "": @@ -1386,6 +1482,12 @@ class Connection(object): readbuf = readbuf[lastlf + 1:] line = string.rstrip(linebuf.pop(0)) + try: + line = line.decode("utf8") + except UnicodeDecodeError: + # Attempt to figure encoding + charset = chardet.detect(line)['encoding'] + line = line.decode(charset) self._procrecvline(line) except SystemExit: # Connection lost normally. @@ -1415,8 +1517,9 @@ class Connection(object): self._outgoing.clear() self._sendline.notify() with self.lock: - (handled, unhandled, exceptions) = self._event("onDisconnect", self.addons + reduce( + self._event("onDisconnect", self.addons + reduce( lambda x, y: x + y, [chan.addons for chan in self.channels], []), expected=self._quitexpected) + self._init() # Notify _outgoingthread that the connection has been @@ -1447,52 +1550,60 @@ class Connection(object): finally: self.logwrite("### Log session ended") - (handled, unhandled, exceptions) = self._event("onSessionClose", self.addons + - reduce(lambda x, y: x + y, [chan.addons for chan in self.channels], [])) + self._event("onSessionClose", self.addons + reduce( + lambda x, y: x + y, [chan.addons for chan in self.channels], [])) # Tell _sendhandler to quit with self._sendline: self._outgoing.append("quit") self._sendline.notify() - def _send(self, line, origin=None): + def _send(self, line, origin=None, T=None): if "\r" in line or "\n" in line: raise InvalidCharacter cmd = line.split(" ")[0].upper() - T = time.time() - if cmd == "PRIVMSG": - # Hard-coding a throttling mechanism for PRIVMSGs only here. Will later build support for custom throttlers. - # The throttle will be triggered when it attempts to send a sixth PRIVMSG in a four-second interval. - # When the throttle is active, PRIVMSGs will be sent in at least one-second intervals. - # The throttle is deactivated when three seconds elapse without - # sending a PRIVMSG. - while len(self.throttledata) and self.throttledata[0] < T - 4: - del self.throttledata[0] - if not self.throttled: - if len(self.throttledata) >= 5: - self.throttled = True - T = self.throttledata[-1] + 1 - else: - if len(self.throttledata) == 0 or self.throttledata[-1] < T - 2: - self.throttled = False + if T == None: + T = time.time() + if cmd == "PRIVMSG": + # Hard-coding a throttling mechanism for PRIVMSGs only here. Will later build support for custom throttlers. + # The throttle will be triggered when it attempts to send a sixth PRIVMSG in a four-second interval. + # When the throttle is active, PRIVMSGs will be sent in at least one-second intervals. + # The throttle is deactivated when three seconds elapse without + # sending a PRIVMSG. + while len(self.throttledata) and self.throttledata[0] < T - 4: + del self.throttledata[0] + if not self.throttled: + if len(self.throttledata) >= 5: + self.throttled = True + T = self.throttledata[-1] + 1 else: - T = max(T, self.throttledata[-1] + 1) - self.throttledata.append(T) + if len(self.throttledata) == 0 or self.throttledata[-1] < T - 2: + self.throttled = False + else: + T = max(T, self.throttledata[-1] + 1) + self.throttledata.append(T) with self._sendline: self._outgoing.append((T, line, origin)) self._sendline.notify() + def _cancelsend(self, line, origin=None, T=None): + with self._sendline: + self._outgoing.remove((T, line, origin)) + self._sendline.notify() + def _procsendline(self, line, origin=None): - match = re.findall(_ircsendmatch, line) + match = re.findall(_ircmatch, line) if len(match) == 0: return - (cmd, target, params, extinfo) = match[0] + (null, username, host, cmd, target, params, extinfo) = match[0] cmd = cmd.upper() with self.lock: if cmd == "QUIT": self._quitexpected = True - self._connection.send("%s\n" % line) + if self._connection == None: + return + origline = line # Modify line if it contains a password so that the password is not # logged or sent to any potentially untrustworthy addons @@ -1612,9 +1723,11 @@ class Connection(object): elif cmd.upper() == "IDENTIFY": target = "********" line = "%s %s" % (cmd, target) - self._event("onSend", self.addons, origin=origin, line=line, - cmd=cmd, target=target, params=params, extinfo=extinfo) - self.logwrite(">>> %s" % line) + if not (cmd in ("PING", "PONG") and self.quietpingpong): + self._event("onSend", self.addons, origin=origin, line=line, + cmd=cmd, target=target, params=params, extinfo=extinfo) + self.logwrite(">>> %s" % line) + self._connection.send("%s\n" % origline.encode('utf8')) def _sendhandler(self): # Enforce that this function must only be run from within @@ -1624,11 +1737,6 @@ class Connection(object): try: while True: - # Wait for one of the following: - # (1) An item placed into _outgoing - # (2) Connection is lost - # (3) self._recvhandlerthread is set to None - with self._sendline: if "quit" in self._outgoing: sys.exit() @@ -1636,11 +1744,19 @@ class Connection(object): if len(self._outgoing): T, line, origin = min(self._outgoing) if T > S: + # The next item in the queue (by time) is still + # scheduled to be sent later. We wait until then, + # or when another item is put into the queue, + # whichever is first. self._sendline.wait(T - S) continue else: + # The next item in the queue (by time) should be + # sent now. self._outgoing.remove((T, line, origin)) else: + # The queue is empty, so we will wait until something + # is put into the queue, then restart the while loop. self._sendline.wait() continue @@ -1717,43 +1833,43 @@ class Connection(object): # locals() def oper(self, name, passwd, origin=None): - self.raw("OPER %s %s" % - (re.findall("^([^\r\n\\s]*)", name)[0], re.findall("^([^\r\n\\s]*)", passwd)[0]), origin=origin) + self._send(u"OPER %s %s" % + (re.findall("^([^\r\n\\s]*)", name)[0], re.findall("^([^\r\n\\s]*)", passwd)[0]), origin=origin) def list(self, params="", origin=None): if len(re.findall("^([^\r\n\\s]*)", params)[0]): - self.raw("LIST %s" % - (re.findall("^([^\r\n\\s]*)", params)[0]), origin=origin) + self._send(u"LIST %s" % + (re.findall("^([^\r\n\\s]*)", params)[0]), origin=origin) else: - self.raw("LIST", origin=origin) + self._send(u"LIST", origin=origin) def getmotd(self, target="", origin=None): if len(re.findall("^([^\r\n\\s]*)", target)[0]): - self.raw("MOTD %s" % - (re.findall("^([^\r\n\\s]*)", target)[0]), origin=origin) + self._send(u"MOTD %s" % + (re.findall("^([^\r\n\\s]*)", target)[0]), origin=origin) else: - self.raw("MOTD", origin=origin) + self._send(u"MOTD", origin=origin) def version(self, target="", origin=None): if len(re.findall("^([^\r\n\\s]*)", target)[0]): - self.raw("VERSION %s" % - (re.findall("^([^\r\n\\s]*)", target)[0]), origin=origin) + self._send(u"VERSION %s" % + (re.findall("^([^\r\n\\s]*)", target)[0]), origin=origin) else: - self.raw("VERSION", origin=origin) + self._send(u"VERSION", origin=origin) def stats(self, query, target="", origin=None): if len(re.findall("^([^\r\n\\s]*)", target)[0]): - self.raw("STATS %s %s" % - (query, re.findall("^([^\r\n\\s]*)", target)[0]), origin=origin) + self._send(u"STATS %s %s" % + (query, re.findall("^([^\r\n\\s]*)", target)[0]), origin=origin) else: - self.raw("STATS %s" % query, origin=origin) + self._send(u"STATS %s" % query, origin=origin) def quit(self, msg="", origin=None): if len(re.findall("^([^\r\n]*)", msg)[0]): - self._send("QUIT :%s" % + self._send(u"QUIT :%s" % re.findall("^([^\r\n]*)", msg)[0], origin=origin) else: - self._send("QUIT", origin=origin) + self._send(u"QUIT", origin=origin) def ctcpversion(self): reply = [] @@ -1780,7 +1896,7 @@ class Connection(object): def raw(self, line, origin=None): self._send(line, origin=origin) - def user(self, nick): + def user(self, nick, init=False): if self.supports.get("CASEMAPPING", "rfc1459") == "ascii": users = [ user for user in self.users if user.nick.lower() == nick.lower()] @@ -1788,6 +1904,8 @@ class Connection(object): users = [user for user in self.users if user.nick.translate( _rfc1459casemapping) == nick.translate(_rfc1459casemapping)] if len(users): + if init: + users[0]._init() return users[0] else: user = User(nick, self) @@ -1796,7 +1914,7 @@ class Connection(object): str(t).rjust(2, "0") for t in time.localtime()[0:6]]) return user - def channel(self, name): + def channel(self, name, init=False): if self.supports.get("CASEMAPPING", "rfc1459") == "ascii": channels = [ chan for chan in self.channels if chan.name.lower() == name.lower()] @@ -1804,6 +1922,8 @@ class Connection(object): channels = [chan for chan in self.channels if chan.name.translate( _rfc1459casemapping) == name.translate(_rfc1459casemapping)] if len(channels): + if init: + channels[0]._init() return channels[0] else: timestamp = reduce(lambda x, y: x + ":" + y, [ @@ -1812,22 +1932,35 @@ class Connection(object): self.channels.append(chan) return chan + def __getitem__(self, item): + chantypes = self.supports.get("CHANTYPES", "&#+!") + if re.match(_chanmatch % re.escape(chantypes), item): + return self.channel(item) + elif re.match(_usermatch, item): + return self.user(item) + else: + raise TypeError, "String argument does not match valid channel name or nick name." + class Channel(object): - def __init__(self, name, context): + def __init__(self, name, context, key=None): chantypes = context.supports.get("CHANTYPES", "&#+!") if not re.match(_chanmatch % re.escape(chantypes), name): raise InvalidName, repr(name) self.name = name self.context = context + self.key = key + self._init() + + def _init(self): self.addons = [] self.topic = "" self.topicsetby = "" self.topictime = () self.topicmod = "" self.modes = {} - self.users = [] + self.users = UserList(context=self.context) self.created = None self.lock = Lock() self._joinrequested = False @@ -1841,24 +1974,24 @@ class Channel(object): if target and target not in self.context.supports.get("PREFIX", ("ohv", "@%+"))[1]: raise InvalidPrefix for line in re.findall("([^\r\n]+)", msg): - self.context._send("PRIVMSG %s%s :%s" % + self.context._send(u"PRIVMSG %s%s :%s" % (target, self.name, line), origin=origin) def who(self, origin=None): - self.context._send("WHO %s" % (self.name), origin=origin) + self.context._send(u"WHO %s" % (self.name), origin=origin) def names(self, origin=None): - self.context._send("NAMES %s" % (self.name), origin=origin) + self.context._send(u"NAMES %s" % (self.name), origin=origin) def notice(self, msg, target="", origin=None): if target and target not in self.context.supports.get("PREFIX", ("ohv", "@%+"))[1]: raise InvalidPrefix for line in re.findall("([^\r\n]+)", msg): - self.context._send("NOTICE %s%s :%s" % + self.context._send(u"NOTICE %s%s :%s" % (target, self.name, line), origin=origin) def settopic(self, msg, origin=None): - self.context._send("TOPIC %s :%s" % + self.context._send(u"TOPIC %s :%s" % (self.name, re.findall("^([^\r\n]*)", msg)[0]), origin=origin) def ctcp(self, act, msg="", origin=None): @@ -1890,9 +2023,9 @@ class Channel(object): self._partrequested = True if len(re.findall("^([^\r\n]*)", msg)[0]): self.context._send( - "PART %s :%s" % (self.name, re.findall("^([^\r\n]*)", msg)[0]), origin=origin) + u"PART %s :%s" % (self.name, re.findall("^([^\r\n]*)", msg)[0]), origin=origin) else: - self.context._send("PART %s" % self.name, origin=origin) + self.context._send(u"PART %s" % self.name, origin=origin) # Anticipated Numeric Replies: @@ -1922,7 +2055,7 @@ class Channel(object): user) == User else re.findall("^([^\r\n\\s]*)", user)[0] if nickname == "": raise InvalidName - self.context._send("INVITE %s %s" % + self.context._send(u"INVITE %s %s" % (nickname, self.name), origin=origin) def join(self, key="", blocking=False, timeout=30, origin=None): @@ -1937,9 +2070,9 @@ class Channel(object): self._joinrequested = True if len(re.findall("^([^\r\n\\s]*)", key)[0]): self.context._send( - "JOIN %s %s" % (self.name, re.findall("^([^\r\n\\s]*)", key)[0]), origin=origin) + u"JOIN %s %s" % (self.name, re.findall("^([^\r\n\\s]*)", key)[0]), origin=origin) else: - self.context._send("JOIN %s" % self.name, origin=origin) + self.context._send(u"JOIN %s" % self.name, origin=origin) # Anticipated Numeric Replies: @@ -1973,14 +2106,17 @@ class Channel(object): if nickname == "": raise InvalidName if len(re.findall("^([^\r\n]*)", msg)[0]): - self.context._send("KICK %s %s :%s" % + self.context._send(u"KICK %s %s :%s" % (self.name, nickname, re.findall("^([^\r\n]*)", msg)[0]), origin=origin) else: - self.context._send("KICK %s %s" % + self.context._send(u"KICK %s %s" % (self.name, nickname), origin=origin) def __repr__(self): - return "<Channel: " + self.name + "@" + self.context.server + "/" + str(self.context.port) + ">" + return (u"<Channel: %s@%s/%d>" % (self.name, self.context.server, self.context.port)).encode("utf8") + + def __contains__(self, item): + return item in self.users class User(object): @@ -1989,10 +2125,13 @@ class User(object): if not re.match(_nickmatch, nick): raise InvalidName self.nick = nick + self.context = context + self._init() + + def _init(self): self.username = "" self.host = "" - self.channels = [] - self.context = context + self.channels = ChanList(context=self.context) self.modes = "" self.snomask = "" self.server = None @@ -2005,24 +2144,24 @@ class User(object): self.away = None def __repr__(self): - return "<User: %(nick)s!%(username)s@%(host)s>" % vars(self) + return (u"<User: %(nick)s!%(username)s@%(host)s>" % vars(self)).encode("utf8") def msg(self, msg, origin=None): for line in re.findall("([^\r\n]+)", msg): - self.context._send("PRIVMSG %s :%s" % + self.context._send(u"PRIVMSG %s :%s" % (self.nick, line), origin=origin) def notice(self, msg, origin=None): for line in re.findall("([^\r\n]+)", msg): - self.context._send("NOTICE %s :%s" % + self.context._send(u"NOTICE %s :%s" % (self.nick, line), origin=origin) def ctcp(self, act, msg="", origin=None): if len(re.findall("^([^\r\n]*)", msg)[0]): - self.msg("\01%s %s\01" % + self.msg(u"\01%s %s\01" % (act.upper(), re.findall("^([^\r\n]*)", msg)[0]), origin=origin) else: - self.msg("\01%s\01" % act.upper()) + self.msg(u"\01%s\01" % act.upper()) def ctcpreply(self, act, msg="", origin=None): if len(re.findall("^([^\r\n]*)", msg)[0]): @@ -2033,3 +2172,167 @@ class User(object): def me(self, msg="", origin=None): self.ctcp("ACTION", msg, origin=origin) + + +class Config(object): + + def __init__(self, **kwargs): + self.__dict__.update(kwargs) + + +class ChanList(list): + + def __init__(self, iterable=None, context=None): + if context != None and type(context) != Connection: + raise TypeError, "context must be irc.Connection object or None" + self.context = context + if iterable: + chanlist = [] + for channel in iterable: + if type(channel) == Channel: + chanlist.append(channel) + elif type(channel) in (str, unicode): + if context == None: + raise ValueError, "No context given for string object." + chanlist.append(context.channel(channel)) + list.__init__(self, chanlist) + else: + list.__init__(self) + + def append(self, item): + if type(item) in (str, unicode): + if self.context: + list.append(self, self.context.channel(item)) + return + else: + raise ValueError, "No context given for string object." + if type(item) != Channel: + raise TypeError, "Only channel objects are permitted in list" + if self.context and item.context != self.context: + raise ValueError, "Channel object does not belong to context." + list.append(self, item) + + def insert(self, index, item): + if type(item) in (str, unicode): + if self.context: + list.insert(self, index, self.context.channel(item)) + return + else: + raise ValueError, "No context given for string object." + if type(item) != Channel: + raise TypeError, "Only channel objects are permitted in list" + if self.context and item.context != self.context: + raise ValueError, "Channel object does not belong to context." + list.insert(self, index, item) + + def extend(self, iterable): + chanlist = [] + for item in iterable: + if type(item) in (str, unicode): + if self.context: + chanlist.append(self.context.channel(item)) + return + else: + raise ValueError, "No context given for string object." + if type(item) != Channel: + raise TypeError, "Only channel objects are permitted in list" + if self.context and item.context != self.context: + raise ValueError, "Channel object does not belong to context." + chanlist.append(item) + list.extend(self, chanlist) + + def join(self, origin=None): + if not self.context: + raise ValueError, "No context defined." + if any([channel.key for channel in self]): + self.context._send(u"JOIN %s %s" % + (self, ",".join([channel.key if channel.key else "" for channel in self])), origin=origin) + else: + self.context._send(u"JOIN %s" % self, origin=origin) + + def part(self, partmsg=None, origin=None): + if not self.context: + raise ValueError, "No context defined." + if partmsg: + self.context._send(u"PART %s :%s" % + (",".join([channel.name for channel in self]), partmsg), origin=origin) + else: + self.context._send(u"PART %s" % self, origin=origin) + + def msg(self, msg, origin=None): + if not self.context: + raise ValueError, "No context defined." + self.context._send(u"PRIVMSG %s :%s" % (self, msg), origin=origin) + + def __str__(self): + return ",".join([channel.name for channel in self]) + + +class UserList(list): + + def __init__(self, iterable=None, context=None): + if context != None and type(context) != Connection: + raise TypeError, "context must be irc.Connection object or None" + self.context = context + if iterable: + userlist = [] + for user in iterable: + if type(user) == User: + userlist.append(user) + elif type(user) in (str, unicode): + if context == None: + raise ValueError, "No context given for string object." + userlist.append(context.user(user)) + list.__init__(self, userlist) + else: + list.__init__(self) + + def append(self, item): + if type(item) in (str, unicode): + if self.context: + list.append(self, self.context.user(item)) + return + else: + raise ValueError, "No context given for string object." + if type(item) != User: + raise TypeError, "Only user objects are permitted in list" + if self.context and item.context != self.context: + raise ValueError, "User object does not belong to context." + list.append(self, item) + + def insert(self, index, item): + if type(item) in (str, unicode): + if self.context: + list.insert(self, index, self.context.user(item)) + return + else: + raise ValueError, "No context given for string object." + if type(item) != User: + raise TypeError, "Only user objects are permitted in list" + if self.context and item.context != self.context: + raise ValueError, "User object does not belong to context." + list.insert(self, index, item) + + def extend(self, iterable): + userlist = [] + for item in iterable: + if type(item) in (str, unicode): + if self.context: + userlist.append(self.context.user(item)) + return + else: + raise ValueError, "No context given for string object." + if type(item) != User: + raise TypeError, "Only user objects are permitted in list" + if self.context and item.context != self.context: + raise ValueError, "User object does not belong to context." + userlist.append(item) + list.extend(self, userlist) + + def msg(self, msg, origin=None): + if not self.context: + raise ValueError, "No context defined." + self.context._send(u"PRIVMSG %s :%s" % (self, msg), origin=origin) + + def __str__(self): + return ",".join([user.nick for user in self]) |