diff options
author | Brian Sherson <caretaker82@euclid.shersonb.net> | 2014-03-22 20:52:05 -0700 |
---|---|---|
committer | Brian Sherson <caretaker82@euclid.shersonb.net> | 2014-03-22 20:52:05 -0700 |
commit | 5c66d93f0ca04c06f3ed122772b9751b3dcc3f6e (patch) | |
tree | 0ec0aa59f5afb7588fe35c95d3d3495ec5b0d503 /irc.py | |
parent | 0b695d73168af6a8444e1c0a0f4e6b0f85506fc4 (diff) |
Adding CAP support, ircapp.py with sample config, speeding up channel and user cache lookup.
Diffstat (limited to 'irc.py')
-rw-r--r-- | irc.py | 1298 |
1 files changed, 923 insertions, 375 deletions
@@ -19,6 +19,9 @@ import inspect import warnings import random +__all__ = ["Connection", "Channel", "ChanList", + "User", "UserList", "Config", "timestamp"] + def autodecode(s): try: @@ -36,11 +39,23 @@ class AddonWarning(Warning): pass +class ConnectionWarning(Warning): + pass + + +class AddonError(BaseException): + pass + + class InvalidName(BaseException): + + """Raised when an invalid string is passed of as a nickname.""" pass class InvalidPrefix(BaseException): + + """Raised when an string with an invalid prefix is passed of as a channel name.""" pass @@ -49,6 +64,8 @@ class InvalidCharacter(BaseException): class ConnectionTimedOut(BaseException): + + """Raised when the connection times out during a blocked Channel.join() or Channel.part() call.""" pass @@ -57,18 +74,26 @@ class ConnectionClosed(BaseException): class RequestTimedOut(BaseException): + + """Raised when a timeout is reached during a blocked Channel.join() or Channel.part() call.""" pass class NotConnected(BaseException): + + """Raised when attempting to send data to a server when not connected.""" pass class BannedFromChannel(BaseException): + + """Raised in a blocked Channel.join() call when server returns a 474 reply (Banned from Channel).""" pass class RedirectedJoin(BaseException): + + """Raised in a blocked Channel.join() call when server returns a 470 reply (Channel redirect).""" pass @@ -157,6 +182,7 @@ _prefixmatch = r"\((.*)\)(.*)" _defaultchanmodes = u"b,k,l,imnpst".split(",") _defaultprefix = ("ov", "@+") _defaultchantypes = "&#+!" +_capmodifiers = "~=-" _privmodeeventnames = dict(q=("Owner", "Deowner"), a=("Admin", "Deadmin"), o=( "Op", "Deop"), h=("Halfop", "Dehalfop"), v=("Voice", "Devoice")) @@ -177,18 +203,50 @@ def timestamp(): class Connection(object): + __doc__ = "Manages a connection to an IRC network. Includes support for addons." __name__ = "pyIRC" - __version__ = "2.0" + __version__ = "2.1" __author__ = "Brian Sherson" __date__ = "February 21, 2014" - def __init__(self, server, nick="ircbot", username="python", realname="Python IRC Library", passwd=None, port=None, ipvers=(socket.AF_INET6, socket.AF_INET), secure=False, autoreconnect=True, timeout=300, retrysleep=5, maxretries=15, protoctl=("UHNAMES", "NAMESX"), quietpingpong=True, pinginterval=60, addons=None, autostart=False): + def __init__( + self, server, port=None, ipvers=(socket.AF_INET6, socket.AF_INET), secure=False, passwd=None, + nick="ircbot", username="python", realname="Python IRC Library", + requestcaps=[], starttls=False, protoctl=[], + autoreconnect=True, retrysleep=5, maxretries=15, + timeout=300, quietpingpong=True, pinginterval=60, addons=[], autostart=False): + """__init__(server[, ...]) + + Constructor for the Connection class. + + Arguments: + + server: Server name. Can provide host name or IP address. + port: Port to use, or automatically selected if port=None. + ipvers: Tuple of IP protocols to try. + secure: Use SSL. + passwd: Password to be sent with PASS during registration process, or None. + nick: A nickname, or list of nicknames. + username: Username that is requested with USER command during registration process. + realname: Desired GECOS. + requestcaps: List of capabilities to request on connect. + protoctl: Protocols to request when support is detected in 005 response. + autoreconnect: Reconnect automatically when disconnected unexpectedly. + retrysleep: Number of seconds to wait between connection attempts. + maxretries: Number of connection attempts before giving up, or -1 to try indefinitely. + timeout: Read timeout. + quietpingpong: Suppress logging and events on PING and PONG events. + pinginterval: Amount of time of not receiving data from a server aftr which a ping request is to be sent. + addons: List of addons that should be initialized with this instance. Items of this list are either instances of addons or dict objects + containing keyword arguments to be used to configure addons. + autostart: Automatically start connection to IRC server upon initialization. + """ if port is None or (type(port) == int and 0 < port < 65536): self.port = port else: raise ValueError, "Invalid value for 'port'" - if re.match(_nickmatch, nick) if (type(nick) in (str, unicode)) else all([re.match(_nickmatch, n) for n in nick]) if (type(nick) in (list, tuple)) else False: + if re.match(_nickmatch, nick) if isinstance(nick, (str, unicode)) else all([re.match(_nickmatch, n) for n in nick]) if isinstance(nick, (list, tuple)) else False: self.nick = nick else: raise ValueError, "Invalid value for 'nick'" @@ -219,17 +277,17 @@ class Connection(object): else: raise ValueError, "Invalid value for 'autoreconnect'" - if type(maxretries) in (int, long): + if isinstance(maxretries, (int, long)): self.maxretries = maxretries else: raise ValueError, "Invalid value for 'maxretries'" - if type(timeout) in (int, long): + if isinstance(timeout, (int, long)): self.timeout = timeout else: raise ValueError, "Invalid value for 'timeout'" - if type(retrysleep) in (int, long): + if isinstance(retrysleep, (int, long, float)) and retrysleep >= 0: self.retrysleep = retrysleep else: raise ValueError, "Invalid value for 'retrysleep'" @@ -239,6 +297,19 @@ class Connection(object): else: raise ValueError, "Invalid value for 'quietpingpong'" + if type(starttls) == bool: + if starttls and secure: + warnings.warn( + "Cannot use STARTTLS when secure=True", ConnectionWarning) + self.starttls = starttls + else: + raise ValueError, "Invalid value for 'starttls'" + + if isinstance(requestcaps, (list, tuple)): + self.requestcaps = list(requestcaps) + else: + raise ValueError, "Invalid value for 'requestcaps'" + if type(pinginterval) in (int, long): self.pinginterval = pinginterval else: @@ -260,18 +331,26 @@ class Connection(object): self._recvhandlerthread = None # Initialize IRC environment variables - self.users = UserList(context=self) - self.channels = ChanList(context=self) + self.users = UserList(context=self, withdict=True) + self.channels = ChanList(context=self, withdict=True) + + # We are going to try something different, to try to make searching quicker. + # self.users={} + # self.channels={} + + self.servers = ServerList(context=self) self.addons = [] - self.trusted = [] # To be implemented later self._init() - if type(addons) == list: - for addon in addons: - if type(addon) == dict: - self.addAddon(**addon) + for conf in addons: + try: + if type(conf) == dict: + self.addAddon(**conf) else: - self.addAddon(addon) + self.addAddon(conf) + except: + pass + if autostart: self.connect() @@ -281,14 +360,11 @@ class Connection(object): self._connected = False self._registered = False self._connection = None + self._starttls = False self.trynick = 0 self.identity = None - self.motdgreet = None - self.motd = None - self.motdend = None - self.serv = None self.welcome = None self.hostinfo = None @@ -298,6 +374,22 @@ class Connection(object): self.supports = {} self.throttledata = [] self.throttled = False + self.enabledcaps = [] + self.supportedcaps = [] + self._requestedcaps = [] + self._caplsrequested = False + + @property + def motdgreet(self): + return self.identity.server.motdgreet + + @property + def motd(self): + return self.identity.server.motd + + @property + def motdend(self): + return self.identity.server.motdend @property def connected(self): @@ -308,17 +400,32 @@ class Connection(object): return self._registered def logwrite(self, *lines): + """logwrite(*lines) + + Writes one or more line to the log file, signed with a timestamp.""" with self._loglock: ts = timestamp() for line in lines: - try: - print >>self.log, u"%s %s" % (ts, line) - except: - print line - raise + print >>self.log, u"%s %s" % (ts, line) self.log.flush() + def logerror(self, *lines): + """logerror(*lines) + + Prints lines and traceback sys.stderr and to the log file.""" + exc, excmsg, tb = sys.exc_info() + lines = lines + tuple(traceback.format_exc().split("\n")) + + # Print to log AND stderr + self.logwrite(*[u"!!! {line}".format(**vars()) for line in lines]) + for line in lines: + print >>sys.stderr, line + def logopen(self, filename, encoding="utf8"): + """logopen(filename[, encoding]) + + Sets the log file to 'filename.'""" + with self._loglock: ts = timestamp() newlog = codecs.open(filename, "a", encoding=encoding) @@ -335,8 +442,8 @@ class Connection(object): handled = [] unhandled = [] errors = [] - for k, addon in enumerate(addons): - if addons.index(addon) < k: + for k, addon in enumerate(addons + [self]): + if addon in addons and addons.index(addon) < k: # Duplicate continue @@ -347,25 +454,20 @@ class Connection(object): # Iterate through all events. for (method, args, fallback) in events: - if method in dir(addon) and callable(getattr(addon, method)): + try: f = getattr(addon, method) - elif fallback and not fellback: - if "onOther" in dir(addon) and callable(addon.onOther) and data: - f = addon.onOther - args = dict(line=line, **data) - fellback = True - 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 + except AttributeError: + if fallback and not fellback and data: + try: + f = getattr(addon, "onOther") + except AttributeError: + unhandled.append(addon) + continue args = dict(line=line, **data) fellback = True else: unhandled.append(addon) continue - else: - unhandled.append(addon) - continue if type(f) == new.instancemethod: argspec = inspect.getargspec(f.im_func) @@ -381,26 +483,19 @@ class Connection(object): exc, excmsg, tb = sys.exc_info() errors.append((addon, exc, excmsg, tb)) - # Print to log AND stderr - tblines = [u"!!! Exception in addon %(addon)s" % vars()] - tblines.append(u"!!! Function: %s" % f) - tblines.append(u"!!! Arguments: %s" % args) - for line in traceback.format_exc().split("\n"): - tblines.append(u"!!! %s" % autodecode(line)) - self.logwrite(*tblines) - print >>sys.stderr, "Exception in addon %(addon)s" % vars() - print >>sys.stderr, u"Function: %s" % f - print >>sys.stderr, u"Arguments: %s" % args - print >>sys.stderr, traceback.format_exc() + self.logerror(u"Exception in addon {addon}".format( + **vars()), u"Function: %s" % f, u"Arguments: %s" % args) + 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. - # Inspects the methods of addon to make sure def validateAddon(self, addon): + """validateAddon(addon) + + Checks the addon's methods and issues warnings when a method's arguments do not line up with what is expected.""" supported = self.eventsupports() keys = supported.keys() for fname in dir(addon): @@ -443,61 +538,82 @@ class Connection(object): "!!! AddonWarning: Function '%s' does not accept supported arguments: %s" % (func.__name__, ", ".join(unsupported))) - def addAddon(self, addon, trusted=False, **params): + def addAddon(self, addon, **params): + """addAddon(addon[, ...]) + + Configures and appends addon to self.addons. + Additional keyword arguments are passed onto addon.onAddonAdd whenever the method exists.""" self.validateAddon(addon) - for a in self.addons: - if (type(a) == Config and a.addon is addon) or a is addon: - raise BaseException, "Addon already added." + with self.lock: - if params: - defconf = Config(addon, **params) - else: - defconf = addon - if hasattr(addon, "onAddonAdd") and callable(addon.onAddonAdd): - conf = addon.onAddonAdd(self, **params) - if conf is not None: - self.addons.append(conf) - else: - self.addons.append(defconf) - else: - self.addons.append(defconf) + addoninstances = [ + conf.addon if type(conf) == Config else conf for conf in self.addons] + if addon in addoninstances: + raise AddonError, "Addon already added." + conf = self._configureAddon(addon, **params) + self.addons.append(conf) self.logwrite("*** Addon %s added." % repr(addon)) - if trusted: - self.trusted.append(addon) - def insertAddon(self, index, addon, trusted=False, **params): + def insertAddon(self, index, addon, **params): + """insertAddon(index, addon[, ...]) + + The 'list.insert' version of addAddon.""" self.validateAddon(addon) - for a in self.addons: - if (type(a) == Config and a.addon is addon) or a is addon: - raise BaseException, "Addon already added." + with self.lock: - if params: - defconf = Config(addon, **params) - else: - defconf = addon - if hasattr(addon, "onAddonAdd") and callable(addon.onAddonAdd): - conf = addon.onAddonAdd(self, **params) - if conf is not None: - self.addons.insert(index, conf) - else: - self.addons.insert(index, defconf) - else: - self.addons.insert(index, defconf) + addoninstances = [ + conf.addon if type(conf) == Config else conf for conf in self.addons] + if addon in addoninstances: + raise AddonError, "Addon already added." + conf = self._configureAddon(addon, **params) + self.addons.insert(index, conf) self.logwrite("*** Addon %s inserted into index %d." % (repr(addon), index)) - if trusted: - self.trusted.append(addon) + # Configures an addon by calling the addon's onAddonAdd instance (if it + # exists) and returns the appropriate config object (or just the addon + # instance if no config) to put into self.addons + def _configureAddon(self, addon, **params): + if hasattr(addon, "onAddonAdd") and callable(addon.onAddonAdd): + try: + conf = addon.onAddonAdd(self, **params) + except: + self.logerror( + u"An exception has occurred while trying to configure addon {addon}.".format(**vars())) + raise + if conf is None: + return addon + return conf + elif params: + return Config(addon, **params) + else: + return addon + + # Removes addon from self.addons def rmAddon(self, addon): + """rmAddon(addon) + + Removes addon from self.addons.""" with self.lock: - self.addons.remove(addon) + addoninstances = [ + conf.addon if type(conf) == Config else conf for conf in self.addons] + + del self.addons[addoninstances.index(addon)] self.logwrite("*** Addon %s removed." % repr(addon)) - if addon in self.trusted: - self.trusted.remove(addon) if hasattr(addon, "onAddonRem") and callable(addon.onAddonAdd): - addon.onAddonRem(self) + try: + addon.onAddonRem(self) + except: + self.logerror( + u"An exception has occurred while trying to configure addon {addon}.".format(**vars())) def connect(self, server=None, port=None, secure=None, ipvers=None, forcereconnect=False, blocking=False): + """connect([...]) + + Starts connection to the IRC server. Optional arguments server, port, secure, and ipvers can be + provided to override the current settings. + Use 'forcereconnect=True' to quit existing connection if already connected. + Use 'blocking=True' to wait until connection is established (or maxretries is exhausted).""" if ipvers != None: ipvers = ipvers if type(ipvers) == tuple else (ipvers,) else: @@ -527,6 +643,7 @@ class Connection(object): raise NotConnected def _connect(self, addr, ipver, secure, hostname=None): + """Makes a single attempt to connect to server.""" with self.lock: if self._connected: raise AlreadyConnected @@ -568,17 +685,19 @@ class Connection(object): raise else: # Run onConnect on all addons to signal connection was established. + self._connected = True with self.lock: self._event( self.getalladdons(), [("onConnect", dict(), False)]) self.logwrite( "*** Connection to {addrstr} established.".format(**vars())) self.addr = addr - self._connected = True with self._connecting: self._connecting.notifyAll() def _tryaddrs(self, server, addrs, ipver, secure): + """Iterates through addrs until a connection is successful, returning True, or returning False when no connections are made. + Raises an exception when it detects Network is unreachable (e.g., IPv6 network is not available).""" for addr in addrs: try: if server == addr[0]: @@ -600,6 +719,7 @@ class Connection(object): return False def _tryipver(self, server, port, ipver, secure): + """Attempts to resolve 'server' to a one or more IP addresses, then tries to establish a connection.""" if ipver == socket.AF_INET6: self.logwrite( "*** Attempting to resolve {server} to an IPv6 address...".format(**vars())) @@ -631,6 +751,7 @@ class Connection(object): return self._tryaddrs(server, addrs, ipver, secure) def _tryipvers(self, server, port, ipvers, secure): + """Attempts to try a connection for each IP version in ipvers until a connection is successful.""" for ipver in ipvers: try: ret = self._tryipver(server, port, ipver, secure) @@ -651,6 +772,7 @@ class Connection(object): return False def _procrecvline(self, line): + """Called whenever a line of data is received from the IRC server.""" matches = re.findall(_ircmatch, line) # We have a match! @@ -667,26 +789,13 @@ class Connection(object): self.logwrite("<<< %s" % line) if origin == "" and cmd == "PING": - self._send(u"PONG :%s" % extinfo) + self.send(u"PONG :%s" % extinfo) with self.lock: data = dict(origin=origin, cmd=cmd, target=target, targetprefix=None, params=params, extinfo=extinfo) - if not self._registered: - if type(cmd) == int and cmd != 451 and target != "*": # Registration complete! - self.identity = self.user(target, init=True) - self.serv = origin - self._event(self.getalladdons(), [ - ("onRegistered", dict(), False)], line, data) - self._registered = True - - elif cmd == 433 and target == "*": # Server reports nick taken, so we need to try another. - self._trynick() - if not self._registered: # Registration is not yet complete - return - - if username and host: + if username and host and self._registered: nickname = origin origin = self.user(origin) if origin.nick != nickname: @@ -698,6 +807,8 @@ class Connection(object): if origin.host != host: # Origin host has changed origin.host = host + else: + origin = self.getserver(origin) # Check to see if target matches a channel (optionally with # prefix) @@ -715,7 +826,7 @@ class Connection(object): # Check to see if target matches a valid nickname. Do NOT # convert target to User instance if cmd is NICK. - elif re.match(_nickmatch, target) and cmd != "NICK": + elif re.match(_nickmatch, target) and cmd in ("PRIVMSG", "NOTICE", "MODE", "INVITE", "KILL") and self._registered: targetprefix = "" target = self.user(target) @@ -739,20 +850,13 @@ class Connection(object): if hasattr(self, parsename) and callable(getattr(self, parsename)): parsemethod = getattr(self, parsename) try: - addons, events = parsemethod( + ret = parsemethod( origin, target, targetprefix, params, extinfo) + addons, events = ret if ret is not None else ( + self.addons, []) except: - exc, excmsg, tb = sys.exc_info() - - # Print to log AND stderr - tblines = [ - u"!!! There was an error in parsing the following line:", u"!!! %s" % line] - for tbline in traceback.format_exc().split("\n"): - tblines.append(u"!!! %s" % autodecode(tbline)) - self.logwrite(*tblines) - print >>sys.stderr, u"There was an error in parsing the following line:" - print >>sys.stderr, u"%s" % line - print >>sys.stderr, traceback.format_exc() + self.logerror( + u"There was an error in parsing the following line:", line) return else: addons = self.addons @@ -763,19 +867,20 @@ class Connection(object): events = [ ("on%s" % cmd.upper(), dict(line=line, origin=origin, target=target, targetprefix=targetprefix, params=params, extinfo=extinfo), True)] - # Supress pings and pongs if self.quietpingpong is set to True + # Suppress pings and pongs if self.quietpingpong is set to True if cmd in ("PING", "PONG") and self.quietpingpong: return # Send parsed data to addons having onRecv method first self._event( - addons + [self], [("onRecv", dict(line=line, **data), False)], line, data) + addons, [("onRecv", dict(line=line, **data), False)], line, data) # Support for further addon events is taken care of here. We also treat the irc.Connection instance itself as an addon for the purpose of # tracking the IRC state, and should be invoked *last*. - self._event(addons + [self], events, line, data) + self._event(addons, events, line, data) def _recvhandler(self, server, port, ipvers, secure): + """Function that is run as a separate thread, both managing the connection and handling data coming from the IRC server.""" if currentThread() != self._recvhandlerthread: # Enforce that this function must only be run from within self._sendhandlerthread. raise RuntimeError, "This function is designed to run in its own thread." @@ -842,12 +947,10 @@ class Connection(object): self._sendline.notify() # Attempt initial registration. - nick = self.nick[0] - if self.passwd: - self._send(u"PASS %s" % self.passwd) - self._trynick() - self._send(u"USER %s * * :%s" % - (self.username.split("\n")[0].rstrip(), self.realname.split("\n")[0].rstrip())) + # nick=self.nick[0] + # if self.passwd: + #self.send(u"PASS %s" % self.passwd) + # self._trynick() # Initialize buffers linebuf = [] @@ -860,7 +963,7 @@ class Connection(object): 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.identity.nick, self.identity.server) if self.identity else ("*", self.server)), self) self._outgoing.append(pingreq) self._sendline.notify() @@ -887,7 +990,7 @@ class Connection(object): exc, excmsg, tb = sys.exc_info() with self.lock: self.logwrite( - "*** Connection to %(server)s:%(port)s failed: %(excmsg)s." % vars()) + "*** Connection to {self:uri} failed: {excmsg}.".format(**vars())) self._event(self.getalladdons(), [ ("onConnectFail", dict(exc=exc, excmsg=excmsg, tb=tb), False)]) @@ -928,10 +1031,7 @@ class Connection(object): pass except: # Print exception to log file - self.logwrite(*["!!! FATAL Exception"] + ["!!! %s" % - line for line in traceback.format_exc().split("\n")]) - print >>sys.stderr, "FATAL Exception" - print >>sys.stderr, traceback.format_exc() + self.logerror(u"FATAL Exception") sys.exit() finally: @@ -944,8 +1044,19 @@ class Connection(object): self._outgoing.append("quit") self._sendline.notify() - # Gets a list of *all* addons, including channel-specific addons. + def lower(self, s): + """lower(s) + + Transforms a string into lowercase, using whatever casemapping the server is using, whether ascii or rfc1459.""" + if self.supports.get("CASEMAPPING", "rfc1459") == "ascii": + return s.lower() + else: + return s.translate(_rfc1459casemapping) + def getalladdons(self): + """getalladdons() --> list + + Returns list of *all* addons, including channel-specific addons.""" return self.addons + reduce(lambda x, y: x + y, [chan.addons for chan in self.channels], []) # The following methods matching parse* are used to determine what addon methods will be called, and prepares the arguments to be passed. @@ -959,6 +1070,20 @@ class Connection(object): # 'fallback' is a flag to determine when a fallback to 'onOther' is permitted. # Each of these functions should allow passing None to all arguments, in # which case, should report back *all* supported methods. + def parseCAP(self, origin=None, target=None, targetprefix=None, params=None, extinfo=None, outgoing=False): + if outgoing: + return ([], []) + if origin == None: + return (None, [ + ("onCapLS", dict(origin=None, capabilities=None), True), + ("onCapAck", dict(origin=None, capabilities=None), True), + ]) + if params.upper() == "LS": + return (self.getalladdons(), [("onCapLS", dict(capabilities=extinfo.split()), True)]) + if params.upper() == "ACK": + return (self.getalladdons(), [("onCapAck", dict(capabilities=extinfo.split()), True)]) + return ([], []) + def parse001(self, origin=None, target=None, targetprefix=None, params=None, extinfo=None): return (self.getalladdons(), [("onWelcome", dict(origin=origin, msg=extinfo), True)]) @@ -1015,10 +1140,10 @@ class Connection(object): return (self.addons, [("onChanCount", dict(origin=origin, chancount=chancount), True)]) def parse305(self, origin=None, target=None, targetprefix=None, params=None, extinfo=None): # Returned from away status - return (self.getalladdons(), [("onReturn", dict(origin=origin, msg=extinfo), True)]) + return (self.getalladdons(), [("onMeReturn", dict(origin=origin, msg=extinfo), True)]) def parse306(self, origin=None, target=None, targetprefix=None, params=None, extinfo=None): # Entered away status - return (self.getalladdons(), [("onAway", dict(origin=origin, msg=extinfo), True)]) + return (self.getalladdons(), [("onMeAway", dict(origin=origin, msg=extinfo), True)]) def parse311(self, origin=None, target=None, targetprefix=None, params=None, extinfo=None): # Start of WHOIS data if origin == None: @@ -1064,6 +1189,7 @@ class Connection(object): return (None, [("onWhoisServer", dict(origin=None, user=None, nickname=None, server=None, servername=None), True)]) nickname, server = params.split(" ") user = self.user(nickname) + server = self.getserver(server) return (self.addons, [("onWhoisServer", dict(origin=origin, user=user, nickname=nickname, server=server, servername=extinfo), True)]) def parse313(self, origin=None, target=None, targetprefix=None, params=None, extinfo=None): # IRC Op @@ -1107,7 +1233,7 @@ class Connection(object): return (self.addons, [("onWhoisEnd", dict(origin=origin, user=user, nickname=params, msg=extinfo), True)]) def parse321(self, origin=None, target=None, targetprefix=None, params=None, extinfo=None): # Start LIST - return (None, [("onListStart", dict(origin=origin, params=params, extinfo=extinfo), True)]) + return (self.addons, [("onListStart", dict(origin=origin, params=params, extinfo=extinfo), True)]) def parse322(self, origin=None, target=None, targetprefix=None, params=None, extinfo=None): # LIST item if origin == None: @@ -1119,7 +1245,7 @@ class Connection(object): return (self.addons, [("onListEntry", dict(origin=origin, channel=chan, population=int(pop), extinfo=extinfo), True)]) def parse323(self, origin=None, target=None, targetprefix=None, params=None, extinfo=None): # End of LIST - return (None, [("onListEnd", dict(origin=None, endmsg=None), True)]) + return (self.addons, [("onListEnd", dict(origin=None, endmsg=None), True)]) def parse324(self, origin=None, target=None, targetprefix=None, params=None, extinfo=None): # Channel Modes if origin == None: @@ -1180,6 +1306,7 @@ class Connection(object): channel = None user = self.user(nick) + serv = self.getserver(serv) if type(channel) == Channel: return (self.addons + channel.addons, [("onWhoEntry", dict(origin=origin, channel=channel, user=user, channame=channame, username=username, host=host, serv=serv, nick=nick, flags=flags, hops=int(hops), realname=realname), True)]) @@ -1227,6 +1354,53 @@ class Connection(object): def parse376(self, origin=None, target=None, targetprefix=None, params=None, extinfo=None): return (self.addons, [("onMOTDEnd", dict(origin=origin, motdend=extinfo), True)]) + def parseACCOUNT(self, origin=None, target=None, targetprefix=None, params=None, extinfo=None, outgoing=False): + if outgoing: + return ([], []) + if origin == None: + return (None, [ + ("onAccountLogin", dict(user=None, account=None), True), + ("onMeAccountLogin", dict(account=None), False), + ("onAccountLogout", dict(user=None), True), + ("onMeAccountLogout", dict(), False) + ]) + + addons = reduce( + lambda x, y: x + y, [channel.addons for channel in origin.channels if self.identity in channel.users], []) + + if origin == self.identity: + if target == "*": + return (self.addons + addons, [ + ("onAccountLogout", dict(user=origin), True), + ("onMeAccountLogout", dict(), False) + ]) + else: + return (self.addons + addons, [ + ("onAccountLogin", dict( + user=origin, account=target), True), + ("onMeAccountLogin", dict(account=target), False) + ]) + else: + if target == "*": + return (self.addons + addons, [("onAccountLogout", dict(user=origin), True)]) + else: + return (self.addons + addons, [("onAccountLogin", dict(user=origin, account=target), True)]) + + def parseAWAY(self, origin=None, target=None, targetprefix=None, params=None, extinfo=None, outgoing=False): + if outgoing: + return ([], []) + + if origin == None: + return (None, [("onAway", dict(user=None, awaymsg=None), True), ("onReturn", dict(user=None), True)]) + + addons = reduce( + lambda x, y: x + y, [channel.addons for channel in origin.channels if self.identity in channel.users], []) + + if extinfo: + return (self.addons + addons, [("onAway", dict(user=origin, awaymsg=extinfo), True)]) + else: + return (self.addons + addons, [("onReturn", dict(user=origin), True)]) + def parseNICK(self, origin=None, target=None, targetprefix=None, params=None, extinfo=None, outgoing=False): if outgoing: return ([], []) @@ -1255,8 +1429,8 @@ class Connection(object): if origin == None: return (None, [ - ("onMeJoin", dict(channel=None), False), - ("onJoin", dict(user=None, channel=None), True) + ("onMeJoin", dict(channel=None, loggedinas=None, realname=None), False), + ("onJoin", dict(user=None, channel=None, loggedinas=None, realname=None), True) ]) if type(target) == Channel: @@ -1265,13 +1439,20 @@ class Connection(object): channel = self.channel(extinfo) channel.name = extinfo + if "extended-join" in self.enabledcaps: + loggedinas = params if params != "*" else None + realname = extinfo + else: + loggedinas = realname = None + if origin == self.identity: return (self.addons + channel.addons, [ - ("onMeJoin", dict(channel=channel), False), - ("onJoin", dict(user=origin, channel=channel), True), + ("onMeJoin", dict(channel=channel, loggedinas=loggedinas, realname=realname), False), + ("onJoin", dict(user=origin, channel=channel, + loggedinas=loggedinas, realname=realname), True), ]) - return (self.addons + channel.addons, [("onJoin", dict(user=origin, channel=channel), True)]) + return (self.addons + channel.addons, [("onJoin", dict(user=origin, channel=channel, loggedinas=loggedinas, realname=realname), True)]) def parseKICK(self, origin=None, target=None, targetprefix=None, params=None, extinfo=None, outgoing=False): if outgoing: @@ -1486,14 +1667,16 @@ class Connection(object): return (self.addons + target.addons, [("onSendChanMsg", dict(origin=origin, channel=target, targetprefix=targetprefix, msg=extinfo), True)]) if origin == None: return (None, [ - ("onPrivMsg", dict(user=None, msg=None), True), - ("onChanMsg", dict(user=None, channel=None, targetprefix=None, msg=None), True), - ("onCTCP", dict(user=None, ctcptype=None, params=None), True), - ("onChanCTCP", dict(user=None, channel=None, - targetprefix=None, ctcptype=None, params=None), True), - ("onPrivAction", dict(user=None, action=None), True), - ("onChanAction", dict( - user=None, channel=None, targetprefix=None, action=None), True), + ("onPrivMsg", dict(user=None, msg=None, identified=None), True), + ("onChanMsg", dict(user=None, channel=None, + targetprefix=None, msg=None, identified=None), True), + ("onCTCP", dict(user=None, ctcptype=None, params=None, identified=None), True), + ("onChanCTCP", dict(user=None, channel=None, targetprefix=None, + ctcptype=None, params=None, identified=None), True), + ("onPrivAction", dict( + user=None, action=None, identified=None), True), + ("onChanAction", dict(user=None, channel=None, + targetprefix=None, action=None, identified=None), True), ("onSendPrivMsg", dict( origin=None, user=None, msg=None), True), ("onSendChanMsg", dict( @@ -1506,22 +1689,26 @@ class Connection(object): ("onSendChanCTCP", dict(origin=None, channel=None, targetprefix=None, ctcptype=None, params=None), True), ]) + if "identify-msg" in self.enabledcaps and extinfo[0] in "+-": + identified, extinfo = extinfo.startswith("+"), extinfo[1:] + else: + identified = None ctcp = re.findall(_ctcpmatch, extinfo) if ctcp: (ctcptype, ext) = ctcp[0] if target == self.identity: if ctcptype.upper() == "ACTION": - return (self.addons, [("onPrivAction", dict(user=origin, action=ext), True)]) - return (self.addons, [("onCTCP", dict(user=origin, ctcptype=ctcptype, params=ext), True)]) + return (self.addons, [("onPrivAction", dict(user=origin, action=ext, identified=identified), True)]) + return (self.addons, [("onCTCP", dict(user=origin, ctcptype=ctcptype, params=ext, identified=identified), True)]) if type(target) == Channel: if ctcptype.upper() == "ACTION": - return (self.addons, [("onChanAction", dict(user=origin, channel=target, targetprefix=targetprefix, action=ext), True)]) - return (self.addons, [("onChanCTCP", dict(user=origin, channel=target, targetprefix=targetprefix, ctcptype=ctcptype, params=ext), True)]) + return (self.addons, [("onChanAction", dict(user=origin, channel=target, targetprefix=targetprefix, action=ext, identified=identified), True)]) + return (self.addons, [("onChanCTCP", dict(user=origin, channel=target, targetprefix=targetprefix, ctcptype=ctcptype, params=ext, identified=identified), True)]) else: if type(target) == Channel: - return (self.addons + target.addons, [("onChanMsg", dict(user=origin, channel=target, targetprefix=targetprefix, msg=extinfo), True)]) + return (self.addons + target.addons, [("onChanMsg", dict(user=origin, channel=target, targetprefix=targetprefix, msg=extinfo, identified=identified), True)]) elif target == self.identity: - return (self.addons, [("onPrivMsg", dict(user=origin, msg=extinfo), True)]) + return (self.addons, [("onPrivMsg", dict(user=origin, msg=extinfo, identified=identified), True)]) def parseNOTICE(self, origin=None, target=None, targetprefix=None, params=None, extinfo=None, outgoing=False): if outgoing: @@ -1536,28 +1723,35 @@ class Connection(object): return (self.addons, [("onSendPrivNotice", dict(origin=origin, user=target, msg=extinfo), True)]) if origin == None: return (None, [ - ("onPrivNotice", dict(origin=None, msg=None), True), - ("onChanNotice", dict( - origin=None, channel=None, targetprefix=None, msg=None), True), + ("onPrivNotice", dict( + origin=None, msg=None, identified=None), True), + ("onServNotice", dict( + origin=None, msg=None, identified=None), True), + ("onChanNotice", dict(origin=None, channel=None, + targetprefix=None, msg=None, identified=None), True), ("onCTCPReply", dict( - origin=None, ctcptype=None, params=None), True), + origin=None, ctcptype=None, params=None, identified=None), True), ("onSendPrivNotice", dict(origin=None, msg=None), True), ("onSendChanNotice", dict( origin=None, channel=None, targetprefix=None, msg=None), True), ("onSendCTCPReply", dict( origin=None, ctcptype=None, params=None), True), ]) + if "identify-msg" in self.enabledcaps and extinfo[0] in "+-": + identified, extinfo = extinfo.startswith("+"), extinfo[1:] + else: + identified = None ctcp = re.findall(_ctcpmatch, extinfo) - # print ctcp if ctcp and target == self.identity: (ctcptype, ext) = ctcp[0] - return (self.addons, [("onCTCPReply", dict(origin=origin, ctcptype=ctcptype, params=ext), True)]) + return (self.addons, [("onCTCPReply", dict(origin=origin, ctcptype=ctcptype, params=ext, identified=identified), True)]) else: + if type(origin) == Server: + return (self.addons, [("onServNotice", dict(origin=origin, msg=extinfo, identified=identified), True)]) if type(target) == Channel: - return (self.addons + target.addons, [("onChanNotice", dict(origin=origin, channel=target, targetprefix=targetprefix, msg=extinfo), True)]) + return (self.addons + target.addons, [("onChanNotice", dict(origin=origin, channel=target, targetprefix=targetprefix, msg=extinfo, identified=identified), True)]) elif target == self.identity: - # print "onPrivNotice" - return (self.addons, [("onPrivNotice", dict(origin=origin, msg=extinfo), True)]) + return (self.addons, [("onPrivNotice", dict(origin=origin, msg=extinfo, identified=identified), True)]) def parse367(self, origin=None, target=None, targetprefix=None, params=None, extinfo=None): # Channel Ban list if origin == None: @@ -1652,6 +1846,9 @@ class Connection(object): return (self.addons + channel.addons, [("onQuietListEnd", dict(channel=channel, endmsg=extinfo), True)]) def eventsupports(self): + """eventsupports() --> {eventname: arguments, ...} + + Generates and returns a dict of supported events and associated arguments. Good for attempting to validate addon events.""" supports = {} for item in dir(self): if re.match(r"parse(\d{3}|[A-Z]+)", item): @@ -1673,7 +1870,100 @@ class Connection(object): }) return supports + def _register(self): + if self.passwd: + self.send(u"PASS %s" % self.passwd) + self._trynick() + self.send( + u"USER {self.username} * * :{self.realname}".format(**vars())) + + def requestcapls(self, origin=None): + """requestcapls(...) + + Sends "CAP LS" to the server to request supported capabilities. Please use this method instead of send().""" + if not self._caplsrequested: + self.send("CAP LS", origin=origin) + self._caplsrequested = True + # Here are the builtin event handlers. + + def onRecv(self, context, line, origin, cmd, target, targetprefix, params, extinfo): + if not self._registered: + if type(cmd) == int and cmd < 100 and target != "*": # Registration complete! + self.identity = self.user(target, init=True) + self.identity.server = origin + self._event(self.getalladdons(), [ + ("onRegistered", dict(), False)]) + + def onRegistered(self, context): + self._registered = True + + def onConnect(self, context): + if self.requestcaps: + self.requestcapls() + elif self.starttls: + self.send("STARTLS") + elif len(self._requestedcaps) == 0 and not self._caplsrequested: + self._register() + + def onCapLS(self, context, capabilities): + self.supportedcaps = capabilities + self._caplsrequested = False + if self.starttls and "tls" in capabilities and not self.secure and not self._starttls: + self.send("STARTTLS") + elif not self.registered: + requestcaps = [ + cap for cap in self.requestcaps if cap in capabilities] + if requestcaps: + self.sendcapsrequest(requestcaps) + elif len(self._requestedcaps) == 0: + self.send("CAP END") + self._register() + + def onCapAck(self, context, capabilities): + for cap in capabilities: + mods, capname = re.findall( + "^([%s]*)(.+)$" % re.escape(_capmodifiers), cap)[0] + if "-" in mods and capname in self.enabledcaps: + self.enabledcaps.remove(capname) + elif cap not in self.enabledcaps: + self.enabledcaps.append(capname) + if cap in self._requestedcaps: + self._requestedcaps.remove(cap) + if not self.registered and len(self._requestedcaps) == 0: + self.send("CAP END") + self._register() + + def onCapNak(self, context, capabilities): + for cap in capabilities: + mods, capname = re.findall( + "^([%s]*)(.+)$" % re.escape(_capmodifiers), cap)[0] + if cap in self._requestedcaps: + self._requestedcaps.remove(cap) + if not self.registered and len(self._requestedcaps) == 0: + self.send("CAP END") + self._register() + + def on433(self, context, line, origin, target, params, extinfo): + if not self._registered: # Server reports nick taken, so we need to try another. + self._trynick() + + def on670(self, context, line, origin, target, params, extinfo): + self.logwrite("*** Attempting StartTLS") + self._connection = ssl.wrap_socket( + self._connection, cert_reqs=ssl.CERT_NONE) # Server says go ahead with starttls. + self._event(self.getalladdons(), [("onStartTLS", dict(), False)]) + + def on691(self, context, line, origin, target, params, extinfo): # STARTTLS Failure + self.logwrite("*** StartTLS Failed") + if self.requestcaps: + self.send("CAP END") + self._register() + + def onStartTLS(self, context): + self._starttls = True + self.onConnect(self) + def onWelcome(self, context, origin, msg): self.welcome = msg # Welcome message @@ -1690,7 +1980,7 @@ class Connection(object): protos = u" ".join( [proto for proto in self.protoctl if proto in supports.keys()]) if protos: - self._send(u"PROTOCTL {protos}".format(**vars())) + self.send(u"PROTOCTL {protos}".format(**vars())) self.supports.update(supports) def onSnoMask(self, context, origin, snomask): # Snomask @@ -1712,13 +2002,20 @@ class Connection(object): def onChanCount(self, context, origin, chancount): self.chancount = chancount - def onReturn(self, identity, origin, msg): # Returned from away status + def onReturn(self, context, user): # Returned from away status + user.away = False + user.awaymsg = None + + def onAway(self, context, user, awaymsg): # Entered away status + user.away = True + user.awaymsg = awaymsg + + def onMeReturn(self, context, origin): # Returned from away status self.identity.away = False self.identity.awaymsg = None - def onAway(self, identity, origin, msg): # Entered away status + def onMeAway(self, context, origin, msg): # Entered away status self.identity.away = True - self.identity.awaymsg = msg def onWhoisStart(self, context, origin, user, nickname, username, host, realname): # Start of WHOIS data user.nick = nickname @@ -1807,14 +2104,14 @@ class Connection(object): channel.modes[mode].append(user) def onMOTDLine(self, context, origin, motdline): # MOTD line - self.motd.append(motdline) + origin.motd.append(motdline) def onMOTDStart(self, context, origin, motdgreet): # Begin MOTD - self.motdgreet = motdgreet - self.motd = [] + origin.motdgreet = motdgreet + origin.motd = [] def onMOTDEnd(self, context, origin, motdend): - self.motdend = motdend # End of MOTD + origin.motdend = motdend # End of MOTD # elif cmd==386 and "q" in self.supports["PREFIX"][0]: # Channel Owner (Unreal) #(channame,owner)=params.split() @@ -1838,24 +2135,49 @@ class Connection(object): #if user not in channel.modes["a"]: channel.modes["a"].append(user) # else: channel.modes["a"]=[user] + # def onNickChange(self, context, user, newnick): + # for other in self.users: + # if self.supports.get("CASEMAPPING", "rfc1459")=="ascii": + # collision=other.nick.lower()==newnick.lower() + # else: + # collision=other.nick.translate(_rfc1459casemapping)==newnick.translate(_rfc1459casemapping) + # if collision: + # self.users.remove(other) ### Nick collision, safe to assume this orphaned user is offline, so we shall remove the old instance. + # for channel in self.channels: + # If for some odd reason, the old user still appears common channels, then we will remove the user anyway. + # if other in channel.users: + # channel.users.remove(other) + # user.nick=newnick + def onNickChange(self, context, user, newnick): - for other in self.users: - if self.supports.get("CASEMAPPING", "rfc1459") == "ascii": - collision = other.nick.lower() == newnick.lower() + if self.lower(user.nick) == self.lower(newnick): + user.nick = newnick + else: + try: + other = self.users[newnick] + except KeyError: + pass else: - collision = other.nick.translate( - _rfc1459casemapping) == newnick.translate(_rfc1459casemapping) - if collision: - self.users.remove( - other) # Nick collision, safe to assume this orphaned user is offline, so we shall remove the old instance. for channel in self.channels: # If for some odd reason, the old user still appears common # channels, then we will remove the user anyway. if other in channel.users: channel.users.remove(other) - user.nick = newnick + self.users.remove(other) + self.users.remove(user) + user.nick = newnick + self.users.append(user) + + def onAccountLogin(self, context, user, account): + user.loggedinas = account - def onJoin(self, context, user, channel): + def onAccountLogout(self, context, user): + user.loggedinas = None + + def onJoin(self, context, user, channel, loggedinas, realname): + if "extended-join" in self.enabledcaps: + user.loggedinas = loggedinas + user.realname = realname if channel not in user.channels: user.channels.append(channel) if user not in channel.users: @@ -1867,10 +2189,10 @@ class Connection(object): if channel._joinrequested: channel._joinreply = "JOIN" channel._joining.notify() - self._send(u"MODE %s" % channel.name) - self._send(u"WHO %s" % channel.name) - self._send(u"MODE %s :%s" % - (channel.name, self.supports.get("CHANMODES", _defaultchanmodes)[0])) + self.send(u"MODE %s" % channel.name) + self.send(u"WHO %s" % channel.name) + self.send(u"MODE %s :%s" % + (channel.name, self.supports.get("CHANMODES", _defaultchanmodes)[0])) def onKick(self, context, kicker, channel, kicked, kickmsg): if channel in kicked.channels: @@ -2082,10 +2404,19 @@ class Connection(object): nick = self.nick[s] if type(self.nick) in (list, tuple) else self.nick if q > 0: nick = "%s%d" % (nick, q) - self._send(u"NICK %s" % nick) + self.send(u"NICK %s" % nick) self.trynick += 1 - def _send(self, line, origin=None, T=None): + def send(self, line, origin=None, T=None): + """send(line[, ...]) + + Sends 'line' to IRC server. Try to use this method sparingly by using other methods designed to format requests correctly. + Supported optional arguments: + + 'origin': Used (voluntarily) by addons to identify origin of sent data. Good for helping addons ignore lines they send + so as to avoid infinite loops. + + 'T': Specifies what time to send the data if not immediately. This method currently throttles PRIVMSGs to avoid floods.""" with self.lock: if not self.connected: raise NotConnected @@ -2125,6 +2456,7 @@ class Connection(object): self._sendline.notify() def _procsendline(self, line, origin=None): + """Function responsible for sending data to the IRC server and calling all applicable event methods.""" match = re.findall(_ircmatch, line) if len(match) == 0: return @@ -2176,31 +2508,6 @@ class Connection(object): elif cscmd[0].upper() not in ("GLIST", "ACCESS", "SASET", "DROP", "SENDPASS", "ALIST", "INFO", "LIST", "LOGOUT", "STATUS", "UPDATE", "GETPASS", "FORBID", "SUSPEND", "UNSUSPEND", "OINFO"): extinfo = "********" line = "%s %s :%s" % (cmd, target, extinfo) - - #ctcp=re.findall(_ctcpmatch, extinfo) - # if ctcp: - #(ctcptype,ext)=ctcp[0] - # if ctcptype.upper()=="ACTION": - # if type(target)==Channel: - #self._event("onSendChanAction", self.addons+target.addons, origin=origin, channel=target, targetprefix=targetprefix, action=ext) - # elif type(target)==User: - #self._event("onSendPrivAction", self.addons, origin=origin, user=target, action=ext) - # else: - # if type(target)==Channel: - #self._event("onSendChanCTCP", self.addons+target.addons, origin=origin, channel=target, targetprefix=targetprefix, ctcptype=ctcptype, params=ext) - # elif type(target)==User: - #self._event("onSendPrivCTCP", self.addons, origin=origin, user=target, ctcptype=ctcptype, params=ext) - # else: - # if type(target)==Channel: - #self._event("onSendChanMsg", self.addons+target.addons, origin=origin, channel=target, targetprefix=targetprefix, msg=extinfo) - # elif type(target)==User: - #self._event("onSendPrivMsg", self.addons, origin=origin, user=target, msg=extinfo) - # elif target.upper()=="CHANSERV": - #msg=extinfo.split(" ") - # if msg[0].upper() in ("IDENTIFY", "REGISTER") and len(msg)>2: - # msg[2]="********" - #extinfo=" ".join(msg) - #line="%s %s :%s"%(cmd, target, extinfo) elif cmd.upper() in ("NS", "NICKSERV"): if target.upper() in ("IDENTIFY", "REGISTER"): params = params.split(" ") @@ -2247,7 +2554,7 @@ class Connection(object): target.name = channame # Check to see if target matches a valid nickname. Do NOT convert # target to User instance if cmd is NICK. - elif re.match(_nickmatch, target) and cmd != "NICK": + elif re.match(_nickmatch, target) and cmd in ("PRIVMSG", "NOTICE", "MODE", "INVITE", "CHGHOST", "CHGIDENT", "CHGNAME", "WHOIS", "KILL", "SAMODE", "SETHOST", "WHO"): targetprefix = "" target = self.user(target) @@ -2260,20 +2567,13 @@ class Connection(object): parsemethod = getattr(self, parsename) if callable(parsemethod): try: - addons, events = parsemethod( + ret = parsemethod( origin, target, targetprefix, params, extinfo, outgoing=True) + addons, events = ret if ret is not None else ( + self.events, []) except: - exc, excmsg, tb = sys.exc_info() - - # Print to log AND stderr - tblines = [ - u"!!! There was an error in parsing the following line:", u"!!! %s" % line] - for tbline in traceback.format_exc().split("\n"): - tblines.append(u"!!! %s" % autodecode(tbline)) - self.logwrite(*tblines) - print >>sys.stderr, u"There was an error in parsing the following line:" - print >>sys.stderr, u"%s" % line - print >>sys.stderr, traceback.format_exc() + self.logerror( + u"There was an error in parsing the following line:", line) return else: addons = self.addons @@ -2288,8 +2588,8 @@ class Connection(object): if cmd not in ("PING", "PONG") or not self.quietpingpong: # Supress pings and pongs if self.quietpingpong is set to True self._event( - addons + [self], [("onSend", dict(origin=origin if origin else self, line=line, cmd=cmd, target=target, targetprefix=targetprefix, params=params, extinfo=extinfo), False)], line) - self._event(addons + [self], events, line) + addons, [("onSend", dict(origin=origin if origin else self, line=line, cmd=cmd, target=target, targetprefix=targetprefix, params=params, extinfo=extinfo), False)], line) + self._event(addons, events, line) if not (cmd in ("PING", "PONG") and self.quietpingpong): #self._event(self.addons, [("onSend" , dict(origin=origin, line=line, cmd=cmd, target=target, params=params, extinfo=extinfo), False)]) @@ -2347,14 +2647,8 @@ class Connection(object): pass except: - tb = traceback.format_exc() self._quitexpected = True - tblines = [u"!!! FATAL Exception"] - for line in traceback.format_exc().split("\n"): - tblines.append(u"!!! %s" % autodecode(line)) - self.logwrite(*tblines) - print >>sys.stderr, "FATAL Exception in {self}".format(**vars()) - print >>sys.stderr, tb + self.logerror("FATAL Exception in {self}".format(**vars())) with self._sendline: try: self._connection.send( @@ -2366,14 +2660,12 @@ class Connection(object): with self._sendline: self._outgoing.clear() # Clear out _outgoing. - # For compatibility, when modules still expect irc.Connection to be a - # subclass of threading.Thread def isAlive(self): + """For compatibility, when modules still expect irc.Connection to be a subclass of threading.Thread.""" return type(self._recvhandlerthread) == Thread and self._recvhandlerthread.isAlive() and type(self._sendhandlerthread) == Thread and self._sendhandlerthread.isAlive() - # For compatibility, when modules still expect irc.Connection to be a - # subclass of threading.Thread def start(self): + """For compatibility, when modules still expect irc.Connection to be a subclass of threading.Thread.""" return self.connect() def __repr__(self): @@ -2394,51 +2686,86 @@ class Connection(object): return "irc{ssl}{proto}://[{self.server}]:{port}".format(**locals()) else: return "irc{ssl}{proto}://{self.server}:{port}".format(**locals()) + else: + return repr(self) def oper(self, name, passwd, origin=None): + """oper(name, passwd[, origin]) + + Sends an OPER request to the server. Warning: Invalid oper credentials may be reported to IRC network admins!""" if re.match(".*[\n\r\\s]", name) or re.match(".*[\n\r\\s]", passwd): raise InvalidCharacter - self._send(u"OPER {name} {passwd}".format(**vars()), origin=origin) + self.send(u"OPER {name} {passwd}".format(**vars()), origin=origin) def list(self, params="", origin=None): + """list(...) + + Sends a LIST request to the server. + TODO: Implement optional blocking.""" if re.match(".*[\n\r\\s]", params): raise InvalidCharacter if params: - self._send(u"LIST {params}".format(**vars()), origin=origin) + self.send(u"LIST {params}".format(**vars()), origin=origin) else: - self._send(u"LIST", origin=origin) + self.send(u"LIST", origin=origin) - def getmotd(self, target="", origin=None): - if re.match(".*[\n\r\\s]", name) or re.match(".*[\n\r\\s]", passwd): - raise InvalidCharacter - if len(re.findall("^([^\r\n\\s]*)", target)[0]): - self._send(u"MOTD %s" % - (re.findall("^([^\r\n\\s]*)", target)[0]), origin=origin) + def getmotd(self, server=None, origin=None): + """getmotd(...) + + Sends an MOTD request to the server, optionally specifying server. + TODO: Implement optional blocking.""" + if server: + self.send(u"MOTD %s" % server.name, origin=origin) else: - self._send(u"MOTD", origin=origin) + self.send(u"MOTD", origin=origin) + + def version(self, server=None, origin=None): + """version(...) - def version(self, target="", origin=None): - if len(re.findall("^([^\r\n\\s]*)", target)[0]): - self._send(u"VERSION %s" % - (re.findall("^([^\r\n\\s]*)", target)[0]), origin=origin) + Sends an VERSION request to the server, optionally specifying server. + This is NOT the same as requesting CTCP version from another user. + TODO: Implement optional blocking.""" + if server: + self.send(u"VERSION %s" % server.name, origin=origin) else: - self._send(u"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._send(u"STATS %s %s" % - (query, re.findall("^([^\r\n\\s]*)", target)[0]), origin=origin) + def stats(self, query, server=None, origin=None): + """stats(query[,...]) + + Sends an STATS request to the server, optionally specifying server. + STATS requests may be logged by IRC network admins. Use responsibly! + TODO: Implement optional blocking.""" + if server: + self.send(u"STATS %s %s" % (query, server.name), origin=origin) else: - self._send(u"STATS %s" % query, origin=origin) + self.send(u"STATS %s" % query, origin=origin) + + def sendcapsrequest(self, capabilities, origin=None): + """sendcapsrequest(capabilities) + + Request capabilities with "CAP REQ". Please use this method instead of using send(...).""" + with self.lock: + for cap in capabilities: + if cap not in self._requestedcaps: + self._requestedcaps.append(cap) + self.send("CAP REQ {cap}".format(**vars()), origin=origin) - # Quit IRC session gracefully def quit(self, msg="", origin=None, blocking=False): + """quit(...) + + Quit IRC session gracefully by first sending a QUIT request to the server. + + Optional arguments: + 'msg': Quit message + 'origin': See help on method 'send' + 'blocking': Wait until connection is terminated.""" if "\r" in msg or "\n" in msg: raise InvalidCharacter if msg: - self._send(u"QUIT :%s" % msg, origin=origin) + self.send(u"QUIT :%s" % msg, origin=origin) else: - self._send(u"QUIT", origin=origin) + self.send(u"QUIT", origin=origin) if blocking: with self._disconnecting: while self.connected: @@ -2446,13 +2773,19 @@ class Connection(object): self._recvhandlerthread.join() self._sendhandlerthread.join() - # Force disconnect -- Not even sending QUIT to server. def disconnect(self): + """disconnect() + + Force disconnect -- Goes right for the jugular, not even sending QUIT to server.""" with self.lock: self._quitexpected = True self._connection.shutdown(2) def ctcpversion(self): + """ctcpversion() --> string + + Formats a CTCP version reply from this instance and all attached addons.""" + reply = [] # Prepare reply for this module reply.append( @@ -2478,58 +2811,72 @@ class Connection(object): return u"; ".join(reply) def raw(self, line, origin=None): - self._send(line, origin=origin) + """raw(line[, origin]) + + Deprecated. Use send() instead.""" + self.send(line, origin=origin) def user(self, nick, init=False): - if type(nick) == str: - nick = autodecode(nick) - if self.supports.get("CASEMAPPING", "rfc1459") == "ascii": - users = [ - user for user in self.users if user.nick.lower() == nick.lower()] - else: - 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) - self.users.append(user) - timestamp = reduce(lambda x, y: x + ":" + y, [ - str(t).rjust(2, "0") for t in time.localtime()[0:6]]) - return user + """user(nick) + + Return a User object associated with a nickname. + Specify init=True to reset all that is known about user.""" + with self.lock: + try: + return self.users[nick] + except KeyError: + user = User(nick, self) + self.users.append(user) + return user def channel(self, name, init=False): - if type(name) == str: - name = autodecode(name) - if self.supports.get("CASEMAPPING", "rfc1459") == "ascii": - channels = [ - chan for chan in self.channels if chan.name.lower() == name.lower()] - else: - 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, [ - str(t).rjust(2, "0") for t in time.localtime()[0:6]]) - chan = Channel(name, self) - self.channels.append(chan) - return chan + """channel(name) + + Return a Channel object associated with a channel name. + Specify init=True to reset all that is known about the channel.""" + with self.lock: + try: + return self.channels[name] + except KeyError: + channel = Channel(name, self) + self.channels.append(channel) + return channel + + def getserver(self, name, init=False): + """server(name) + + Return a Server object associated with a server name. + Specify init=True to reset all that is known about the server.""" + with self.lock: + if type(name) == str: + name = autodecode(name) + servers = [server for server in self.servers if self.lower( + server.name) == self.lower(name)] + + if len(servers): + if init: + servers[0]._init() + return servers[0] + else: + server = Server(name, self) + self.servers.append(server) + return server def __getitem__(self, item): chantypes = self.supports.get("CHANTYPES", _defaultchantypes) + if "\r" in item or "\n" in item or " " in item: + raise InvalidCharacter 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." + return self.getserver(item) def fmtsupports(self): + """fmtsupports() --> list + + Formats a valid 005 response from known information.""" supports = [ "CHANMODES=%s" % (",".join(value)) if name == "CHANMODES" else "PREFIX=(%s)%s" % value if name == "PREFIX" else "%s=%s" % (name, value) if value else name for name, value in self.supports.items()] @@ -2548,7 +2895,9 @@ class Connection(object): return lines def fmtgreeting(self): - # Prepare greeting (Responses 001 through 004) + """fmtgreeting() --> list + + Formats a valid greeting from known information (Responses 001 through 004).""" lines = [] if self.welcome: lines.append( @@ -2565,14 +2914,21 @@ class Connection(object): return lines def fmtusermodes(self): - # Prepars 221 response + """fmtusermodes() --> list + + Formats a valid user modes reply from known information (Response 221).""" return u":{self.serv} 221 {self.identity.nick} +{self.identity.modes}".format(**vars()) def fmtsnomasks(self): - # Prepare 008 response + """fmtsnomasks() --> list + + Formats a valid snomasks reply from known information (Response 008).""" return u":{self.serv} 008 {self.identity.nick} +{self.identity.snomask} :Server notice mask".format(**vars()) def fmtmotd(self): + """fmtmotd() --> list + + Formats a valid MOTD reply from known information (Response 375, 372, and 376; Response 422 if no MOTD).""" if self.motdgreet and self.motd and self.motdend: lines = [] lines.append( @@ -2606,7 +2962,7 @@ class Channel(object): self._partreply = None def _init(self): - for user in self.context.users: + for user in self.context.users._dict.values(): if self in user.channels: user.channels.remove(self) self.addons = [] @@ -2622,19 +2978,19 @@ 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(u"PRIVMSG %s%s :%s" % - (target, self.name, line), origin=origin) + self.context.send(u"PRIVMSG %s%s :%s" % + (target, self.name, line), origin=origin) def who(self, origin=None, blocking=False): # Send WHO request to server - self.context._send(u"WHO %s" % (self.name), origin=origin) + self.context.send(u"WHO %s" % (self.name), origin=origin) def fmtwho(self): # Create WHO reply from current data. TODO pass def names(self, origin=None): - self.context._send(u"NAMES %s" % (self.name), origin=origin) + self.context.send(u"NAMES %s" % (self.name), origin=origin) def fmtnames(self, sort=None, uhnames=False, namesx=False): # Create NAMES reply from current data. @@ -2646,9 +3002,9 @@ class Channel(object): users = list(self.users) if sort == "mode": users.sort(key=lambda user: ([user not in self.modes.get(mode, []) - for mode, char in zip(*self.context.supports.get("PREFIX", ("ohv", "@%+")))], user.nick.lower())) + for mode, char in zip(*self.context.supports.get("PREFIX", ("ohv", "@%+")))], self.context.lower(user.nick))) elif sort == "nick": - users.sort(key=lambda user: user.nick.lower()) + users.sort(key=lambda user: self.context.lower(user.nick)) if uhnames: template = u"{prefixes}{user:full}" else: @@ -2715,12 +3071,12 @@ 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(u"NOTICE %s%s :%s" % - (target, self.name, line), origin=origin) + self.context.send(u"NOTICE %s%s :%s" % + (target, self.name, line), origin=origin) def settopic(self, msg, origin=None): - self.context._send(u"TOPIC %s :%s" % - (self.name, re.findall("^([^\r\n]*)", msg)[0]), origin=origin) + self.context.send(u"TOPIC %s :%s" % + (self.name, re.findall("^([^\r\n]*)", msg)[0]), origin=origin) def ctcp(self, act, msg="", origin=None): if len(re.findall("^([^\r\n]*)", msg)[0]): @@ -2750,10 +3106,10 @@ class Channel(object): raise ActionAlreadyRequested self._partrequested = True if len(re.findall("^([^\r\n]*)", msg)[0]): - self.context._send( + self.context.send( u"PART %s :%s" % (self.name, re.findall("^([^\r\n]*)", msg)[0]), origin=origin) else: - self.context._send(u"PART %s" % self.name, origin=origin) + self.context.send(u"PART %s" % self.name, origin=origin) # Anticipated Numeric Replies: @@ -2783,8 +3139,8 @@ class Channel(object): user) == User else re.findall("^([^\r\n\\s]*)", user)[0] if nickname == "": raise InvalidName - self.context._send(u"INVITE %s %s" % - (nickname, self.name), origin=origin) + self.context.send(u"INVITE %s %s" % + (nickname, self.name), origin=origin) def join(self, key="", blocking=False, timeout=30, origin=None): with self.context.lock: @@ -2799,10 +3155,10 @@ class Channel(object): raise ActionAlreadyRequested self._joinrequested = True if len(re.findall("^([^\r\n\\s]*)", key)[0]): - self.context._send( + self.context.send( u"JOIN %s %s" % (self.name, re.findall("^([^\r\n\\s]*)", key)[0]), origin=origin) else: - self.context._send(u"JOIN %s" % self.name, origin=origin) + self.context.send(u"JOIN %s" % self.name, origin=origin) # Anticipated Numeric Replies: @@ -2836,11 +3192,11 @@ class Channel(object): if nickname == "": raise InvalidName if len(re.findall("^([^\r\n]*)", msg)[0]): - self.context._send(u"KICK %s %s :%s" % - (self.name, nickname, re.findall("^([^\r\n]*)", msg)[0]), origin=origin) + self.context.send(u"KICK %s %s :%s" % + (self.name, nickname, re.findall("^([^\r\n]*)", msg)[0]), origin=origin) else: - self.context._send(u"KICK %s %s" % - (self.name, nickname), origin=origin) + self.context.send(u"KICK %s %s" % + (self.name, nickname), origin=origin) def __repr__(self): return u"<Channel: {self.name} on {self.context:uri}>".format(**vars()) @@ -2878,6 +3234,7 @@ class User(object): self.signontime = None self.secure = None self.away = None + self.loggedinas = None def __repr__(self): return (u"<User: %(nick)s!%(username)s@%(host)s>" % vars(self)).encode("utf8") @@ -2890,13 +3247,13 @@ class User(object): def msg(self, msg, origin=None): for line in re.findall("([^\r\n]+)", msg): - self.context._send(u"PRIVMSG %s :%s" % - (self.nick, line), origin=origin) + 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(u"NOTICE %s :%s" % - (self.nick, line), origin=origin) + 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]): @@ -2967,7 +3324,8 @@ class Config(object): class ChanList(list): - def __init__(self, iterable=None, context=None): + def __init__(self, iterable=None, context=None, withdict=False): + self._dict = {} if withdict else None if context != None and type(context) != Connection: raise TypeError, "context must be irc.Connection object or None" self.context = context @@ -2976,18 +3334,30 @@ class ChanList(list): for channel in iterable: if type(channel) == Channel: chanlist.append(channel) + if context and channel.context != context: + raise ValueError, "Channel object does not belong to context." 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) + if self._dict is not None: + if self.context: + self._dict.update( + {self.context.lower(channel.name): channel for channel in chanlist}) + else: + self._dict.update( + {(channel.context, channel.context.lower(channel.name)): channel for channel in 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)) + channel = self.context.channel(item) + list.append(self, channel) + if self._dict is not None: + self._dict[self.context.lower(item)] = channel return else: raise ValueError, "No context given for string object." @@ -2996,11 +3366,19 @@ class ChanList(list): if self.context and item.context != self.context: raise ValueError, "Channel object does not belong to context." list.append(self, item) + if self._dict is not None: + if self.context: + self._dict[self.context.lower(item.name)] = item + else: + self._dict[item.context, item.context.lower(item.name)] = item def insert(self, index, item): if type(item) in (str, unicode): if self.context: - list.insert(self, index, self.context.channel(item)) + channel = self.context.channel(item) + list.insert(self, index, channel) + if self._dict is not None: + self._dict[self.context.lower(item)] = channel return else: raise ValueError, "No context given for string object." @@ -3009,53 +3387,74 @@ class ChanList(list): if self.context and item.context != self.context: raise ValueError, "Channel object does not belong to context." list.insert(self, index, item) + if self._dict is not None: + if self.context: + self._dict[self.context.lower(item.name)] = item + else: + self._dict[item.context, item.context.lower(item.name)] = 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) + list.extend(self, ChanList(iterable, context=self.context)) 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) + 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) + 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) + 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) + 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) + self.context.send(u"PRIVMSG %s :%s" % (self, msg), origin=origin) def __str__(self): return ",".join([channel.name for channel in self]) + def __getitem__(self, key): + if type(key) in (int, long): + return list.__getitem__(self, key) + else: + if self._dict is not None: + if self.context == None: + raise ValueError, "No context given for string object." + keylower = self.context.lower(key) + return self._dict[keylower] + else: + raise ValueError, "No dict available." + + def __delitem__(self, key): + if type(key) in (int, long): + channel = self[key] + del self._dict[self.context.lower(channel.name)] + list.__delitem__(self, channel) + else: + if self._dict is not None: + if self.context == None: + raise ValueError, "No context given for string object." + keylower = self.context.lower(key) + list.__delitem__(self, self._dict[keylower]) + del self._dict[keylower] + else: + raise ValueError, "No dict available." + class UserList(list): + __doc__ = "Subclass of list, with builtin validation." - def __init__(self, iterable=None, context=None): + def __init__(self, iterable=None, context=None, withdict=False): + self._dict = {} if withdict else None if context != None and type(context) != Connection: raise TypeError, "context must be irc.Connection object or None" self.context = context @@ -3063,19 +3462,35 @@ class UserList(list): userlist = [] for user in iterable: if type(user) == User: + if context and user.context != context: + raise ValueError, "User object does not belong to context." 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) + if self._dict is not None: + if self.context: + self._dict.update( + {self.context.lower(user.nick): user for user in userlist}) + else: + self._dict.update( + {(user.context, user.context.lower(user.nick)): user for user in userlist}) else: list.__init__(self) def append(self, item): + """append(item) + + Like list.append, but enforces that the appended item must be a User instance. + If item is a string, then a User instance will be appended in its place.""" if type(item) in (str, unicode): if self.context: - list.append(self, self.context.user(item)) + user = self.context.user(item) + list.append(self, user) + if self._dict is not None: + self._dict[self.context.lower(item)] = user return else: raise ValueError, "No context given for string object." @@ -3084,11 +3499,22 @@ class UserList(list): if self.context and item.context != self.context: raise ValueError, "User object does not belong to context." list.append(self, item) + if self._dict is not None: + if self.context: + self._dict[self.context.lower(item.nick)] = item + else: + self._dict[item.context, item.context.lower(item.nick)] = item def insert(self, index, item): + """insert(index, item) + + Like list.insert.""" if type(item) in (str, unicode): if self.context: - list.insert(self, index, self.context.user(item)) + user = self.context.user(item) + list.insert(self, index, user) + if self._dict is not None: + self._dict[self.context.lower(item)] = user return else: raise ValueError, "No context given for string object." @@ -3097,31 +3523,66 @@ class UserList(list): if self.context and item.context != self.context: raise ValueError, "User object does not belong to context." list.insert(self, index, item) + if self._dict is not None: + if self.context: + self._dict[self.context.lower(item.nick)] = item + else: + self._dict[item.context, item.context.lower(item.nick)] = 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) + """extend(iterable) + + Like list.extend.""" + list.extend(self, UserList(iterable, context=self.context)) def msg(self, msg, origin=None): + """msg(msg[, origin]) + + Sends a PRIVMSG to all users on list.""" if not self.context: raise ValueError, "No context defined." - self.context._send(u"PRIVMSG %s :%s" % (self, msg), origin=origin) + self.context.send(u"PRIVMSG %s :%s" % (self, msg), origin=origin) def __str__(self): return ",".join([user.nick for user in self]) + def __getitem__(self, index): + if type(index) in (int, long): + return list.__getitem__(self, index) + else: + if self._dict is not None: + if self.context == None: + raise ValueError, "No context given for string object." + return self._dict[self.context.lower(index)] + else: + raise ValueError, "No dict available." + + def __delitem__(self, index): + if type(index) in (int, long): + user = self[index] + del self._dict[self.context.lower(user.name)] + list.__delitem__(self, user) + else: + if self._dict is not None: + if self.context == None: + raise ValueError, "No context given for string object." + index = self.context.lower(index) + list.__delitem__(self, self._dict[index]) + del self._dict[index] + else: + raise ValueError, "No dict available." + + def remove(self, item): + if type(item) == User: + list.remove(self, item) + if self._dict is not None: + if self.context: + del self._dict[self.context.lower(item.nick)] + else: + del self._dict[item.context, item.context.lower(item.nick)] + else: + self.remove(self[item]) + class Server(object): @@ -3138,3 +3599,90 @@ class Server(object): self.motdgreet = None self.motd = [] self.motdend = None + + def stats(self, query, origin=None): + self.context(query, self, origin=origin) + + def __repr__(self): + return u"<Server: {self.name} on {self.context:uri}>".format(**vars()) + + def __str__(self): + return self.name + + +class ServerList(list): + __doc__ = "Subclass of list, with builtin validation." + + 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: + serverlist = [] + for server in iterable: + if type(server) == Server: + if self.context and server.context != self.context: + raise ValueError, "Server object does not belong to context." + serverlist.append(server) + elif type(server) in (str, unicode): + if context == None: + raise ValueError, "No context given for string object." + serverlist.append(context.getserver(server)) + list.__init__(self, serverlist) + else: + list.__init__(self) + + def append(self, item): + """append(item) + + Like list.append, but enforces that the appended item must be a Server instance. + If item is a string, then a Server instance will be appended in its place.""" + if type(item) in (str, unicode): + if self.context: + list.append(self, self.context.getserver(item)) + return + else: + raise ValueError, "No context given for string object." + if type(item) != Server: + raise TypeError, "Only Server objects are permitted in list" + if self.context and item.context != self.context: + raise ValueError, "Server object does not belong to context." + list.append(self, item) + + def insert(self, index, item): + """insert(index, item) + + Like list.insert.""" + if type(item) in (str, unicode): + if self.context: + list.insert(self, index, self.context.getserver(item)) + return + else: + raise ValueError, "No context given for string object." + if type(item) != Server: + raise TypeError, "Only Server objects are permitted in list" + if self.context and item.context != self.context: + raise ValueError, "Server object does not belong to context." + list.insert(self, index, item) + + def extend(self, iterable): + """extend(iterable) + + Like list.extend.""" + serverlist = [] + for item in iterable: + if type(item) in (str, unicode): + if self.context: + serverlist.append(self.context.getserver(item)) + return + else: + raise ValueError, "No context given for string object." + if type(item) != User: + raise TypeError, "Only Server objects are permitted in list" + if self.context and item.context != self.context: + raise ValueError, "Server object does not belong to context." + serverlist.append(item) + list.extend(self, serverlist) + + def __str__(self): + return ",".join([user.nick for user in self]) |