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 | |
parent | 0b695d73168af6a8444e1c0a0f4e6b0f85506fc4 (diff) |
Adding CAP support, ircapp.py with sample config, speeding up channel and user cache lookup.
-rw-r--r-- | autoexec.py | 7 | ||||
-rw-r--r-- | bouncer.py | 90 | ||||
-rw-r--r-- | cannon.py | 8 | ||||
-rw-r--r-- | irc.py | 1298 | ||||
-rw-r--r-- | ircapp.conf | 64 | ||||
-rwxr-xr-x | ircapp.py | 125 | ||||
-rw-r--r-- | logger.py | 55 | ||||
-rw-r--r-- | modjson.py | 96 | ||||
-rwxr-xr-x | startirc.py | 74 | ||||
-rw-r--r-- | wallet.py | 26 |
10 files changed, 1264 insertions, 579 deletions
diff --git a/autoexec.py b/autoexec.py index 501ec2c..0b30894 100644 --- a/autoexec.py +++ b/autoexec.py @@ -19,15 +19,14 @@ class Autoexec(object): self._rejoinchannels = {} # Saved channels for when a connection is lost - def onAddonAdd(self, context, label, onconnect=None, onregister=None, autojoin=None, usermodes=None, nsautojoin=None, nsmatch=None, wallet=None, opername=None, opermodes=None, snomasks=None, operexec=None, operjoin=None, autorejoin=True): + def onAddonAdd(self, context, label, onconnect=[], onregister=[], autojoin=[], usermodes=None, nsautojoin=[], nsmatch=None, wallet=None, opername=None, opermodes=None, snomasks=None, operexec=None, operjoin=[], autorejoin=True): labels = [v.label for v in self.networks.values()] if label in labels: raise BaseException, "Label already exists" if context in self.networks.keys(): raise BaseException, "Network already exists" self.networks[context] = irc.Config( - self, label=label, onconnect=onconnect, onregister=onregister, autojoin=irc.ChanList( - autojoin, context=context), + self, label=label, onconnect=list(onconnect), onregister=list(onregister), autojoin=irc.ChanList(autojoin, context=context), usermodes=usermodes, nsautojoin=irc.ChanList(nsautojoin, context=context), nsmatch=nsmatch, wallet=wallet, opername=opername, opermodes=opermodes, snomasks=snomasks, operexec=operexec, operjoin=irc.ChanList(operjoin, context=context), autorejoin=autorejoin) self._rejoinchannels[context] = None @@ -35,7 +34,7 @@ class Autoexec(object): def onDisconnect(self, context, expected): conf = self.networks[context] - if conf.autorejoin and not expected and context.identity: + if conf.autorejoin and not expected and context.identity and context.identity.channels: self._rejoinchannels[context] = irc.ChanList( context.identity.channels, context=context) # Store a *copy* of the list of channels @@ -14,6 +14,10 @@ from threading import Thread from threading import RLock as Lock import Queue import chardet +import modjson + +dec = modjson.ModJSONDecoder() +enc = modjson.ModJSONEncoder(indent=3) # TODO: Rewrite this *entire* module and make more efficient. @@ -26,21 +30,14 @@ _listnumerics = dict(b=(367, 368, "channel ban list"), def BouncerReload(BNC): + networks, configs = zip(*BNC.conf.items()) + json = enc.encode([BNC, configs]) if BNC.isAlive(): BNC.stop() - if BNC.__version__ == "1.3": - newBNC = Bouncer( - addr=BNC.addr, port=BNC.port, secure=BNC.ssl, ipv6=BNC.ipv6, - certfile=BNC.certfile, keyfile=BNC.keyfile, timeout=BNC.timeout, autoaway=BNC.autoaway) - for label, (context, passwd, hashtype) in BNC.servers.items(): - context.rmAddon(BNC) - context.addAddon( - newBNC, label=label, passwd=passwd, hashtype=hashtype) - else: - newBNC = Bouncer(**BNC.__options__) - for context, conf in BNC.conf.items(): - context.rmAddon(BNC) - context.addAddon(newBNC, **conf.__dict__) + newBNC, newconfs = dec.decode(json) + for network, newconf in zip(networks, newconfs): + network.rmAddon(BNC) + network.addAddon(**newconf) return newBNC @@ -56,7 +53,7 @@ class Bouncer (Thread): self.conf = {} self.passwd = {} self.socket = None - self.ssl = ssl + self.secure = secure self.ipv6 = ipv6 self.certfile = certfile self.keyfile = keyfile @@ -119,7 +116,7 @@ class Bouncer (Thread): Thread.__init__(self) self.daemon = True - def onAddonAdd(self, context, label, passwd=None, hashtype="sha512", ignore=None, autoaway=None, translations=None, hidden=None): + def onAddonAdd(self, context, label, passwd=None, hashtype="sha512", ignore=None, autoaway=None, translations=[], hidden=[]): for (context2, conf2) in self.conf.items(): if context == context2: raise ValueError, "Context already exists in config." @@ -132,9 +129,8 @@ class Bouncer (Thread): break print "Passwords do not match!" passwd = hashlib.new(hashtype, passwd).hexdigest() - conf = irc.Config( - self, label=label, passwd=passwd, hashtype=hashtype, ignore=ignore, autoaway=autoaway, - translations={} if translations == None else translations, hidden=irc.ChanList(hidden, context=context)) + conf = irc.Config(self, label=label, passwd=passwd, hashtype=hashtype, ignore=ignore, autoaway=autoaway, translations=[ + (key if type(key) == irc.Channel else context[key], value) for key, value in translations], hidden=irc.ChanList(hidden, context=context)) self.conf[context] = conf self._whoexpected[context] = [] if self.debug: @@ -262,11 +258,11 @@ class Bouncer (Thread): def onWhoEntry(self, context, origin, channel, user, channame, username, host, serv, nick, flags, hops, realname): # Called when a WHO list is received. - conf = self.conf[context] - if len(self._whoexpected[context]) and self._whoexpected[context][0] in self.clients: + if len(self._whoexpected[context]): client = self._whoexpected[context][0] - client.send(origin=origin, cmd=352, target=context.identity, params=u"{channame} {username} {host} {serv} {nick} {flags}".format( - **vars()), extinfo=u"{hops} {realname}".format(**vars())) + if client in self.clients: + client.send(origin=origin, cmd=352, target=context.identity, params=u"{channame} {username} {host} {serv} {nick} {flags}".format( + **vars()), extinfo=u"{hops} {realname}".format(**vars())) # client.send(":%s 352 %s %s %s %s %s %s %s :%s %s\n"%(origin, context.identity.nick, channame, username, host, serv, nick, flags, hops, realname)) def onWhoEnd(self, context, origin, param, endmsg): @@ -301,15 +297,10 @@ class Bouncer (Thread): def onListEntry(self, context, origin, channel, population, extinfo): # Called when a WHO list is received. - conf = self.conf[context] - if channel in conf.translations.keys(): - channame = conf.translations[channel] - else: - channame = channel.name if len(self._listexpected[context]) and self._listexpected[context][0] in self.clients: client = self._listexpected[context][0] client.send(origin=origin, cmd=322, target=context.identity, - params=u"{channame} {population}".format(**vars()), extinfo=extinfo) + params=u"{channel.name} {population}".format(**vars()), extinfo=extinfo) # client.send(":%s 322 %s %s %d :%s\n"%(origin, context.identity.nick, channame, population, extinfo)) def onListEnd(self, context, origin, endmsg): @@ -424,7 +415,7 @@ class Bouncer (Thread): self.broadcast(context, origin=user, cmd="JOIN", target=channel, clients=[ client for client in self.clients if channel not in client.hidden]) - def onUnhandled(self, context, line, origin, cmd, target, params, extinfo, targetprefix): + def onOther(self, context, line, origin, cmd, target, params, extinfo, targetprefix): conf = self.conf[context] self.broadcast( context, origin=origin, cmd=cmd, target=target, params=params, extinfo=extinfo, @@ -482,26 +473,26 @@ class BouncerConnection (Thread): if type(target) == irc.Channel: if targetprefix == None: targetprefix = "" - if target in self.translations.keys(): - target = targetprefix + self.translations[target] - else: - target = targetprefix + target.name + # if target in self.translations.keys(): + # target=targetprefix+self.translations[target] + # else: + # target=targetprefix+target.name + target = targetprefix + target.name elif type(target) == irc.User: target = target.nick if type(cmd) == int: cmd = "%03d" % cmd - translated = [] - if params: - for param in params.split(" "): - chantypes = self.context.supports.get( - "CHANTYPES", irc._defaultchantypes) - if re.match(irc._chanmatch % re.escape(chantypes), param) and self.context[param] in self.translations.keys(): - translated.append(self.translations[self.context[param]]) - else: - translated.append(param) - params = " ".join(translated) + # translated=[] + # if params: + # for param in params.split(" "): + #chantypes=self.context.supports.get("CHANTYPES", irc._defaultchantypes) + # if re.match(irc._chanmatch % re.escape(chantypes), param) and self.context[param] in self.translations.keys(): + # translated.append(self.translations[self.context[param]]) + # else: + # translated.append(param) + #params=" ".join(translated) if params: line = u"{cmd} {target} {params}".format(**vars()) @@ -841,7 +832,7 @@ class BouncerConnection (Thread): self.context.logwrite( "*** [BouncerConnection] Incoming connection from %s to %s denied: Invalid password." % (self.host, self.context)) self.bouncer.broadcast( - self.context, origin=self.bouncer.servname, cmd="NOTICE", target=client.context.identity, + self.context, origin=self.bouncer.servname, cmd="NOTICE", target=self.context.identity, extinfo="Incoming connection from %s to %s denied: Invalid password." % (self.host, self.context)) # for client in self.bouncer.clients: # if client.context!=self.context: @@ -899,7 +890,8 @@ class BouncerConnection (Thread): else: chantypes = self.context.supports.get( "CHANTYPES", irc._defaultchantypes) - if cmd.upper() not in ("SETTRANSLATE", "RMTRANSLATE"): + # Disable translating for now. + if False and cmd.upper() not in ("SETTRANSLATE", "RMTRANSLATE"): translated = [] for targ in target.split(","): translatefound = False @@ -1104,7 +1096,7 @@ class BouncerConnection (Thread): def cmdSHOW(self, line, target, params, extinfo): chantypes = self.context.supports.get( "CHANTYPES", irc._defaultchantypes) - with self.lock: + with self.context.lock, self.lock: for channame in target.split(): if re.match(irc._chanmatch % re.escape(chantypes), channame): channel = self.context[channame] @@ -1128,7 +1120,7 @@ class BouncerConnection (Thread): def cmdHIDE(self, line, target, params, extinfo): chantypes = self.context.supports.get( "CHANTYPES", irc._defaultchantypes) - with self.lock: + with self.context.lock, self.lock: for channame in target.split(): if re.match(irc._chanmatch % re.escape(chantypes), channame): channel = self.context[channame] @@ -1153,7 +1145,7 @@ class BouncerConnection (Thread): def cmdSETTRANSLATE(self, line, target, params, extinfo): chantypes = self.context.supports.get( "CHANTYPES", irc._defaultchantypes) - with self.lock: + with self.context.lock, self.lock: if re.match(irc._chanmatch % re.escape(chantypes), target) and re.match(irc._chanmatch % re.escape(chantypes), target): channel = self.context[target] if self.context.supports.get("CASEMAPPING", "rfc1459") == "ascii": @@ -1177,7 +1169,7 @@ class BouncerConnection (Thread): def cmdRMTRANSLATE(self, line, target, params, extinfo): chantypes = self.context.supports.get( "CHANTYPES", irc._defaultchantypes) - with self.lock: + with self.context.lock, self.lock: if re.match(irc._chanmatch % re.escape(chantypes), target): channel = self.context[target] if channel not in self.translations.keys(): @@ -9,12 +9,12 @@ class Cannon(object): def __init__(self): self.firecount = {} - def onChanMsg(self, IRC, user, channel, targetprefix, msg): + def onChanMsg(self, context, user, channel, targetprefix, msg): matches = re.findall("^!fire\\s+(.*)$", msg) if matches: nickname = matches[0] if any([nickname.lower() == usr.nick.lower() for usr in channel.users]): - vic = IRC.user(nickname) + vic = context.user(nickname) if vic in self.firecount.keys(): count = self.firecount[vic] + 1 else: @@ -37,5 +37,5 @@ class Cannon(object): "%s: I cannot fire %s out of a cannon, as he or she is not here." % (user.nick, nickname)) - def onSendChanMsg(self, IRC, origin, channel, targetprefix, msg): - self.onChanMsg(IRC, IRC.identity, channel, targetprefix, msg) + def onSendChanMsg(self, context, origin, channel, targetprefix, msg): + self.onChanMsg(context, context.identity, channel, targetprefix, msg) @@ -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]) diff --git a/ircapp.conf b/ircapp.conf new file mode 100644 index 0000000..72fe9a2 --- /dev/null +++ b/ircapp.conf @@ -0,0 +1,64 @@ +{ + "addons": { + "ax": { + "class": "autoexec.Autoexec" + }, + "log": { + "class": "logger.Logger", + "logroot": "~/pyIRC-logs" + } + }, + "networks": { + "Freenode": { + "class": "irc.Connection", + "server": "irc.freenode.net", + "secure": true, + "nick": "pyIRC-user", + "requestcaps": [ + "away-notify", + "multi-prefix", + "userhost-in-names", + "account-notify" + ], + "addons": [ + { + "addon": <addons.log>, + "label": "Freenode" + }, + { + "addon": <addons.ax>, + "label": "Freenode", + "autojoin": [ + "#pyirc-ng" + ] + } + ] + }, + "InsomniaIRC": { + "class": "irc.Connection", + "server": "irc.insomniairc.net", + "secure": true, + "nick": "pyIRC-user", + "requestcaps": [ + "extended-join", + "away-notify", + "multi-prefix", + "userhost-in-names", + "account-notify" + ], + "addons": [ + { + "addon": <addons.log>, + "label": "InsomniaIRC" + }, + { + "addon": <addons.ax>, + "label": "InsomniaIRC", + "autojoin": [ + "#chat" + ] + } + ] + } + } +} diff --git a/ircapp.py b/ircapp.py new file mode 100755 index 0000000..3bc444f --- /dev/null +++ b/ircapp.py @@ -0,0 +1,125 @@ +#!/usr/bin/python +import os +import re +import time +import signal +import sys +import irc +import modjson +import readline +import rlcompleter +import types +import code + +nonaddontypes = (types.ModuleType, types.MethodType, + types.FunctionType, types.TypeType, irc.Connection) + + +class IRCApplication: + + def __init__(self, conffile=None): + self._quitting = False + self.conffile = conffile + self.termcaught = False + self.confdecoder = modjson.ModJSONDecoder() + self.confencoder = modjson.ModJSONEncoder(indent=3) + signal.signal(signal.SIGTERM, self.sigterm) + self.namespace = {} + if conffile and os.path.isfile(conffile): + with open(conffile, "r") as f: + pyirc = self.confdecoder.decode(f.read()) + if "addons" in pyirc.keys(): + self.namespace.update(pyirc["addons"]) + if "networks" in pyirc.keys(): + self.namespace.update(pyirc["networks"]) + self.shell = code.InteractiveConsole(locals=self.namespace) + self.namespace["quit"] = self.quit + self.namespace["save"] = self.save + # self.namespace["exit"]=self.exit + self.namespace["irc"] = irc + + def quit(self, quitmsg="Goodbye!"): + networks = [ + o for o in self.namespace.values() if type(o) == irc.Connection] + for context in networks: + if type(context) == irc.Connection and context.isAlive(): + context.quit(quitmsg) + for context in networks: + if type(context) == irc.Connection: + with context._disconnecting: + while context.connected: + context._disconnecting.wait(30) + if context._recvhandlerthread: + context._recvhandlerthread.join() + if context._sendhandlerthread: + context._sendhandlerthread.join() + + def complete(self, text, state): + raise NotImplemented + + def start(self): + sys.ps1 = "(ircapp) " + sys.ps2 = "........ " + readline.parse_and_bind("tab: complete") + completer = rlcompleter.Completer(self.namespace) + readline.set_completer(completer.complete) + for o in self.namespace.values(): + if type(o) == irc.Connection: + o.connect() + while True: + try: + self.shell.interact(banner="Welcome to pyIRC!") + except SystemExit, quitmsg: + if not self._quitting: + if quitmsg.message: + self.quit(quitmsg.message) + else: + self.quit() + break + # In case CTRL+D is accidentally sent to the console. + print "Ooops... Did you mean to do that?" + + def sigterm(self, signum, frame): + if not self.termcaught: + self.termcaught = True + self.exit("Caught SIGTERM") + + def save(self, conffile=None): + addons = {key: o for (key, o) in self.namespace.items() + if not isinstance(o, nonaddontypes) and not key.startswith("_")} + extraaddons = [] + networks = {key: o for (key, o) in self.namespace.items() if type( + o) == irc.Connection and not key.startswith("_")} + if not conffile: + conffile = self.conffile + with open(conffile, "w") as f: + print >>f, self.confencoder.encode( + dict(addons=addons, networks=networks)) + + def exit(self, quitmsg="Goodbye!"): + self.quit(quitmsg) + addons = [o for (key, o) in self.namespace.items() + if not isinstance(o, nonaddontypes) and not key.startswith("_")] + networks = [o for (key, o) in self.namespace.items() if type( + o) == irc.Connection and not key.startswith("_")] + for context in networks: + for conf in list(context.addons): + addon = conf.addon if type(conf) == irc.Config else conf + context.rmAddon(addon) + if addon not in addons: + addons.append(addon) + for addon in addons: + if "stop" in dir(addon) and callable(addon.stop) and "isAlive" in dir(addon) and callable(addon.isAlive) and addon.isAlive(): + try: + addon.stop() + except: + pass + print "Quit: {quitmsg}".format(**vars()) + self._quitting = True + sys.exit() + +if __name__ == "__main__": + ircapp = IRCApplication( + sys.argv[1] if len(sys.argv) > 1 else "ircapp.conf") + ircapp.start() + ircapp.exit() @@ -24,9 +24,9 @@ def LoggerReload(log): with newlog.rotatelock, log.rotatelock: newlog.logs = log.logs log.logs = {} - for context, label in log.labels.items(): + for context, conf in log.conf.items(): context.rmAddon(log) - context.addAddon(newlog, label=label) + context.addAddon(newlog, label=conf.label) return newlog @@ -34,7 +34,7 @@ class Logger(Thread): def __init__(self, logroot): self.logroot = logroot - path = [logroot] + path = [os.path.expanduser(logroot)] while not os.path.isdir(path[0]): split = os.path.split(path[0]) @@ -48,7 +48,7 @@ class Logger(Thread): os.mkdir(path[0]) self.logs = {} - self.labels = {} + self.conf = {} self.rotatelock = Lock() Thread.__init__(self) @@ -66,7 +66,9 @@ class Logger(Thread): if all([not log or log.closed for log in self.logs.values()]): break Y, M, D, h, m, s, w, d, dst = now = time.localtime() - for context in self.labels.keys(): + + logroot = os.path.expanduser(self.logroot) + for context in self.conf.keys(): if context.connected: with context.lock: try: @@ -92,20 +94,27 @@ class Logger(Thread): context.logwrite(*["!!! [Logger] Exception in module %(module)s" % vars()] + [ "!!! [Logger] %s" % tbline for tbline in traceback.format_exc().split("\n")]) context.logopen( - os.path.join(self.logroot, self.labels[context], "rawdata-%04d.%02d.%02d.log" % now[:3])) + os.path.join(logroot, self.conf[context].label, "rawdata-%04d.%02d.%02d.log" % now[:3])) nextrotate = int(time.mktime((Y, M, D + 1, 0, 0, 0, 0, 0, -1))) finally: Thread.__init__(self) self.daemon = True def onAddonAdd(self, context, label): - if label in self.labels.values(): - raise BaseException, "Label already exists" - if context in self.labels.keys(): - raise BaseException, "Network already exists" - if not os.path.isdir(os.path.join(self.logroot, label)): - os.mkdir(os.path.join(self.logroot, label)) - self.labels[context] = label + logroot = os.path.expanduser(self.logroot) + + for (context2, conf2) in self.conf.items(): + if context == context2: + raise ValueError, "Context already exists in config." + if label == conf2.label: + raise ValueError, "Unique label required." + + conf = irc.Config(self, label=label) + + contextroot = os.path.join(logroot, label) + if not os.path.isdir(contextroot): + os.mkdir(contextroot) + if context.connected: self.openLog(context) if context.identity: @@ -115,7 +124,9 @@ class Logger(Thread): timestamp = reduce(lambda x, y: x + ":" + y, [ str(t).rjust(2, "0") for t in now[0:6]]) context.logopen( - os.path.join(self.logroot, self.labels[context], "rawdata-%04d.%02d.%02d.log" % now[:3])) + os.path.join(contextroot, "rawdata-%04d.%02d.%02d.log" % now[:3])) + self.conf[context] = conf + return conf def onAddonRem(self, context): if context.connected: @@ -129,9 +140,11 @@ class Logger(Thread): self.closeLog(user) if context in self.logs.keys() and not self.logs[context].closed: self.closeLog(context) - del self.labels[context] + del self.conf[context] def openLog(self, window): + logroot = os.path.expanduser(self.logroot) + with self.rotatelock: if not self.isAlive(): self.start() @@ -145,12 +158,12 @@ class Logger(Thread): str(t).rjust(2, "0") for t in now[0:6]]) if type(window) == irc.Connection: log = self.logs[window] = codecs.open( - os.path.join(self.logroot, self.labels[window], "console-%04d.%02d.%02d.log" % now[:3]), "a", encoding="utf8") + os.path.join(logroot, self.conf[window].label, "console-%04d.%02d.%02d.log" % now[:3]), "a", encoding="utf8") print >>log, "%s ### Log file opened" % (irc.timestamp()) elif type(window) == irc.Channel: - label = self.labels[window.context] - log = self.logs[window] = codecs.open(os.path.join(self.logroot, label, "channel-%s-%04d.%02d.%02d.log" % ( + label = self.conf[window.context].label + log = self.logs[window] = codecs.open(os.path.join(logroot, label, "channel-%s-%04d.%02d.%02d.log" % ( (urllib2.quote(window.name.lower().decode("utf8")).replace("/", "%2f"),) + now[:3])), "a", encoding="utf8") print >>log, "%s ### Log file opened" % (irc.timestamp()) self.logs[window].flush() @@ -169,8 +182,10 @@ class Logger(Thread): irc.timestamp(), window.fmtchancreated()) if type(window) == irc.User: - logname = os.path.join(self.logroot, self.labels[window.context], "query-%s-%04d.%02d.%02d.log" % ( - (urllib2.quote(window.nick.lower()).replace("/", "%2f"),) + now[:3])) + label = self.conf[window.context].label + logname = os.path.join( + logroot, label, "query-%s-%04d.%02d.%02d.log" % + ((urllib2.quote(window.nick.lower()).replace("/", "%2f"),) + now[:3])) for (other, log) in self.logs.items(): if other == window: continue @@ -351,7 +351,7 @@ class ModJSONDecoder(json.JSONDecoder): return obj, end -class JSONEncoder(object): +class ModJSONEncoder(object): """Extensible JSON <http://json.org> encoder for Python data structures. @@ -478,7 +478,49 @@ class JSONEncoder(object): return JSONEncoder.default(self, o) """ - if path: + if "json" in dir(o) and callable(o.json): + conf = o.json() + + else: + conf = collections.OrderedDict() + conf["class"] = "{o.__class__.__module__}.{o.__class__.__name__}".format( + **vars()) + + if "__init__" in dir(o) and type(o.__init__) == new.instancemethod: + try: + arginspect = inspect.getargspec(o.__init__) + except: + raise TypeError(repr(o) + " is not JSON serializable") + + if arginspect.defaults: + requiredargs = arginspect.args[ + 1:len(arginspect.args) - len(arginspect.defaults)] + argswithdefaults = arginspect.args[ + len(arginspect.args) - len(arginspect.defaults):] + defaultvalues = arginspect.defaults + else: + requiredargs = arginspect.args[1:] + argswithdefaults = [] + defaultvalues = [] + + for key in requiredargs: + try: + conf[key] = getattr(o, key) + except AttributeError: + print key + print refs.keys() + raise TypeError( + repr(o) + " is not JSON serializable (Cannot recover required argument '%s')" % key) + + for key, default in zip(argswithdefaults, defaultvalues): + try: + value = getattr(o, key) + if value != default: + conf[key] = getattr(o, key) + except AttributeError: + pass + + if path and not isinstance(conf, (int, long, bool, basestring)) and conf is not None: pathstr = str(path[0]) numindices = [] for index in path[1:]: @@ -494,45 +536,7 @@ class JSONEncoder(object): numindices = [] if pathstr not in refs.keys(): refs[pathstr] = o - if "json" in dir(o) and callable(o.json): - return o.json() - - conf = collections.OrderedDict() - conf["class"] = "{o.__class__.__module__}.{o.__class__.__name__}".format( - **vars()) - - if "__init__" in dir(o) and type(o.__init__) == new.instancemethod: - try: - arginspect = inspect.getargspec(o.__init__) - except: - raise TypeError(repr(o) + " is not JSON serializable") - - if arginspect.defaults: - requiredargs = arginspect.args[ - 1:len(arginspect.args) - len(arginspect.defaults)] - argswithdefaults = arginspect.args[ - len(arginspect.args) - len(arginspect.defaults):] - defaultvalues = arginspect.defaults - else: - requiredargs = arginspect.args[1:] - argswithdefaults = [] - defaultvalues = [] - for key in requiredargs: - try: - conf[key] = getattr(o, key) - except AttributeError: - print key - raise TypeError( - repr(o) + " is not JSON serializable (Cannot recover required argument '%s')" % key) - - for key, default in zip(argswithdefaults, defaultvalues): - try: - value = getattr(o, key) - if value != default: - conf[key] = getattr(o, key) - except AttributeError: - pass return conf def encode(self, o): @@ -630,10 +634,10 @@ class JSONEncoder(object): o, _current_indent_level, markers, refs, path) if ref: yield ref - elif isinstance(o, (list, tuple)): + elif isinstance(o, (list, tuple)) and "json" not in dir(o): for chunk in self._iterencode_list(o, _current_indent_level, markers, refs, path): yield chunk - elif isinstance(o, dict): + elif isinstance(o, dict) and "json" not in dir(o): for chunk in self._iterencode_dict(o, _current_indent_level, markers, refs, path): yield chunk else: @@ -714,10 +718,10 @@ class JSONEncoder(object): yield buf + ref else: yield buf - if isinstance(value, (list, tuple)): + if isinstance(value, (list, tuple)) and "json" not in dir(value): chunks = self._iterencode_list( value, _current_indent_level, markers, refs, path + (k,)) - elif isinstance(value, dict): + elif isinstance(value, dict) and "json" not in dir(value): chunks = self._iterencode_dict( value, _current_indent_level, markers, refs, path + (k,)) else: @@ -816,10 +820,10 @@ class JSONEncoder(object): if ref: yield ref else: - if isinstance(value, (list, tuple)): + if isinstance(value, (list, tuple)) and "json" not in dir(value): chunks = self._iterencode_list( value, _current_indent_level, markers, refs, path + (key,)) - elif isinstance(value, dict): + elif isinstance(value, dict) and "json" not in dir(value): chunks = self._iterencode_dict( value, _current_indent_level, markers, refs, path + (key,)) else: diff --git a/startirc.py b/startirc.py deleted file mode 100755 index 111d9d0..0000000 --- a/startirc.py +++ /dev/null @@ -1,74 +0,0 @@ -#!/usr/bin/python -i -import os -import re -import time -import logger -import signal -import figlet -import cannon -import wallet -import autoexec -import sys -import irc -import bouncer -import readline -import rlcompleter -readline.parse_and_bind("tab: complete") - -networks = {} - - -def quit(quitmsg="Goodbye!"): - global networks - addons = [] - for IRC in networks.values(): - if IRC.isAlive(): - IRC.quit(quitmsg) - while any([IRC.isAlive() for IRC in networks.values()]): - time.sleep(0.25) - for IRC in networks.values(): - for addon in list(IRC.addons): - IRC.rmAddon(addon) - if addon not in addons: - addons.append(addon) - for addon in addons: - if "stop" in dir(addon) and callable(addon.stop) and "isAlive" in dir(addon) and callable(addon.isAlive) and addon.isAlive(): - try: - addon.stop() - except: - pass - print "Goodbye!" - sys.exit() - -termcaught = False - - -def sigterm(signum, frame): - global termcaught - if not termcaught: - termcaught = True - quit("Caught SIGTERM") - -signal.signal(signal.SIGTERM, sigterm) - -logroot = os.path.join(os.environ["HOME"], "IRC") - -InsomniaIRC = networks["InsomniaIRC"] = irc.Connection( - server="irc.insomniairc.net", nick="pyIRC", secure=True) - -ax = autoexec.Autoexec() -log = logger.Logger(logroot) - -### Be sure to generate your own cert.pem and key.pem files! -BNC = bouncer.Bouncer( - "", 16698, secure=True, certfile="cert.pem", keyfile="key.pem", autoaway="I'm off to see the wizard!") - -for (label, IRC) in networks.items(): - IRC.addAddon(log, label=label) - ### The password is 'hunter2' - IRC.addAddon(BNC, label=label, passwd="6b97ed68d14eb3f1aa959ce5d49c7dc612e1eb1dafd73b1e705847483fd6a6c809f2ceb4e8df6ff9984c6298ff0285cace6614bf8daa9f0070101b6c89899e22", hashtype="sha512") - -InsomniaIRC.addAddon(ax, label="InsomniaIRC", autojoin=["#chat"]) - -for (label, IRC) in networks.items(): - IRC.start() @@ -4,27 +4,30 @@ import Crypto.Cipher.Blowfish import os import getpass from threading import Lock +from collections import OrderedDict class Wallet(dict): + def __init__(self, filename): + self.filename = filename self.lock = Lock() if os.path.isfile(filename): self.f = open(filename, "rb+") self.passwd = getpass.getpass() self.crypt = Crypto.Cipher.Blowfish.new(self.passwd) contents_encrypted = self.f.read() - contents = self.crypt.decrypt(contents_encrypted + - "\x00"*((-len(contents_encrypted))%8)) + contents = self.crypt.decrypt( + contents_encrypted + "\x00" * ((-len(contents_encrypted)) % 8)) if contents.startswith(self.passwd): self.update(dict(pickle.loads(contents[len(self.passwd):]))) else: self.f.close() - raise BaseException("Incorrect Password") + raise BaseException, "Incorrect Password" else: self.f = open(filename, "wb+") passwd = self.passwd = None - while passwd is None or passwd != self.passwd: + while passwd == None or passwd != self.passwd: passwd = getpass.getpass("Enter new password: ") self.passwd = getpass.getpass("Confirm new password: ") if passwd != self.passwd: @@ -33,13 +36,22 @@ class Wallet(dict): self.flush() def flush(self): - contents = self.passwd+pickle.dumps(self.items(), protocol=2) + contents = self.passwd + pickle.dumps(self.items(), protocol=2) self.lock.acquire() try: self.f.seek(0) - self.f.write(self.crypt.encrypt( - contents+"\x00"*((-len(contents))%8))) + self.f.write( + self.crypt.encrypt(contents + "\x00" * ((-len(contents)) % 8))) self.f.truncate() self.f.flush() finally: self.lock.release() + + def json(self): + return OrderedDict([("class", "wallet.Wallet"), ("filename", self.filename)]) + + def __repr__(self): + return "<Wallet: %s>" % self.filename + + def __str__(self): + return "<Wallet: %s>" % self.filename |