diff options
-rw-r--r-- | autoexec.py | 144 | ||||
-rw-r--r-- | bouncer.py | 1550 | ||||
-rw-r--r-- | cannon.py | 23 | ||||
-rw-r--r-- | figlet.py | 1 | ||||
-rw-r--r-- | irc.conf | 48 | ||||
-rw-r--r-- | irc.py | 4668 | ||||
-rw-r--r-- | ircapp.conf | 64 | ||||
-rwxr-xr-x | ircapp.py | 125 | ||||
-rw-r--r-- | logger.py | 594 | ||||
-rw-r--r-- | modjson.py | 839 | ||||
-rw-r--r-- | quote.py | 55 | ||||
-rw-r--r-- | sedbot.py | 73 | ||||
-rwxr-xr-x | startirc.py | 74 | ||||
-rw-r--r-- | testaddon.py | 142 | ||||
-rw-r--r-- | wallet.py | 26 |
15 files changed, 6127 insertions, 2299 deletions
diff --git a/autoexec.py b/autoexec.py index 978501e..0b30894 100644 --- a/autoexec.py +++ b/autoexec.py @@ -1,82 +1,102 @@ #!/usr/bin/python import re import irc +import fnmatch + + +def AutoexecReload(old_ax): + ax = Autoexec() + for (context, conf) in old_ax.networks.items(): + context.rmAddon(old_ax) + context.addAddon(ax, **conf.__dict__) + return ax class Autoexec(object): + def __init__(self): self.networks = {} + self._rejoinchannels = {} + # Saved channels for when a connection is lost - def onAddonAdd(self, IRC, label, onconnect=None, onregister=None, autojoin=None, usermodes=None, wallet=None, opername=None, opermodes=None, snomasks=None, operexec=None, operjoin=None): - labels = [v[0] for v in self.networks.values()] + 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 IRC in self.networks.keys(): - raise BaseException("Network already exists") - self.networks[IRC] = (label, onconnect, onregister, autojoin, usermodes, wallet, opername, opermodes, snomasks, operexec, operjoin) - - def onAddonRem(self, IRC): - del self.networks[IRC] + 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=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 + return self.networks[context] - def onConnect(self, IRC): - (label, onconnect, onregister, autojoin, usermodes, wallet, opername, opermodes, snomasks, operexec, operjoin) = self.networks[IRC] - if onconnect: - for line in onconnect: - IRC.raw(line, origin=self) + def onDisconnect(self, context, expected): + conf = self.networks[context] + 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 - def onRegistered(self, IRC): - (label, onconnect, onregister, autojoin, usermodes, wallet, opername, opermodes, snomasks, operexec, operjoin) = self.networks[IRC] - if onregister: - for line in onregister: - IRC.raw(line, origin=self) - if usermodes: - IRC.raw("MODE %s %s"%(IRC.identity.nick, usermodes), origin=self) - if opername and wallet and "%s/opers/%s"%(label, opername) in wallet.keys(): - IRC.raw("OPER %s %s"%(opername, wallet[ - "%s/opers/%s"%(label, opername)]), origin=self) - if autojoin: - IRC.raw("JOIN %s"%(",".join(autojoin)), origin=self) + def onQuit(self, context, user, quitmsg): + if user == context.identity and not context._quitexpected: + # Bot received a QUIT message for itself, and was not expected. + self.onDisconnect(context, False) - def on381(self, IRC, line, origin, target, params, extinfo): - (label, onconnect, onregister, autojoin, usermodes, wallet, opername, opermodes, snomasks, operexec, operjoin) = self.networks[IRC] - if operexec: - for line in operexec: - IRC.raw(line, origin=self) - if opermodes: - IRC.raw("MODE %s %s"%(IRC.identity.nick, opermodes), origin=self) - if snomasks: - IRC.raw("MODE %s +s %s"%(IRC.identity.nick, snomasks), origin=self) - if operjoin: - IRC.raw("JOIN %s"%(",".join(operjoin)), origin=self) + def onAddonRem(self, context): + del self.networks[context], self._rejoinchannels[context] + def onConnect(self, context): + conf = self.networks[context] + if conf.onconnect: + for line in conf.onconnect: + context.raw(line, origin=self) -class NickServ(object): - def __init__(self): - self.networks = {} - - def onAddonAdd(self, IRC, label, wallet=None, autojoin=None): - labels = [v[0] for v in self.networks.values()] - #print labels - if label in labels: - raise BaseException("Label already exists") - if IRC in self.networks.keys(): - raise BaseException("Network already exists") - self.networks[IRC] = (label, wallet, autojoin) + def onRegistered(self, context): + conf = self.networks[context] + if conf.onregister: + for line in conf.onregister: + context.raw(line, origin=self) + if conf.usermodes: + context.raw("MODE %s %s" % + (context.identity.nick, conf.usermodes), origin=self) + if conf.opername and conf.wallet and "%s/opers/%s" % (conf.label, conf.opername) in conf.wallet.keys(): + context.raw("OPER %s %s" % + (conf.opername, conf.wallet["%s/opers/%s" % (conf.label, conf.opername)]), origin=self) + if conf.autojoin: + conf.autojoin.join(origin=self) + if conf.autorejoin and self._rejoinchannels[context]: + rejoin = irc.ChanList([channel for channel in self._rejoinchannels[ + context] if channel not in conf.autojoin + conf.nsautojoin + conf.operjoin], context=context) + if len(rejoin): + rejoin.join(origin=self) + self._rejoinchannels[context] = None - def onAddonRem(self, IRC): - del self.networks[IRC] + def on381(self, context, line, origin, target, params, extinfo): + conf = self.networks[context] + if conf.operexec: + for line in conf.operexec: + context.raw(line, origin=self) + if conf.opermodes: + context.raw("MODE %s %s" % + (context.identity.nick, conf.opermodes), origin=self) + if conf.snomasks: + context.raw("MODE %s +s %s" % + (context.identity.nick, conf.snomasks), origin=self) + if conf.operjoin: + conf.operjoin.join(origin=self) - def onPrivNotice(self, IRC, origin, msg): - label, wallet, autojoin = self.networks[IRC] + def onPrivNotice(self, context, origin, msg): + conf = self.networks[context] if type(origin) == irc.User and origin.nick.lower() == "nickserv": - if re.match("This nickname is registered( and protected)?", msg) and wallet and "%s/NickServ/%s"%(label, IRC.identity.nick.lower()) in wallet.keys(): - origin.msg("identify %s" % wallet["%s/NickServ/%s" % - (label, IRC.identity.nick.lower())]) + if re.match("This nickname is registered( and protected)?", msg) and (not conf.nsmatch or fnmatch.fnmatch("%s!%s@%s" % (origin.nick, origin.username, origin.host), conf.nsmatch)) and conf.wallet and "%s/NickServ/%s" % (conf.label, context.identity.nick.lower()) in conf.wallet.keys(): + origin.msg("identify %s" % + conf.wallet["%s/NickServ/%s" % (conf.label, context.identity.nick.lower())]) if re.match("You are now identified", msg): - if autojoin: - IRC.raw("JOIN %s"%(",".join(autojoin)), origin=self) + if conf.nsautojoin: + conf.nsautojoin.join(origin=self) - def on900(self, IRC, line, origin, target, params, extinfo): - label, wallet, autojoin = self.networks[IRC] - if autojoin: - IRC.raw("JOIN %s"%(",".join(autojoin)), origin=self) + def on900(self, context, line, origin, target, params, extinfo): + conf = self.networks[context] + if conf.nsautojoin: + conf.nsautojoin.join(origin=self) @@ -9,40 +9,67 @@ import string import hashlib import traceback import irc -from threading import Thread, Lock +import getpass +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. + +_listnumerics = dict(b=(367, 368, "channel ban list"), + e=(348, 349, "Channel Exception List"), + I=(346, 347, "Channel Invite Exception List"), + w=(910, 911, "Channel Access List"), + g=(941, 940, "chanel spamfilter list"), + X=(954, 953, "channel exemptchanops list")) + + +def BouncerReload(BNC): + networks, configs = zip(*BNC.conf.items()) + json = enc.encode([BNC, configs]) + if BNC.isAlive(): + BNC.stop() + newBNC, newconfs = dec.decode(json) + for network, newconf in zip(networks, newconfs): + network.rmAddon(BNC) + network.addAddon(**newconf) + return newBNC class Bouncer (Thread): - def __init__(self, addr="", port=16667, ssl=False, ipv6=False, certfile=None, keyfile=None, ignore=None, debug=False, log=sys.stderr, timeout=300, BNC=None, autoaway=None): - self.__name__ = "Bouncer for pyIRC" - self.__version__ = "1.1" - self.__author__ = "Brian Sherson" - self.__date__ = "December 1, 2013" + __name__ = "Bouncer for pyIRC" + __version__ = "2.0" + __author__ = "Brian Sherson" + __date__ = "February 21, 2014" + def __init__(self, addr="", port=16667, secure=False, ipv6=False, certfile=None, keyfile=None, ignore=None, debug=False, timeout=300, autoaway=None, servname="bouncer.site"): self.addr = addr self.port = port - self.servers = {} + self.conf = {} self.passwd = {} - self.socket = socket.socket( - socket.AF_INET6 if ipv6 else socket.AF_INET) - self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) - self.ssl = ssl + self.socket = None + self.secure = secure self.ipv6 = ipv6 self.certfile = certfile self.keyfile = keyfile - self.socket.bind((self.addr, self.port)) - self.connections = [] + self.clients = [] self.ignore = ignore self.debug = debug self.timeout = timeout self.autoaway = autoaway - - ### Keep track of what extensions/connections are requesting WHO, WHOIS, and LIST, because we don't want to spam every bouncer connection with the server's replies. - ### In the future, MAY implement this idea in the irc module. - self.whoexpected = {} - self.whoisexpected = {} - self.listexpected = {} + self.servname = servname + self._stopexpected = False + + # Keep track of what extensions/clients are requesting WHO, WHOIS, and LIST, because we don't want to spam every bouncer connection with the server's replies. + # In the future, MAY implement this idea in the irc module. + self._whoexpected = {} + self._whoisexpected = {} + self._listexpected = {} self.lock = Lock() self.starttime = int(time.time()) Thread.__init__(self) @@ -50,22 +77,33 @@ class Bouncer (Thread): self.start() def __repr__(self): - return "<Bouncer listening on port %(addr)s:%(port)s>" % vars(self) + h = hash(self) + return "<Bouncer listening on {self.addr}:{self.port} at 0x{h:x}0>".format(**vars()) def run(self): + self.socket = socket.socket( + socket.AF_INET6 if self.ipv6 else socket.AF_INET) + self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + self.socket.bind((self.addr, self.port)) self.socket.listen(5) #print ((self,"Now listening on port "+str(self.port))) while True: try: (connection, addr) = self.socket.accept() - if self.ssl: - connection = ssl.wrap_socket(connection, server_side=True, certfile=self.certfile, keyfile=self.keyfile, ssl_version=ssl.PROTOCOL_SSLv23) + if self.secure: + connection = ssl.wrap_socket( + connection, server_side=True, certfile=self.certfile, keyfile=self.keyfile, ssl_version=ssl.PROTOCOL_SSLv23) #print ((self,"New client connecting from %s:%s"%addr)) except socket.error: - #print "Shutting down Listener" + # print "Shutting down Listener" self.socket.close() - #raise + if not self._stopexpected: + raise sys.exit() + except: + tb = traceback.format_exc() + print >>sys.stderr, tb + continue connection.settimeout(self.timeout) bouncer = BouncerConnection( self, connection, addr, debug=self.debug) @@ -74,282 +112,333 @@ class Bouncer (Thread): self.socket.close() except: pass + self.socket = None + Thread.__init__(self) + self.daemon = True - def onAddonAdd(self, IRC, label, passwd, hashtype="md5"): - if IRC in [connection for (connection, passwd, hashtype) in self.servers.values()]: - return # Silently do nothing - if label in self.servers.keys(): - return - self.servers[label] = (IRC, passwd, hashtype) - self.whoexpected[IRC] = [] + 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." + if label == conf2.label: + raise ValueError, "Unique label required." + if passwd == None: + while True: + passwd = getpass.getpass("Enter new password: ") + if passwd == getpass.getpass("Confirm new password: "): + 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=[ + (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: - IRC.logwrite("dbg [Bouncer.onAddonAdd] Clearing WHO expected list." % vars()) - self.whoisexpected[IRC] = [] - self.listexpected[IRC] = [] - - def onAddonRem(self, IRC): - for bouncerconnection in self.connections: - if bouncerconnection.IRC == IRC: - bouncerconnection.quit(quitmsg="Bouncer extension removed") - for (label, (connection, passwd, hashtype)) in self.servers.items(): - if connection == IRC: - del self.servers[label] - - def stop(self): + context.logwrite( + "dbg [Bouncer.onAddonAdd] Clearing WHO expected list." % vars()) + self._whoisexpected[context] = [] + self._listexpected[context] = [] + return conf + + def onAddonRem(self, context): + for client in self.clients: + if client.context == context: + client.quit(quitmsg="Bouncer extension removed") + del self.conf[context] + del self._whoexpected[context], self._whoisexpected[ + context], self._listexpected[context] + + def stop(self, disconnectall=False): + self._stopexpected = True self.socket.shutdown(0) + if disconnectall: + self.disconnectall() def disconnectall(self, quitmsg="Disconnecting all sessions"): - for bouncerconnection in self.connections: - bouncerconnection.stop(quitmsg=quitmsg) - - def onDisconnect(self, IRC, expected=False): - self.whoexpected[IRC] = [] - if self.debug: - IRC.logwrite("dbg [Bouncer.onDisconnect] Clearing WHO expected list.") - self.whoisexpected[IRC] = [] - self.listexpected[IRC] = [] - for bouncerconnection in self.connections: - if bouncerconnection.IRC == IRC: - bouncerconnection.quit(quitmsg="IRC connection lost") - - def onSendChanMsg(self, IRC, origin, channel, targetprefix, msg): - ### Called when bot sends a PRIVMSG to channel. - ### The variable origin refers to a class instance voluntarily identifying itself as that which requested data be sent. - for bouncerconnection in self.connections: - if IRC == bouncerconnection.IRC and origin != bouncerconnection: - bouncerconnection.send(":%s!%s@%s PRIVMSG %s%s :%s\n"%(IRC.identity.nick, IRC.identity.username, IRC.identity.host, targetprefix, channel.name, msg)) - - def onSendChanAction(self, IRC, origin, channel, targetprefix, action): - self.onSendChanMsg(IRC, origin, channel, targetprefix, - "\x01ACTION %s\x01"%action) - - def onSendChanNotice(self, IRC, origin, channel, targetprefix, msg): - ### Called when bot sends a NOTICE to channel. - ### The variable origin refers to a class instance voluntarily identifying itself as that which requested data be sent. - for bouncerconnection in self.connections: - if IRC == bouncerconnection.IRC and origin != bouncerconnection: - bouncerconnection.send(":%s!%s@%s NOTICE %s%s :%s\n"%(IRC.identity.nick, IRC.identity.username, IRC.identity.host, targetprefix, channel.name, msg)) - - def onSend(self, IRC, origin, line, cmd, target, params, extinfo): + for client in self.clients: + client.quit(quitmsg=quitmsg) + + def onDisconnect(self, context, expected=False): + self._whoexpected[context] = [] + self._whoisexpected[context] = [] + self._listexpected[context] = [] + if context.identity: + for channel in context.identity.channels: + self.broadcast(context, origin=context.identity, cmd="PART", target=channel, extinfo="Bouncer Connection Lost", clients=[ + client for client in self.clients if channel not in client.hidden]) + self.broadcast(context, origin=context.identity, + cmd="QUIT", extinfo="Bouncer Connection Lost") + self.broadcast( + context, origin=self.servname, cmd="NOTICE", target=context.identity, + extinfo=":Connection to %s:%s has been lost." % (context.server, context.port)) + + def onQuit(self, context, user, quitmsg): + # For some odd reason, certain networks (*cough*Freenode*cough*) will send a quit message for the user, causing context.identity.channels to be cleared + # before onDisconnect can be executed. This is the remedy. + if user == context.identity: + for channel in context.identity.channels: + self.broadcast(context, origin=user, cmd="PART", target=channel, extinfo="Bouncer Connection Lost", clients=[ + client for client in self.clients if channel not in client.hidden]) + self.broadcast(context, origin=user, cmd="QUIT", extinfo=quitmsg, clients=[ + client for client in self.clients if any([user in channel for channel in context.channels if channel not in client.hidden])]) + + def onConnectAttempt(self, context): + self.broadcast( + context, origin=self.servname, cmd="NOTICE", target=context.identity, + extinfo="Attempting connection to %s:%s." % (context.server, context.port)) + + def onConnect(self, context): + self.broadcast( + context, origin=self.servname, cmd="NOTICE", target=context.identity, + extinfo="Connection to %s:%s established." % (context.server, context.port)) + + def onMeNickChange(self, context, newnick): + for client in self.clients: + if client.context == context: + client.send( + origin=context.identity, cmd="NICK", target=newnick) + client.nick = newnick + + def onNickChange(self, context, user, newnick): + self.broadcast(context, origin=user, cmd="NICK", target=newnick, clients=[ + client for client in self.clients if any([user in channel for channel in context.channels if channel not in client.hidden])]) + + def onRegistered(self, context): + for client in self.clients: + if client.context == context: + if client.nick != context.identity.nick: + client.send(origin="%s!%s@%s" % + (client.nick, client.username, client.host), cmd="NICK", target=context.identity.nick) + client.nick = context.identity.nick + + def onConnectFail(self, context, exc, excmsg, tb): + for client in self.clients: + if client.context == context: + client.send( + origin=self.servname, cmd="NOTICE", target=client.nick, + extinfo="Connection to %s:%s failed: %s." % (context.server, context.port, excmsg)) + + def onSendChanMsg(self, context, origin, channel, targetprefix, msg): + # Called when bot sends a PRIVMSG to channel. + # The variable origin refers to a class instance voluntarily + # identifying itself as that which requested data be sent. + self.broadcast( + context, origin=context.identity, cmd="PRIVMSG", targetprefix=targetprefix, + target=channel, extinfo=msg, clients=[client for client in self.clients if client != origin]) + + def onSendChanAction(self, context, origin, channel, targetprefix, action): + self.onSendChanMsg( + context, origin, channel, targetprefix, u"\x01ACTION {action}\x01".format(**vars())) + + def onSendChanNotice(self, context, origin, channel, targetprefix, msg): + # Called when bot sends a NOTICE to channel. + # The variable origin refers to a class instance voluntarily + # identifying itself as that which requested data be sent. + self.broadcast( + context, origin=context.identity, cmd="NOTICE", targetprefix=targetprefix, + target=channel, extinfo=msg, clients=[client for client in self.clients if client != origin]) + + def onSend(self, context, origin, line, cmd, target, targetprefix, params, extinfo): if cmd.upper() == "WHO": - self.whoexpected[IRC].append(origin) + self._whoexpected[context].append(origin) if self.debug: - with IRC.loglock: - timestamp = irc.timestamp() - if issubclass(type(origin), Thread): - name = origin.name - print >>IRC.log, "%(timestamp)s dbg [Bouncer.onSend] Adding %(origin)s (%(name)s) to WHO expected list." % vars() - else: - print >>IRC.log, "%(timestamp)s dbg [Bouncer.onSend] Adding %(origin)s to WHO expected list." % vars() - for obj in self.whoexpected[IRC]: - if issubclass(type(obj), Thread): - name = obj.name - print >>IRC.log, "%(timestamp)s dbg [Bouncer.onSend] WHO expected: %(obj)s (%(name)s)" % vars() - else: - print >>IRC.log, "%(timestamp)s dbg [Bouncer.onSend] WHO expected: %(obj)s" % vars() - IRC.log.flush() + if issubclass(type(origin), Thread): + name = origin.name + context.logwrite( + "dbg [Bouncer.onSend] Adding {origin} ({name}) to WHO expected list.".format(**vars())) + else: + context.logwrite( + "dbg [Bouncer.onSend] Adding %(origin)s to WHO expected list." % vars()) + context.logwrite( + "dbg [Bouncer.onSend] WHO expected list size: %d" % len(self._whoexpected[context])) elif cmd.upper() == "WHOIS": - self.whoisexpected[IRC].append(origin) + self._whoisexpected[context].append(origin) elif cmd.upper() == "LIST": - self.listexpected[IRC].append(origin) - - def onWhoEntry(self, IRC, origin, channel, user, channame, username, host, serv, nick, flags, hops, realname): - ### Called when a WHO list is received. - if len(self.whoexpected[IRC]) and self.whoexpected[IRC][0] in self.connections: - bncconnection = self.whoexpected[IRC][0] - try: - bncconnection.send(":%s 352 %s %s %s %s %s %s %s :%s %s\n"%(origin, IRC.identity.nick, channame, username, host, serv, nick, flags, hops, realname)) - except socket.error: - pass - - def onWhoEnd(self, IRC, origin, param, endmsg): - ### Called when a WHO list is received. - if len(self.whoexpected[IRC]) and self.whoexpected[IRC][0] in self.connections: - bncconnection = self.whoexpected[IRC][0] - try: - bncconnection.send(":%s 315 %s %s :%s\n"%( - origin, IRC.identity.nick, param, endmsg)) - except socket.error: - pass - finally: - del self.whoexpected[IRC][0] - - def onWho(self, IRC, params, wholist, endmsg): - ### Called when a WHO list is received. - if len(self.whoexpected[IRC]): - try: - if self.whoexpected[IRC][0] in self.connections: - bncconnection = self.whoexpected[IRC][0] - lines = [":%s 352 %s %s %s %s %s %s %s :%s %s\n"%(IRC.serv, IRC.identity.nick, channame, username, host, serv, nick, flags, hops, realname) for (channel, user, channame, username, host, serv, nick, flags, hops, realname) in wholist]+[":%s 315 %s %s :%s\n"%(IRC.serv, IRC.identity.nick, params, endmsg)] - bncconnection.send("".join(lines)) - finally: - del self.whoexpected[IRC][0] - - def onListStart(self, IRC, origin, params, extinfo): - ### Called when a WHO list is received. - if len(self.listexpected[IRC]) and self.listexpected[IRC][0] in self.connections: - bncconnection = self.listexpected[IRC][0] - try: - bncconnection.send(":%s 321 %s %s :%s\n"%(origin, - IRC.identity.nick, params, extinfo)) - except socket.error: - pass - - def onListEntry(self, IRC, origin, channel, population, extinfo): - ### Called when a WHO list is received. - if len(self.listexpected[IRC]) and self.listexpected[IRC][0] in self.connections: - bncconnection = self.listexpected[IRC][0] - try: - bncconnection.send(":%s 322 %s %s %d :%s\n"%(origin, IRC.identity.nick, channel.name, population, extinfo)) - except socket.error: - pass - - def onListEnd(self, IRC, origin, endmsg): - ### Called when a WHO list is received. - if len(self.listexpected[IRC]) and self.listexpected[IRC][0] in self.connections: - bncconnection = self.listexpected[IRC][0] - try: - bncconnection.send(":%s 323 %s :%s\n"%( - origin, IRC.identity.nick, endmsg)) - except socket.error: - pass - finally: - del self.listexpected[IRC][0] - - def onWhoisStart(self, IRC, origin, user, nickname, username, host, realname): - ### Called when a WHOIS reply is received. - if len(self.whoisexpected[IRC]): - if self.whoisexpected[IRC][0] in self.connections: - bncconnection = self.whoisexpected[IRC][0] - try: - bncconnection.send(":%s 311 %s %s %s %s * :%s\n" % (origin, IRC.identity.nick, nickname, username, host, realname)) - except socket.error: - pass - - def onWhoisRegisteredNick(self, IRC, origin, user, nickname, msg): - ### Called when a WHOIS reply is received. - if len(self.whoisexpected[IRC]) and self.whoisexpected[IRC][0] in self.connections: - bncconnection = self.whoisexpected[IRC][0] - try: - bncconnection.send(":%s 307 %s %s :%s\n" % ( - origin, IRC.identity.nick, nickname, msg)) - except socket.error: - pass - - def onWhoisConnectingFrom(self, IRC, origin, user, nickname, msg): - ### Called when a WHOIS reply is received. - if len(self.whoisexpected[IRC]) and self.whoisexpected[IRC][0] in self.connections: - bncconnection = self.whoisexpected[IRC][0] - try: - bncconnection.send(":%s 378 %s %s :%s\n" % ( - origin, IRC.identity.nick, nickname, msg)) - except socket.error: - pass - - def onWhoisChannels(self, IRC, origin, user, nickname, chanlist): - ### Called when a WHOIS reply is received. - if len(self.whoisexpected[IRC]) and self.whoisexpected[IRC][0] in self.connections: - bncconnection = self.whoisexpected[IRC][0] - try: - bncconnection.send(":%s 319 %s %s :%s\n" % (origin, IRC.identity.nick, nickname, " ".join(chanlist))) - except socket.error: - pass - - def onWhoisAvailability(self, IRC, origin, user, nickname, msg): - ### Called when a WHOIS reply is received. - if len(self.whoisexpected[IRC]) and self.whoisexpected[IRC][0] in self.connections: - bncconnection = self.whoisexpected[IRC][0] - try: - bncconnection.send(":%s 310 %s %s :%s\n" % ( - origin, IRC.identity.nick, nickname, msg)) - except socket.error: - pass - - def onWhoisServer(self, IRC, origin, user, nickname, server, servername): - ### Called when a WHOIS reply is received. - if len(self.whoisexpected[IRC]) and self.whoisexpected[IRC][0] in self.connections: - bncconnection = self.whoisexpected[IRC][0] - try: - bncconnection.send(":%s 312 %s %s %s :%s\n" % (origin, IRC.identity.nick, nickname, server, servername)) - except socket.error: - pass - - def onWhoisOp(self, IRC, origin, user, nickname, msg): - if len(self.whoisexpected[IRC]) and self.whoisexpected[IRC][0] in self.connections: - bncconnection = self.whoisexpected[IRC][0] - try: - bncconnection.send(":%s 313 %s %s :%s\n" % ( - origin, IRC.identity.nick, nickname, msg)) - except socket.error: - pass - - def onWhoisAway(self, IRC, origin, user, nickname, awaymsg): - if len(self.whoisexpected[IRC]) and self.whoisexpected[IRC][0] in self.connections: - bncconnection = self.whoisexpected[IRC][0] - try: - bncconnection.send(":%s 301 %s %s :%s\n" % (origin, - IRC.identity.nick, nickname, awaymsg)) - except socket.error: - pass - - def onWhoisTimes(self, IRC, origin, user, nickname, idletime, signontime, msg): - if len(self.whoisexpected[IRC]) and self.whoisexpected[IRC][0] in self.connections: - bncconnection = self.whoisexpected[IRC][0] - try: - bncconnection.send(":%s 317 %s %s %d %d :%s\n" % (origin, IRC.identity.nick, nickname, idletime, signontime, msg)) - except socket.error: - pass - - def onWhoisSSL(self, IRC, origin, user, nickname, msg): - if len(self.whoisexpected[IRC]) and self.whoisexpected[IRC][0] in self.connections: - bncconnection = self.whoisexpected[IRC][0] - try: - bncconnection.send(":%s 671 %s %s :%s\n" % ( - origin, IRC.identity.nick, nickname, msg)) - except socket.error: - pass - - def onWhoisModes(self, IRC, origin, user, nickname, msg): - if len(self.whoisexpected[IRC]) and self.whoisexpected[IRC][0] in self.connections: - bncconnection = self.whoisexpected[IRC][0] - try: - bncconnection.send(":%s 339 %s %s :%s\n" % ( - origin, IRC.identity.nick, nickname, msg)) - except socket.error: - pass - - def onWhoisLoggedInAs(self, IRC, origin, user, nickname, loggedinas, msg): - if len(self.whoisexpected[IRC]) and self.whoisexpected[IRC][0] in self.connections: - bncconnection = self.whoisexpected[IRC][0] - try: - bncconnection.send(":%s 330 %s %s %s :%s\n" % (origin, IRC.identity.nick, nickname, loggedinas, msg)) - except socket.error: - pass - - def onWhoisEnd(self, IRC, origin, user, nickname, msg): - if len(self.whoisexpected[IRC]) and self.whoisexpected[IRC][0] in self.connections: - bncconnection = self.whoisexpected[IRC][0] - try: - bncconnection.send(":%s 318 %s %s :%s\n" % ( - origin, IRC.identity.nick, nickname, msg)) - except socket.error: - pass - finally: - del self.whoisexpected[IRC][0] - - def onUnhandled(self, IRC, line, origin, cmd, target, params, extinfo): - for bouncerconnection in self.connections: - if bouncerconnection.IRC == IRC: - bouncerconnection.send("%s\n"%line) + self._listexpected[context].append(origin) + + def onWhoEntry(self, context, origin, channel, user, channame, username, host, serv, nick, flags, hops, realname): + # Called when a WHO list is received. + if len(self._whoexpected[context]): + client = self._whoexpected[context][0] + 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): + # Called when a WHO list is received. + if len(self._whoexpected[context]) and self._whoexpected[context][0] in self.clients: + client = self._whoexpected[context][0] + client.send( + origin=origin, cmd=315, target=context.identity, params=param, extinfo=endmsg) + #client.send(":%s 315 %s %s :%s\n"%(origin, context.identity.nick, param, endmsg)) + if self.debug: + if issubclass(type(self._whoexpected[context][0]), Thread): + name = self._whoexpected[context][0].name + context.logwrite( + "dbg [Bouncer.onWhoEnd] Removing %s (%s) from WHO expected list." % + (self._whoexpected[context][0], name)) + else: + context.logwrite( + "dbg [Bouncer.onWhoEnd] Removing %s from WHO expected list." % self._whoexpected[context][0]) + del self._whoexpected[context][0] + if self.debug: + context.logwrite( + "dbg [Bouncer.onWhoEnd] WHO expected list size: %d" % + len(self._whoexpected[context])) + + def onListStart(self, context, origin, params, extinfo): + # Called when a WHO list is received. + if len(self._listexpected[context]) and self._listexpected[context][0] in self.clients: + client = self._listexpected[context][0] + client.send(origin=origin, cmd=321, + target=context.identity, params=params, extinfo=extinfo) + #client.send(":%s 321 %s %s :%s\n"%(origin, context.identity.nick, params, extinfo)) + + def onListEntry(self, context, origin, channel, population, extinfo): + # Called when a WHO list is received. + 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"{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): + # Called when a WHO list is received. + if len(self._listexpected[context]) and self._listexpected[context][0] in self.clients: + client = self._listexpected[context][0] + client.send( + origin=origin, cmd=323, target=context.identity, extinfo=endmsg) + # client.send(":%s 323 %s :%s\n"%(origin, context.identity.nick, endmsg)) + del self._listexpected[context][0] + + def onWhoisStart(self, context, origin, user, nickname, username, host, realname): + # Called when a WHOIS reply is received. + if len(self._whoisexpected[context]) and self._whoisexpected[context][0] in self.clients: + client = self._whoisexpected[context][0] + client.send(origin=origin, cmd=311, target=context.identity, + params=u"{nickname} {username} {host} *".format(**vars()), extinfo=realname) + # client.send(":%s 311 %s %s %s %s * :%s\n" % (origin, context.identity.nick, nickname, username, host, realname)) + + def onWhoisRegisteredNick(self, context, origin, user, nickname, msg): + # Called when a WHOIS reply is received. + if len(self._whoisexpected[context]) and self._whoisexpected[context][0] in self.clients: + client = self._whoisexpected[context][0] + client.send( + origin=origin, cmd=307, target=context.identity, params=nickname, extinfo=msg) + # client.send(":%s 307 %s %s :%s\n" % (origin, context.identity.nick, nickname, msg)) + + def onWhoisConnectingFrom(self, context, origin, user, nickname, msg): + # Called when a WHOIS reply is received. + if len(self._whoisexpected[context]) and self._whoisexpected[context][0] in self.clients: + client = self._whoisexpected[context][0] + client.send(origin=origin, cmd=378, + target=context.identity, params=nickname, extinfo=msg) + # client.send(":%s 378 %s %s :%s\n" % (origin, context.identity.nick, nickname, msg)) + + def onWhoisChannels(self, context, origin, user, nickname, chanlist): + # Called when a WHOIS reply is received. + # TODO: Translations implementation + if len(self._whoisexpected[context]) and self._whoisexpected[context][0] in self.clients: + client = self._whoisexpected[context][0] + client.send(origin=origin, cmd=319, target=context.identity, + params=nickname, extinfo=" ".join(chanlist)) + # client.send(":%s 319 %s %s :%s\n" % (origin, context.identity.nick, nickname, " ".join(chanlist))) + + def onWhoisAvailability(self, context, origin, user, nickname, msg): + # Called when a WHOIS reply is received. + if len(self._whoisexpected[context]) and self._whoisexpected[context][0] in self.clients: + client = self._whoisexpected[context][0] + client.send( + origin=origin, cmd=310, target=context.identity, params=nickname, extinfo=msg) + # client.send(":%s 310 %s %s :%s\n" % (origin, context.identity.nick, nickname, msg)) + + def onWhoisServer(self, context, origin, user, nickname, server, servername): + # Called when a WHOIS reply is received. + if len(self._whoisexpected[context]) and self._whoisexpected[context][0] in self.clients: + client = self._whoisexpected[context][0] + client.send(origin=origin, cmd=312, target=context.identity, + params=u"{nickname} {server}".format(**vars()), extinfo=servername) + # client.send(":%s 312 %s %s %s :%s\n" % (origin, context.identity.nick, nickname, server, servername)) + + def onWhoisOp(self, context, origin, user, nickname, msg): + if len(self._whoisexpected[context]) and self._whoisexpected[context][0] in self.clients: + client = self._whoisexpected[context][0] + client.send( + origin=origin, cmd=313, target=context.identity, params=nickname, extinfo=msg) + # client.send(":%s 313 %s %s :%s\n" % (origin, context.identity.nick, nickname, msg)) + + def onWhoisAway(self, context, origin, user, nickname, awaymsg): + if len(self._whoisexpected[context]) and self._whoisexpected[context][0] in self.clients: + client = self._whoisexpected[context][0] + client.send(origin=origin, cmd=301, target=context.identity, + params=u"{nickname} {idletime} {signontime}".format(**vars()), extinfo=awaymsg) + # client.send(":%s 301 %s %s :%s\n" % (origin, context.identity.nick, nickname, awaymsg)) + + def onWhoisTimes(self, context, origin, user, nickname, idletime, signontime, msg): + if len(self._whoisexpected[context]) and self._whoisexpected[context][0] in self.clients: + client = self._whoisexpected[context][0] + client.send(origin=origin, cmd=317, target=context.identity, + params=u"{nickname} {idletime} {signontime}".format(**vars()), extinfo=msg) + # client.send(":%s 317 %s %s %d %d :%s\n" % (origin, context.identity.nick, nickname, idletime, signontime, msg)) + + def onWhoisSSL(self, context, origin, user, nickname, msg): + if len(self._whoisexpected[context]) and self._whoisexpected[context][0] in self.clients: + client = self._whoisexpected[context][0] + client.send(origin=origin, cmd=671, + target=context.identity, params=nickname, extinfo=msg) + # client.send(":%s 671 %s %s :%s\n" % (origin, context.identity.nick, nickname, msg)) + + def onWhoisModes(self, context, origin, user, nickname, msg): + if len(self._whoisexpected[context]) and self._whoisexpected[context][0] in self.clients: + client = self._whoisexpected[context][0] + client.send( + origin=origin, cmd=339, target=context.identity, params=nickname, extinfo=msg) + # ":%s 339 %s %s :%s\n" % (origin, context.identity.nick, nickname, msg)) + + def onWhoisLoggedInAs(self, context, origin, user, nickname, loggedinas, msg): + if len(self._whoisexpected[context]) and self._whoisexpected[context][0] in self.clients: + client = self._whoisexpected[context][0] + client.send(origin=origin, cmd=330, target=context.identity, + params=" ".join((nickname, loggedinas)), extinfo=msg) + # ":%s 330 %s %s %s :%s\n" % (origin, context.identity.nick, nickname, loggedinas, msg)) + + def onWhoisEnd(self, context, origin, user, nickname, msg): + if len(self._whoisexpected[context]) and self._whoisexpected[context][0] in self.clients: + client = self._whoisexpected[context][0] + client.send(origin=origin, cmd=318, + target=context.identity, params=nickname, extinfo=msg) + # ":%s 318 %s %s :%s\n" % (origin, context.identity.nick, nickname, msg) + del self._whoisexpected[context][0] + + def onJoin(self, context, user, channel): + self.broadcast(context, origin=user, cmd="JOIN", target=channel, clients=[ + client for client in self.clients if channel not in client.hidden]) + + 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, + targetprefix=targetprefix, clients=[client for client in self.clients if target not in client.hidden]) + + def broadcast(self, context, origin=None, cmd=None, target=None, params=None, extinfo=None, targetprefix=None, clients=None): + if clients == None: + clients = self.clients + for client in clients: + with client.lock: + if client.context == context and not client.quitting: + client.send( + origin, cmd, target, params, extinfo, targetprefix) class BouncerConnection (Thread): + def __init__(self, bouncer, connection, addr, debug=False): - #print "Initializing ListenThread..." + # print "Initializing ListenThread..." self.bouncer = bouncer self.connection = connection self.host, self.port = self.addr = addr[:2] - self.IRC = None + self.context = None self.pwd = None self.nick = None self.label = None @@ -360,59 +449,286 @@ class BouncerConnection (Thread): self.lock = Lock() self.quitmsg = "Connection Closed" self.quitting = False + self.hidden = irc.ChanList() + self.translations = {} + self.namesx = False + self.uhnames = False Thread.__init__(self) self.daemon = True self.start() - def send(self, data, flags=0): - try: - with self.lock: - self.connection.send(data) - except socket.error: - exc, excmsg, tb = sys.exc_info() - print >>self.IRC.logwrite(*["!!! [BouncerConnection.send] Exception in thread %(self)s" % vars()]+["!!! [BouncerConnection.send] %(tbline)s" % vars() for tbline in traceback.format_exc().split("\n")]) - self.quit(quitmsg=excmsg.message) - - def __repr__(self): - server = self.IRC.server if self.IRC else "*" - port = self.IRC.port if self.IRC else "*" - if self.IRC and self.IRC.identity: - nick = self.IRC.identity.nick - ident = self.IRC.identity.username if self.IRC.identity.username else "*" - host = self.IRC.identity.host if self.IRC.identity.host else "*" - else: - nick = "*" - ident = "*" - host = "*" - if self.IRC.ssl and self.IRC.ipv6: - protocol = "ircs6" - elif self.IRC.ssl: - protocol = "ircs" - elif self.IRC.ipv6: - protocol = "irc6" + def sendstr(self, data, flags=0): + with self.lock: + try: + self.connection.send(data.encode("utf8")) + except socket.error: + exc, excmsg, tb = sys.exc_info() + print >>self.context.logwrite(*["!!! [BouncerConnection.send] Exception in thread %(self)s" % vars()] + [ + "!!! [BouncerConnection.send] %(tbline)s" % vars() for tbline in traceback.format_exc().split("\n")]) + self.quit(quitmsg=excmsg.message) + + # Format and send a string to the client + def send(self, origin=None, cmd=None, target=None, params=None, extinfo=None, targetprefix=None, flags=0): + 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 + 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) + + if params: + line = u"{cmd} {target} {params}".format(**vars()) + elif target: + line = u"{cmd} {target}".format(**vars()) else: - protocol = "irc" - addr = self.host - return "<Bouncer connection from %(addr)s to %(nick)s!%(ident)s@%(host)s on %(protocol)s://%(server)s:%(port)s>" % locals() + line = cmd + + if extinfo != None: + line = u"{line} :{extinfo}".format(**vars()) + + if type(origin) == irc.User: + line = u":{origin:full} {line}".format(**vars()) + elif origin: + line = u":{origin} {line}".format(**vars()) + self.sendstr(u"{line}\n".format(**vars())) + + #server=self.context.server if self.context else "*" + #port=self.context.port if self.context else "*" + # if self.context and self.context.identity: + # nick=self.context.identity.nick + #ident=self.context.identity.username if self.context.identity.username else "*" + #host=self.context.identity.host if self.context.identity.host else "*" + # else: + # nick="*" + # ident="*" + # host="*" + # if self.context.ssl and self.context.ipv6: + # protocol="ircs6" + # elif self.context.ssl: + # protocol="ircs" + # elif self.context.ipv6: + # protocol="irc6" + # else: + # protocol="irc" + # addr=self.host + def __repr__(self): + return "<Bouncer connection from {self.host} to {self.context.identity} on {self.context:uri}>".format(**vars()) def quit(self, quitmsg="Disconnected"): - if not self.quitting: - self.quitmsg = quitmsg - with self.lock: + with self.lock: + if not self.quitting: + self.quitmsg = quitmsg try: - self.connection.send("ERROR :Closing link: (%s@%s) [%s]\n" % (self.IRC.identity.nick if self.IRC else "*", self.host, quitmsg)) + self.send(cmd="ERROR", extinfo="Closing link: (%s@%s) [%s]\n" % ( + self.context.identity.nick if self.context else "*", self.host, quitmsg)) except: pass try: - self.connection.shutdown(1) + self.connection.shutdown(socket.SHUT_WR) self.connection.close() except: pass self.quitting = True + def showchannel(self, channel): + with self.context.lock, self.lock: + if channel in self.hidden: + self.hidden.remove(channel) + if self.context.identity in channel.users: + self.send( + origin=self.context.identity, cmd="JOIN", target=channel) + self.sendchanneltopic(channel) + self.sendchannelnames(channel) + + def sendchanneltopic(self, channel): + with self.context.lock, self.lock: + if channel.topic and channel.topictime: + self.send(origin=self.bouncer.servname, cmd=332, + target=self.context.identity, params=channel.name, extinfo=channel.topic) + # u":{self.context.serv} 332 {self.context.identity.nick} {self.name} :{self.topic}".format(**vars()) + self.send( + origin=self.bouncer.servname, cmd=333, target=self.context.identity, + params="{channel.name} {channel.topicsetby} {channel.topictime}".format(**vars())) + # u":{self.context.serv} 333 {self.context.identity.nick} {self.name} {self.topicsetby.nick} {self.topictime}".format(**vars()) + else: + self.send(origin=self.bouncer.servname, cmd=331, + target=self.context.identity, params=channel.name, extinfo="No topic is set") + # u":{self.context.serv} 331 {self.context.identity.nick} + # {self.name} :No topic is set".format(**vars())] + + def sendchannelnames(self, channel): + with self.context.lock, self.lock: + secret = "s" in channel.modes.keys() and channel.modes["s"] + private = "p" in channel.modes.keys() and channel.modes["p"] + flag = "@" if secret else ("*" if private else "=") + + modes, symbols = supports = self.context.supports.get( + "PREFIX", irc._defaultprefix) + users = list(channel.users) + users.sort(key=lambda user: ([user not in channel.modes.get(mode, []) + for mode, char in zip(*supports)], user.nick.lower())) + if self.uhnames: + template = u"{prefixes}{user:full}" + else: + template = u"{prefixes}{user}" + + nameslist = [] + for user in users: + prefixes = u"".join( + [prefix if mode in channel.modes.keys() and user in channel.modes[mode] else "" for prefix, mode in zip(symbols, modes)]) + if not self.namesx: + prefixes = prefixes[:1] + nameslist.append(template.format(**vars())) + names = " ".join(nameslist) + + lines = [] + while len(names) > 196: + index = names.rfind(" ", 0, 196) + slice = names[:index] + self.send( + origin=self.bouncer.servname, cmd=353, target=self.context.identity, + params="{flag} {channel.name}".format(**vars()), extinfo=slice) + #u":{channel.context.serv} 353 {channel.context.identity.nick} {flag} {channel.name} :{slice}".format(**vars()) + names = names[index + 1:] + if len(names): + self.send( + origin=self.bouncer.servname, cmd=353, target=self.context.identity, + params="{flag} {channel.name}".format(**vars()), extinfo=names) + #u":{channel.context.serv} 353 {channel.context.identity.nick} {flag} {channel.name} :{names}".format(**vars()) + + self.send( + origin=self.bouncer.servname, cmd=366, target=self.context.identity, + params=channel.name, extinfo="End of /NAMES list.") + # u":{channel.context.serv} 366 {channel.context.identity.nick} {channel.name} :End of /NAMES list.".format(**vars()) + + def sendchannelmodes(self, channel, modechars=None): + with self.context.lock, self.lock: + if modechars: + for mode in modechars: + if mode not in _listnumerics.keys(): + continue + i, e, l = _listnumerics[mode] + if mode in channel.modes.keys(): + for (mask, setby, settime) in channel.modes[mode]: + self.send( + origin=self.bouncer.servname, cmd=i, target=self.context.identity, + params=u"{channel.name} {mask} {setby} {settime}".format(**vars())) + self.send(origin=self.bouncer.servname, cmd=e, + target=self.context.identity, params=u"{channel.name} {l}".format(**vars())) + else: + items = channel.modes.items() + chanmodes = self.context.supports.get( + "CHANMODES", irc._defaultchanmodes) + prefix = self.context.supports.get( + "PREFIX", irc._defaultprefix) + modes = "".join( + [mode for (mode, val) in items if mode not in chanmodes[0] + prefix[0] and val]) + params = " ".join( + [val for (mode, val) in items if mode in chanmodes[1] + chanmodes[2] and val]) + if modes and params: + self.send( + origin=self.bouncer.servname, cmd=324, target=self.context.identity, + params="{channel.name} +{modes} {params}".format(**vars())) + # u":{channel.context.identity.server} 324 {channel.context.identity.nick} {channel.name} +{modes} {params}".format(**vars()) + elif modes: + self.send( + origin=self.bouncer.servname, cmd=324, target=self.context.identity, + params="{channel.name} +{modes}".format(**vars())) + # u":{channel.context.identity.server} 324 {channel.context.identity.nick} {channel.name} +{modes}".format(**vars()) + + def sendsupports(self): + with self.context.lock, self.lock: + 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.context.supports.items()] + if "UHNAMES" not in supports: + supports.append("UHNAMES") + if "NAMESX" not in supports: + supports.append("NAMESX") + supports.sort() + supports = " ".join(supports) + lines = [] + while len(supports) > 196: + index = supports.rfind(" ", 0, 196) + slice = supports[:index] + self.send( + origin=self.bouncer.servname, cmd=5, target=self.context.identity, + params=slice, extinfo="are supported by this server") + # u":{self.context.serv} 005 {self.context.identity.nick} {slice} :are supported by this server".format(**vars()) + supports = supports[index + 1:] + if supports: + self.send( + origin=self.bouncer.servname, cmd=5, target=self.context.identity, + params=supports, extinfo="are supported by this server") + # u":{self.context.serv} 005 {self.context.identity.nick} {supports} :are supported by this server".format(**vars()) + + def sendgreeting(self): + with self.context.lock, self.lock: + if self.context.welcome: + self.send(origin=self.bouncer.servname, cmd=1, + target=self.context.identity, extinfo=self.context.welcome) + # u":{self.context.serv} 001 {self.context.identity.nick} :{self.context.welcome}".format(**vars()) + if self.context.hostinfo: + self.send(origin=self.bouncer.servname, cmd=2, + target=self.context.identity, extinfo=self.context.hostinfo) + # u":{self.context.serv} 002 {self.context.identity.nick} :{self.context.hostinfo}".format(**vars()) + if self.context.servcreated: + self.send(origin=self.bouncer.servname, cmd=3, + target=self.context.identity, extinfo=self.context.servcreated) + # u":{self.context.serv} 003 {self.context.identity.nick} :{self.context.servcreated}".format(**vars()) + if self.context.servinfo: + self.send(origin=self.bouncer.servname, cmd=4, + target=self.context.identity, params=self.context.servinfo) + # u":{self.context.serv} 004 {self.context.identity.nick} {self.context.servinfo}".format(**vars()) + + def sendmotd(self): + with self.context.lock, self.lock: + if self.context.motdgreet and self.context.motd and self.context.motdend: + self.send(origin=self.bouncer.servname, cmd=375, + target=self.context.identity, extinfo=self.context.motdgreet) + # u":{server} 375 {self.identity.nick} :{self.motdgreet}".format(**vars()) + for motdline in self.context.motd: + self.send(origin=self.bouncer.servname, cmd=372, + target=self.context.identity, extinfo=motdline) + # u":{server} 372 {self.identity.nick} :{motdline}".format(**vars()) + self.send(origin=self.bouncer.servname, cmd=376, + target=self.context.identity, extinfo=self.context.motdend) + # u":{server} 376 {self.identity.nick} :{self.motdend}".format(**vars()) + else: + self.send(origin=self.bouncer.servname, cmd=422, + target=self.context.identity, extinfo="MOTD File is missing") + # u":{server} 422 {self.identity.nick} :MOTD File is missing".format(**vars()) + + def sendusermodes(self): + with self.context.lock, self.lock: + self.send( + origin=self.bouncer.servname, cmd=221, target=self.context.identity, + params="+{self.context.identity.modes}".format(**vars())) + if "s" in self.context.identity.modes: + self.send( + origin=self.bouncer.servname, cmd=8, target=self.context.identity, + params="+{self.context.identity.snomask}".format(**vars()), extinfo="Server notice mask") + def run(self): - ### Name loopup should happen here instead + # Name loopup should happen here instead ipv4match = re.findall( r"^::ffff:((\d+)\.(\d+)\.(\d+)\.(\d+))$", self.host) if self.bouncer.ipv6 and ipv4match: @@ -428,14 +744,7 @@ class BouncerConnection (Thread): except: pass - ### Add connection to connection list. - - listnumerics = dict(b=(367, 368, "channel ban list"), - e=(348, 349, "Channel Exception List"), - I=(346, 347, "Channel Invite Exception List"), - w=(910, 911, "Channel Access List"), - g=(941, 940, "chanel spamfilter list"), - X=(954, 953, "channel exemptchanops list")) + # Add connection to connection list. passwd = None nick = None @@ -447,10 +756,18 @@ class BouncerConnection (Thread): try: while True: - ### Read data (appending) into readbuf, then break lines and append lines to linebuf + # Read data (appending) into readbuf, then break lines and + # append lines to linebuf while len(linebuf) == 0: timestamp = irc.timestamp() - read = self.connection.recv(512) + try: + read = self.connection.recv(512) + except socket.error, msg: + self.quit(msg) + sys.exit() + except ssl.SSLError, msg: + self.quit(msg) + sys.exit() if read == "" and len(linebuf) == 0: # No more data to process. #self.quitmsg="Connection Closed" sys.exit() @@ -460,10 +777,18 @@ class BouncerConnection (Thread): if lastlf >= 0: linebuf.extend(string.split(readbuf[0:lastlf], "\n")) - readbuf = readbuf[lastlf+1:] + readbuf = readbuf[lastlf + 1:] line = string.rstrip(linebuf.pop(0)) - match = re.findall("^(.+?)(?:\\s+(.+?)(?:\\s+(.+?))??)??(?:\\s+:(.*))?$", line, re.I) + try: + line = line.decode("utf8") + except UnicodeDecodeError: + # Attempt to figure encoding + charset = chardet.detect(line)['encoding'] + line = line.decode(charset) + match = re.findall( + "^(.+?)(?:\\s+(.+?)(?:\\s+(.+?))??)??(?:\\s+:(.*))?$", line, re.I) + # print match if len(match) == 0: continue @@ -474,218 +799,176 @@ class BouncerConnection (Thread): passwd = target if target else extinfo else: self.quit("Access Denied") + print "*** [BouncerConnection] Incoming connection from %s failed: Expected PASS." % (self.host) break - elif not nick: # Bouncer expects a NICK command + elif not self.nick: # Bouncer expects a NICK command if cmd.upper() == "NICK": - nick = target if target else extinfo + self.nick = target if target else extinfo else: self.quit("Access Denied") + print "*** [BouncerConnection] Incoming connection from %s failed: Expected NICK." % (self.host) break elif not self.username: # Bouncer expects a USER command to finish registration if cmd.upper() == "USER": self.username = target - #print self.username - if self.username in self.bouncer.servers.keys(): - self.IRC, passwdhash, hashtype = self.bouncer.servers[self.username] - passmatch = hashlib.new(hashtype, passwd).hexdigest() == passwdhash - with self.IRC.lock: - self.IRC.logwrite("*** [BouncerConnection] Incoming connection from %s to %s." % (self.host, self.IRC)) - with self.bouncer.lock: - ### Announce connection to all other bouncer connections. - if self.debug: - self.IRC.logwrite("dbg [BouncerConnection] Attempting to broadcast incoming connection %(self)s." % vars()) - for bouncerconnection in self.bouncer.connections: - if bouncerconnection.IRC != self.IRC: - continue - if self.debug: - self.IRC.logwrite("dbg [BouncerConnection] Broadcasting to %(bouncerconnection)s." % vars()) - if not bouncerconnection.quitting: - bouncerconnection.send(":*Bouncer* NOTICE %s :Incoming connection from %s to %s\n" % (bouncerconnection.IRC.identity.nick, self.host, self.IRC)) - if self.debug: - self.IRC.logwrite("dbg [BouncerConnection] Success: %(bouncerconnection)s." % vars()) - if len([bncconnection for bncconnection in self.bouncer.connections if bncconnection.IRC == self.IRC]) == 0 and self.IRC.identity.away: - ### Bouncer connection should automatically return from away status. - self.IRC.raw("AWAY") - self.bouncer.connections.append(self) - - if not (self.IRC.connected and self.IRC.registered and type(self.IRC.supports) == dict and "CHANMODES" in self.IRC.supports.keys() and passmatch): - self.quit("Access Denied") - break - - ### If we have made it to this point, then access has been granted. - labels = [bouncerconnection.label for bouncerconnection in self.bouncer.connections if bouncerconnection.IRC == self.IRC and bouncerconnection.label] - n = 1 - while "*%s_%d"%(self.username, n) in labels: - n += 1 - self.label = "*%s_%d"%(self.username, n) - - ### Request Version info. - #self.connection.send(":$bouncer PRIVMSG %s :\x01VERSION\x01\n" % (self.IRC.identity.nick)) - - ### Log incoming connection - - ### Send Greeting. - with self.lock: - self.connection.send(":%s 001 %s :%s\n" % (self.IRC.serv, self.IRC.identity.nick, self.IRC.welcome)) - self.connection.send(":%s 002 %s :%s\n" % (self.IRC.serv, self.IRC.identity.nick, self.IRC.hostinfo)) - self.connection.send(":%s 003 %s :%s\n" % (self.IRC.serv, self.IRC.identity.nick, self.IRC.servcreated)) - self.connection.send(":%s 004 %s %s\n" % (self.IRC.serv, self.IRC.identity.nick, self.IRC.servinfo)) - - ### Send 005 response. - 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.IRC.supports.items()] - supports.sort() - supportsreply = [] - supportsstr = " ".join(supports) - index = 0 - while True: - if len(supportsstr)-index > 196: - nextindex = supportsstr.rfind(" ", index, index+196) - supportsreply.append(supportsstr[index:nextindex]) - index = nextindex+1 - else: - supportsreply.append(supportsstr[index:]) - break - for support in supportsreply: - self.connection.send(":%s 005 %s %s :are supported by this server\n" % (self.IRC.serv, self.IRC.identity.nick, support)) - - ### Send MOTD - if self.IRC.motdgreet and self.IRC.motd and self.IRC.motdend: - self.connection.send(":%s 375 %s :%s\n" % (self.IRC.serv, self.IRC.identity.nick, self.IRC.motdgreet)) - for motdline in self.IRC.motd: - self.connection.send(":%s 372 %s :%s\n" % (self.IRC.serv, self.IRC.identity.nick, motdline)) - try: - self.connection.send(":%s 376 %s :%s\n" % (self.IRC.serv, self.IRC.identity.nick, self.IRC.motdend)) - except AttributeError: - self.connection.send(":%s 376 %s\n" % (self.IRC.serv, self.IRC.identity.nick)) - else: - self.connection.send(":%s 422 %s :MOTD File is missing\n" % (self.IRC.serv, self.IRC.identity.nick)) - - ### Send user modes and snomasks. - self.connection.send(":%s 221 %s +%s\n" % (self.IRC.serv, self.IRC.identity.nick, self.IRC.identity.modes)) - if "s" in self.IRC.identity.modes and self.IRC.identity.snomask: - self.connection.send(":%s 008 %s +%s :Server notice mask\n" % (self.IRC.server, self.IRC.identity.nick, self.IRC.identity.snomask)) - - # ### Join user to internal bouncer channel. - # self.connection.send(":%s!%s@%s JOIN :$bouncer\n" % (self.IRC.identity.nick, self.IRC.identity.username, self.IRC.identity.host)) - - # ### Set internal bouncer topic. - # self.connection.send(":$bouncer 332 %s $bouncer :Bouncer internal channel. Enter bouncer commands here.\n" % (self.IRC.identity.nick)) - # self.connection.send(":$bouncer 333 %s $bouncer $bouncer %s\n" % (self.IRC.identity.nick, self.bouncer.starttime)) - - # ### Send NAMES for internal bouncer channel. - # self.connection.send(":$bouncer 353 %s @ $bouncer :%s\n" % ( - # self.IRC.identity.nick, - # string.join(["@*Bouncer*"]+["@%s"%bouncerconnection.label for bouncerconnection in self.bouncer.connections])) - # ) - # self.connection.send(":$bouncer 366 %s $bouncer :End of /NAMES list.\n" % (self.IRC.identity.nick)) - - # ### Give operator mode to user. - # self.connection.send(":*Bouncer* MODE $bouncer +o %s\n" % (self.IRC.identity.nick)) - - ### Join user to channels. - for channel in self.IRC.identity.channels: - ### JOIN command - self.connection.send(":%s!%s@%s JOIN :%s\n" % (self.IRC.identity.nick, self.IRC.identity.username, self.IRC.identity.host, channel.name)) - - ### Topic - self.connection.send(":%s 332 %s %s :%s\n" % (self.IRC.serv, self.IRC.identity.nick, channel.name, channel.topic)) - self.connection.send(":%s 333 %s %s %s %s\n" % (self.IRC.serv, self.IRC.identity.nick, channel.name, channel.topicsetby, channel.topictime)) - - ### Determine if +s or +p modes are set in channel - secret = "s" in channel.modes.keys() and channel.modes["s"] - private = "p" in channel.modes.keys() and channel.modes["p"] - - ### Construct NAMES for channel. - namesusers = [] - modes, symbols = self.IRC.supports["PREFIX"] - self.connection.send(":%s 353 %s %s %s :%s\n" % ( - self.IRC.serv, - self.IRC.identity.nick, - "@" if secret else ("*" if private else "="), - channel.name, - string.join([string.join([symbols[k] if modes[k] in channel.modes.keys() and user in channel.modes[modes[k]] else "" for k in xrange(len(modes))], "")+user.nick for user in channel.users])) - ) - self.connection.send(":%s 366 %s %s :End of /NAMES list.\n" % (self.IRC.serv, self.IRC.identity.nick, channel.name)) - - else: # User not found + contextfound = False + for self.context, conf in self.bouncer.conf.items(): + # print conf.label, self.username + if conf.label == self.username: + contextfound = True + break + if not contextfound: self.quit("Access Denied") + print >>sys.stderr, "*** [BouncerConnection] Incoming connection from %s denied: Context not found." % ( + self.host) break + passmatch = hashlib.new( + conf.hashtype, passwd).hexdigest() == conf.passwd + with self.context.lock: + if not passmatch: + self.quit("Access Denied") + 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=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: + # continue + # if not client.quitting: + #client.send(origin=self.bouncer.servname, cmd="NOTICE", target=client.context.identity, extinfo="Incoming connection from %s to %s dened: Invalid password.\n" % (self.host, self.context)) + break + + self.context.logwrite( + "*** [BouncerConnection] Incoming connection from %s to %s established." % (self.host, self.context)) + with self.bouncer.lock: + self.translations = dict( + self.bouncer.conf[self.context].translations) + # Announce connection to all other bouncer + # clients. + self.bouncer.broadcast( + self.context, origin=self.bouncer.servname, cmd="NOTICE", target=self.context.identity, + extinfo="Incoming connection from %s to %s established." % (self.host, self.context)) + # for client in self.bouncer.clients: + # if client.context!=self.context: + # continue + # if not client.quitting: + #client.send(":*Bouncer* NOTICE %s :Incoming connection from %s to %s\n" % (client.context.identity.nick, self.host, self.context)) + if len([client for client in self.bouncer.clients if client.context == self.context]) == 0 and self.context.registered and type(self.context.identity) == irc.User and self.context.identity.away: + # Bouncer connection should + # automatically return from away + # status. + self.context.raw("AWAY") + self.hidden = irc.ChanList( + self.bouncer.conf[self.context].hidden, context=self.context) + self.bouncer.clients.append(self) + + if self.context.registered: + # Send Greeting. + with self.lock: + self.sendgreeting() + self.sendsupports() + self.sendmotd() + self.sendusermodes() + + # Join user to channels. + for channel in self.context.identity.channels: + if channel not in self.hidden: + self.showchannel(channel) + else: + self.send( + origin=self.bouncer.servname, cmd="NOTICE", target=self.nick, + extinfo="Not connected to server. Type /bncconnect to attempt connection.") + #self.send(u":%s 001 %s :Welcome to the Bouncer context Network %s!%s@%s\n" % ("*Bouncer*", self.nick, self.nick, self.username, self.host)) else: # Client did not send USER command when expected self.quit("Access Denied") + print "*** [BouncerConnection] Incoming connection from %s failed: Expected USER." % (self.host) break - elif cmd.upper() == "QUIT": - self.quit(extinfo) - break - - elif cmd.upper() == "PING": - self.send(":%s PONG %s :%s\n" % (self.IRC.serv, self.IRC.serv, self.IRC.identity.nick)) - - elif cmd.upper() in ("PRIVMSG", "NOTICE"): - ### Check if CTCP - ctcp = re.findall("^\x01(.*?)(?:\\s+(.*?)\\s*)?\x01$", - extinfo) - - if ctcp: # If CTCP, only want to - (ctcptype, ext) = ctcp[0] # Unpack CTCP info - - if ctcptype == "LAGCHECK": # Client is doing a lag check. No need to send to IRC network, just reply back. - self.send(":%s!%s@%s %s\n" % (self.IRC.identity.nick, self.IRC.identity.username, self.IRC.identity.host, line)) - else: - self.IRC.raw(line, origin=self) - else: - self.IRC.raw(line, origin=self) - - elif cmd.upper() == "MODE": # Will want to determine is requesting modes, or attempting to modify modes. - if target and "CHANTYPES" in self.IRC.supports.keys() and target[0] in self.IRC.supports["CHANTYPES"]: - if params == "": - channel = self.IRC.channel(target) - modes = channel.modes.keys() - modestr = "".join([mode for mode in modes if mode not in self.IRC.supports["CHANMODES"][0]+self.IRC.supports["PREFIX"][0] and channel.modes[mode]]) - params = " ".join([channel.modes[mode] for mode in modes if mode in self.IRC.supports["CHANMODES"][1]+self.IRC.supports["CHANMODES"][2] and channel.modes[mode]]) - with self.lock: - if len(modestr): - self.connection.send(":%s 324 %s %s +%s %s\n" % (self.IRC.serv, self.IRC.identity.nick, channel.name, modestr, params)) - if channel.created: - self.connection.send(":%s 329 %s %s %s\n" % (self.IRC.serv, self.IRC.identity.nick, channel.name, channel.created)) - elif re.match("^\\+?[%s]+$"%self.IRC.supports["CHANMODES"][0], params) and extinfo == "": - #print "ddd Mode List Request", params - channel = self.IRC.channel(target) - redundant = [] - for mode in params.lstrip("+"): - if mode in redundant or mode not in listnumerics.keys(): - continue - i, e, l = listnumerics[mode] - with self.lock: - if mode in channel.modes.keys(): - for (mask, setby, settime) in channel.modes[mode]: - self.connection.send(":%s %d %s %s %s %s %s\n" % (self.IRC.serv, i, channel.context.identity.nick, channel.name, mask, setby, settime)) - self.connection.send(":%s %d %s %s :End of %s\n" % (self.IRC.serv, e, channel.context.identity.nick, channel.name, l)) - redundant.append(mode) - else: - self.IRC.raw(line, origin=self) - elif params == "" and target.lower() == self.IRC.identity.nick.lower(): - with self.lock: - self.connection.send(":%s 221 %s +%s\n" % (self.IRC.serv, self.IRC.identity.nick, self.IRC.identity.modes)) - if "s" in self.IRC.identity.modes and self.IRC.identity.snomask: - self.connection.send(":%s 008 %s +%s :Server notice mask\n" % (self.IRC.serv, self.IRC.identity.nick, self.IRC.identity.snomask)) - else: - self.IRC.raw(line, origin=self) else: - self.IRC.raw(line, origin=self) + chantypes = self.context.supports.get( + "CHANTYPES", irc._defaultchantypes) + # Disable translating for now. + if False and cmd.upper() not in ("SETTRANSLATE", "RMTRANSLATE"): + translated = [] + for targ in target.split(","): + translatefound = False + if re.match(irc._chanmatch % re.escape(chantypes), targ): + for channel, translate in self.translations.items(): + if targ.lower() == translate.lower(): + translated.append(channel.name) + translatefound = True + break + if not translatefound: + translated.append(targ) + target = ",".join(translated) + + translated = [] + for param in params.split(" "): + translatefound = False + if re.match(irc._chanmatch % re.escape(chantypes), param): + for channel, translate in self.translations.items(): + if param.lower() == translate.lower(): + translated.append(channel.name) + translatefound = True + break + if not translatefound: + translated.append(param) + params = " ".join(translated) + + if params: + line = u"{cmd} {target} {params}".format(**vars()) + elif target: + line = u"{cmd} {target}".format(**vars()) + else: + line = cmd + + if extinfo: + line = u"{line} :{extinfo}".format(**vars()) + + cmdmethod = "cmd%s" % cmd.upper() + if hasattr(self, cmdmethod): + method = getattr(self, cmdmethod) + try: + method(line, target, params, extinfo) + except SystemExit: + sys.exit() + except: + if self.context: + exc, excmsg, tb = sys.exc_info() + self.context.logwrite(*[u"!!! [BouncerConnection] Exception in thread %(self)s" % vars()] + [ + u"!!! [BouncerConnection] %(tbline)s" % vars() for tbline in traceback.format_exc().split("\n")]) + print >>sys.stderr, "Exception in thread %(self)s" % vars( + ) + print >>sys.stderr, traceback.format_exc() + elif not self.context.connected: + self.send( + origin=self.bouncer.servname, cmd="NOTICE", target=self.nick, + extinfo="Not connected to server. Type /bncconnect to attempt connection.") + continue + + elif not self.context.registered: + self.send(origin=self.bouncer.servname, cmd="NOTICE", + target=self.nick, extinfo="Not registered.") + continue + else: + self.context.raw(line, origin=self) except SystemExit: pass # No need to pass error message if break resulted from sys.exit() except: exc, excmsg, tb = sys.exc_info() self.quitmsg = str(excmsg) - if self.IRC: + if self.context: exc, excmsg, tb = sys.exc_info() - self.IRC.logwrite(*["!!! [BouncerConnection] Exception in thread %(self)s" % vars()]+["!!! [BouncerConnection] %(tbline)s" % vars() for tbline in traceback.format_exc().split("\n")]) + self.context.logwrite(*["!!! [BouncerConnection] Exception in thread %(self)s" % vars()] + [ + "!!! [BouncerConnection] %(tbline)s" % vars() for tbline in traceback.format_exc().split("\n")]) + print >>sys.stderr, "Exception in thread %(self)s" % vars() + print >>sys.stderr, traceback.format_exc() finally: - ### Juuuuuuust in case. + # Juuuuuuust in case. with self.lock: try: self.connection.shutdown(1) @@ -693,29 +976,208 @@ class BouncerConnection (Thread): except: pass - if self.IRC: - self.IRC.logwrite("*** [BouncerConnection] Connection from %s terminated (%s)." % (self.host, self.quitmsg)) - - if self in self.bouncer.connections: - with self.bouncer.lock: - self.bouncer.connections.remove(self) - if self.IRC.connected and self.IRC.identity and len([bncconnection for bncconnection in self.bouncer.connections if bncconnection.IRC == self.IRC]) == 0 and not self.IRC.identity.away and self.bouncer.autoaway: - ### Bouncer automatically sets away status. - self.IRC.raw("AWAY :%s"%self.bouncer.autoaway) - if self.debug: - self.IRC.logwrite("dbg [BouncerConnection] Attempting to broadcast terminated connection %(self)s." % vars()) - for bouncerconnection in self.bouncer.connections: - if bouncerconnection.IRC == self.IRC: - if self.debug: - self.IRC.logwrite("dbg [BouncerConnection] Broadcasting to %(bouncerconnection)s." % vars()) - if not bouncerconnection.quitting: - bouncerconnection.connection.send(":*Bouncer* NOTICE %s :Connection from %s to %s terminated (%s)\n" % (bouncerconnection.IRC.identity.nick, self.host, self.IRC, self.quitmsg)) - if self.debug: - self.IRC.logwrite("dbg [BouncerConnection] Success: %(bouncerconnection)s." % vars()) - -# ### Announce QUIT to other bouncer connections. -# for bouncerconnection in self.bouncer.connections: -# try: -# bouncerconnection.connection.send(":%s!%s@%s QUIT :%s\n" % (self.label, self.username, self.host, self.quitmsg)) -# except: -# pass + if self.context: + self.context.logwrite( + "*** [BouncerConnection] Connection from %s terminated (%s)." % (self.host, self.quitmsg)) + + with self.bouncer.lock: + if self in self.bouncer.clients: + self.bouncer.clients.remove(self) + if self.context.connected and self.context.identity and len([client for client in self.bouncer.clients if client.context == self.context]) == 0 and self.context.registered and type(self.context.identity) == irc.User and not self.context.identity.away and self.bouncer.autoaway: + # Bouncer automatically sets away status. + self.context.raw("AWAY :%s" % self.bouncer.autoaway) + self.bouncer.broadcast( + self.context, origin=self.bouncer.servname, cmd="NOTICE", target=self.context.identity, + extinfo="Connection from %s to %s terminated (%s)\n" % (self.host, self.context, self.quitmsg)) + # ":*Bouncer* NOTICE %s :Connection from %s to %s terminated (%s)\n" % (client.context.identity.nick, self.host, self.context, self.quitmsg)) + + def cmdQUIT(self, line, target, params, extinfo): + self.quit(extinfo) + sys.exit() + + def cmdPROTOCTL(self, line, target, params, extinfo): + protoparams = [target.upper()] + params.upper().split() + if "NAMESX" in protoparams: + self.namesx = True + if "UHNAMES" in protoparams: + self.uhnames = True + + def cmdPING(self, line, target, params, extinfo): + with self.context.lock: + if True or (self.context.identity and type(self.context.identity) == irc.User): + self.send(origin=self.bouncer.servname, + cmd="PONG", target=params, extinfo=target) + # u":{self.context.identity.server} PONG {params} + # :{target}\n".format(**vars()).encode("utf8")) + else: + self.send(origin=self.bouncer.servname, + cmd="PONG", params=params, extinfo=target) + self.send( + u":{self.context.server} PONG {params} :{target}\n".format(**vars()).encode("utf8")) + + def cmdPRIVMSG(self, line, target, params, extinfo): + # Check if CTCP + ctcp = re.findall("^\x01(.*?)(?:\\s+(.*?)\\s*)?\x01$", extinfo) + + if ctcp: + (ctcptype, ext) = ctcp[0] # Unpack CTCP info + + if ctcptype == "LAGCHECK": # Client is doing a lag check. No need to send to context network, just reply back. + self.send( + u":{self.context.identity:full} {line}\n".format(**vars()).encode("utf8")) + else: + self.context.raw(line, origin=self) + else: + self.context.raw(line, origin=self) + + def cmdMODE(self, line, target, params, extinfo): # Will want to determine is requesting modes, or attempting to modify modes. + # if target and "CHANTYPES" in self.context.supports.keys() and + # target[0] in self.context.supports["CHANTYPES"]: + chantypes = self.context.supports.get( + "CHANTYPES", irc._defaultchantypes) + chanmodes = self.context.supports.get( + "CHANMODES", irc._defaultchanmodes) + prefix = self.context.supports.get("PREFIX", irc._defaultprefix) + if re.match(irc._chanmatch % re.escape(chantypes), target): + channel = self.context[target] + + if params == "": + # We are requesting the modes for the channel + if self.context.identity in channel.users: + # We are in the channel, and we know the channel modes + self.sendchannelmodes(channel) + else: + # We are NOT in the channel, so we will forward the request + # to the server. + self.context.raw( + u"MODE {channel.name}".format(**vars()), origin=self) + + elif re.match("^\\+?[%s]+$" % chanmodes[0], params) and extinfo == "": + # We are requesting one or more mode lists. + modechars = "" + for mode in params.lstrip("+"): + if mode not in modechars: + modechars += mode + if self.context.identity in channel.users: + self.sendchannelmodes(channel, modechars) + else: + self.context.raw( + u"MODE {channel.name} {params}".format(**vars()), origin=self) + else: + self.context.raw(line, origin=self) + elif params == "" and target.lower() == self.context.identity.nick.lower(): + self.sendusermodes() + else: + self.context.raw( + u"MODE {target} {params}".format(**vars()), origin=self) + + def cmdNAMES(self, line, target, params, extinfo): + chantypes = self.context.supports.get( + "CHANTYPES", irc._defaultchantypes) + chanmodes = self.context.supports.get( + "CHANMODES", irc._defaultchanmodes) + prefix = self.context.supports.get("PREFIX", irc._defaultprefix) + fallback = [] + with self.lock: + for channame in target.split(): + if re.match(irc._chanmatch % re.escape(chantypes), channame): + channel = self.context[channame] + with self.lock: + if self.context.identity in channel: + self.sendchannelnames(channel) + else: + fallback.append(channame) + else: + fallback.append(channame) + if fallback: + self.context.raw("NAMES %s" % + (",".join(fallback)), origin=self) + + def cmdSHOW(self, line, target, params, extinfo): + chantypes = self.context.supports.get( + "CHANTYPES", irc._defaultchantypes) + with self.context.lock, self.lock: + for channame in target.split(): + if re.match(irc._chanmatch % re.escape(chantypes), channame): + channel = self.context[channame] + if channel in self.hidden: + if self.context.identity in channel: + self.showchannel(channel) + else: + self.hidden.remove(channel) + self.send( + origin=self.bouncer.servname, cmd="NOTICE", target=self.context.identity, + extinfo="{channel.name} removed from hidden list, but not joined.".format(**vars())) + else: + self.send( + origin=self.bouncer.servname, cmd="NOTICE", target=self.context.identity, + extinfo="{channel.name} not in hidden list.".format(**vars())) + else: + self.send( + origin=self.bouncer.servname, cmd="NOTICE", target=self.context.identity, + extinfo="{channame}: invalid channel name.".format(**vars())) + + def cmdHIDE(self, line, target, params, extinfo): + chantypes = self.context.supports.get( + "CHANTYPES", irc._defaultchantypes) + with self.context.lock, self.lock: + for channame in target.split(): + if re.match(irc._chanmatch % re.escape(chantypes), channame): + channel = self.context[channame] + if channel not in self.hidden: + if self.context.identity in channel: + self.send( + origin=self.context.identity, cmd="PART", target=channel, extinfo="Hiding channel") + else: + self.send( + origin=self.bouncer.servname, cmd="NOTICE", target=self.context.identity, + extinfo="{channel.name} added to the hidden list, but not joined.".format(**vars())) + self.hidden.append(channel) + else: + self.send( + origin=self.bouncer.servname, cmd="NOTICE", target=self.context.identity, + extinfo="{channel.name} already in hidden list.".format(**vars())) + else: + self.send( + origin=self.bouncer.servname, cmd="NOTICE", target=self.context.identity, + extinfo="{channame}: invalid channel name.".format(**vars())) + + def cmdSETTRANSLATE(self, line, target, params, extinfo): + chantypes = self.context.supports.get( + "CHANTYPES", irc._defaultchantypes) + 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": + translations_lower = [translation.translate(irc._rfc1459casemapping) + for translation in self.translations.values()] + params_lower = params.translate(irc._rfc1459casemapping) + else: + translations_lower = [translation.lower() + for translation in self.translations.values()] + params_lower = params.lower() + if params_lower in translations_lower: + self.send( + origin=self.bouncer.servname, cmd="NOTICE", target=self.context.identity, + extinfo="Cannot set translation for {channel.name} to {param}.".format(**vars())) + else: + self.send(origin=self.context.identity, cmd="PART", + target=channel, extinfo="Translating...") + self.translations[channel] = params + self.showchannel(channel) + + def cmdRMTRANSLATE(self, line, target, params, extinfo): + chantypes = self.context.supports.get( + "CHANTYPES", irc._defaultchantypes) + 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(): + self.send( + origin=self.bouncer.servname, cmd="NOTICE", target=self.context.identity, + extinfo="Cannot remove translation for {channel.name}.".format(**vars())) + else: + self.send(origin=self.context.identity, cmd="PART", + target=channel, extinfo="Translating...") + del self.translations[channel] + self.showchannel(channel) @@ -5,34 +5,37 @@ import os 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 + count = self.firecount[vic] + 1 else: count = 1 self.firecount[vic] = count - if 10 <= count%100 < 20: + if 10 <= count % 100 < 20: ordinal = "th" - elif count%10 == 1: + elif count % 10 == 1: ordinal = "st" - elif count%10 == 2: + elif count % 10 == 2: ordinal = "nd" - elif count%10 == 3: + elif count % 10 == 3: ordinal = "rd" else: ordinal = "th" channel.me("fires %s out of a cannon for the %d%s time." % (vic.nick, count, ordinal)) else: - channel.msg("%s: I cannot fire %s out of a cannon, as he or she is not here."%(user.nick, nickname)) + channel.msg( + "%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) @@ -4,6 +4,7 @@ import os class Figlet(object): + def onChanMsg(self, IRC, user, channel, targetprefix, msg): matches = re.findall("^!figlet\\s+(.*)$", msg) if matches: diff --git a/irc.conf b/irc.conf new file mode 100644 index 0000000..498ce86 --- /dev/null +++ b/irc.conf @@ -0,0 +1,48 @@ +{ + "addons": { + "logger": { + "class": "logger.Logger", + "logroot": "/home/caretaker82/IRC" + }, + "bouncer": { + "class": "bouncer.Bouncer", + "port": 16698, + "certfile": "cert.pem", + "keyfile": "key.pem", + "autoaway": "I'm off to see the wizard!" + }, + "autoexec": { + "class": "autoexec.Autoexec" + } + }, + "networks": { + "InsomniaIRC": { + "class": "irc.Connection", + "server": "irc.insomniairc.net", + "nick": "pyIRC", + "secure": true, + "addons": [ + { + "addon": <addons.logger>, + "label": "InsomniaIRC" + }, + { + "addon": <addons.bouncer>, + "label": "InsomniaIRC", + "passwd": "6b97ed68d14eb3f1aa959ce5d49c7dc612e1eb1dafd73b1e705847483fd6a6c809f2ceb4e8df6ff9984c6298ff0285cace6614bf8daa9f0070101b6c89899e22", + "translations": {}, + "hidden": [] + }, + { + "addon": <addons.autoexec>, + "label": "InsomniaIRC", + "autojoin": [ + "#chat" + ], + "nsautojoin": [], + "operjoin": [] + } + ] + } + } +}
\ No newline at end of file @@ -1,5 +1,6 @@ #!/usr/bin/python -from threading import Thread, Event, Lock +from threading import Thread, Condition, currentThread +from threading import RLock as Lock import re import time import sys @@ -10,23 +11,51 @@ import platform import traceback import ssl import glob -import iqueue as Queue +from collections import deque, OrderedDict +import chardet +import codecs +import new +import inspect +import warnings +import random + +__all__ = ["Connection", "Channel", "ChanList", + "User", "UserList", "Config", "timestamp"] + + +def autodecode(s): + try: + return s.decode("utf8") + except UnicodeDecodeError: + # Attempt to figure encoding + detected = chardet.detect(s) + try: + return s.decode(detected['encoding']) + except UnicodeDecodeError: + return s.decode("utf8", "replace") -def timestamp(): - t = time.time() - ms = 1000*t%1000 - ymdhms = time.localtime(t) - tz = time.altzone if ymdhms.tm_isdst else time.timezone - sgn = "-" if tz >= 0 else "+" - return "%04d-%02d-%02d %02d:%02d:%02d.%03d%s%02d:%02d"%(ymdhms[:6]+(1000*t%1000, sgn, abs(tz)/3600, abs(tz)/60%60)) +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 @@ -34,1231 +63,3142 @@ class InvalidCharacter(BaseException): pass -class Connection(Thread): - def __init__(self, server, nick="ircbot", username="python", realname="Python IRC Library", passwd=None, port=None, ipv6=False, ssl=False, autoreconnect=True, log=sys.stderr, timeout=300, retrysleep=5, maxretries=15, onlogin=None): - self.__name__ = "pyIRC" - self.__version__ = "1.1" - self.__author__ = "Brian Sherson" - self.__date__ = "December 1, 2013" +class ConnectionTimedOut(BaseException): - if port is None: - self.port = 6667 if not ssl else 6697 - else: + """Raised when the connection times out during a blocked Channel.join() or Channel.part() call.""" + pass + + +class ConnectionClosed(BaseException): + pass + + +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 + + +class ChannelFull(BaseException): + pass + + +class InviteOnly(BaseException): + pass + + +class NotOnChannel(BaseException): + pass + + +class NoSuchChannel(BaseException): + pass + + +class BadChannelKey(BaseException): + pass + + +class BadChannelMask(BaseException): + pass + + +class TooManyChannels(BaseException): + pass + + +class Unavailable(BaseException): + pass + + +class Cbaned(BaseException): + pass + + +class ActionAlreadyRequested(BaseException): + pass + + +class OpersOnly(BaseException): + pass + + +class OperCreateOnly(BaseException): + pass + + +class SSLOnly(BaseException): + pass + + +class AlreadyJoined(BaseException): + pass + + +class AlreadyConnected(BaseException): + pass + + +class RegistrationRequired(BaseException): + pass + + +class RejoinDelay(BaseException): + pass + +_rfc1459casemapping = string.maketrans(string.ascii_uppercase + r'\[]~', + string.ascii_lowercase + r'|{}^').decode("ISO-8859-2") + +# The IRC RFC does not permit the first character in a nickname to be a +# numeral. However, this is not always adhered to. +_nickmatch = r"^[A-Za-z0-9\-\^\`\\\|\_\{\}\[\]]+$" + +_intmatch = r"^\d+$" +_chanmatch = r"^[%%s][^%s\s\n]*$" % re.escape("\x07,") +_targchanmatch = r"^([%%s]?)([%%s][^%s\s\n]*)$" % re.escape("\x07,") +_usermatch = r"^[A-Za-z0-9\-\^\`\\\|\_\{\}\[\]]+$" +_realnamematch = r"^[^\n]*$" +_ircmatch = r"^(?::(.+?)(?:!(.+?)@(.+?))?\s+)?([A-Za-z0-9]+?)\s*(?:\s+(.+?)(?:\s+(.+?))??)??(?:\s+:(.*))?$" +_ctcpmatch = "^\x01(.*?)(?:\\s+(.*?)\\s*)?\x01$" +_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")) +_maskmodeeventnames = dict(b=("Ban", "Unban"), e=( + "BanExcept", "UnbanExcept"), I=("Invite", "Uninvite")) + +exceptcodes = {489: SSLOnly, 384: Cbaned, 403: NoSuchChannel, 405: TooManyChannels, 442: NotOnChannel, 470: RedirectedJoin, 471: ChannelFull, 473: InviteOnly, 474: + BannedFromChannel, 475: BadChannelKey, 476: BadChannelMask, 520: OpersOnly, 437: Unavailable, 477: RegistrationRequired, 495: RejoinDelay, 530: OperCreateOnly} + + +def timestamp(): + t = time.time() + ms = 1000 * t % 1000 + ymdhms = time.localtime(t) + tz = time.altzone if ymdhms.tm_isdst else time.timezone + sgn = "-" if tz >= 0 else "+" + return "%04d-%02d-%02d %02d:%02d:%02d.%03d%s%02d:%02d" % (ymdhms[:6] + (1000 * t % 1000, sgn, abs(tz) / 3600, abs(tz) / 60 % 60)) + + +class Connection(object): + __doc__ = "Manages a connection to an IRC network. Includes support for addons." + __name__ = "pyIRC" + __version__ = "2.1" + __author__ = "Brian Sherson" + __date__ = "February 21, 2014" + + 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 type(nick) in (str, unicode): - self.nick = [nick] - elif type(nick) in (list, tuple): + 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'" + + if re.match(_realnamematch, realname): + self.realname = realname + else: + raise InvalidCharacter + + if re.match(_usermatch, username): + self.username = username + else: + raise InvalidCharacter + + if passwd == None or "\n" not in passwd: + self.passwd = passwd + else: + raise InvalidCharacter - self.realname = realname - self.username = username - self.passwd = passwd self.server = server - self.ssl = ssl - self.ipv6 = ipv6 + self.secure = secure + self.ipvers = ipvers if type(ipvers) == tuple else (ipvers,) + + self.protoctl = protoctl + + if type(autoreconnect) == bool: + self.autoreconnect = autoreconnect + else: + raise ValueError, "Invalid value for 'autoreconnect'" + + if isinstance(maxretries, (int, long)): + self.maxretries = maxretries + else: + raise ValueError, "Invalid value for 'maxretries'" + + if isinstance(timeout, (int, long)): + self.timeout = timeout + else: + raise ValueError, "Invalid value for 'timeout'" + + if isinstance(retrysleep, (int, long, float)) and retrysleep >= 0: + self.retrysleep = retrysleep + else: + raise ValueError, "Invalid value for 'retrysleep'" + + if type(quietpingpong) == bool: + self.quietpingpong = quietpingpong + 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: + raise ValueError, "Invalid value for 'pinginterval'" + + self._quitexpected = False + self.log = sys.stdout + + self.lock = Lock() - self.connected = False - self.registered = False - self.connection = None + self._loglock = Lock() + self._outlock = Lock() + self._sendline = Condition(self._outlock) + self._connecting = Condition(self.lock) + self._disconnecting = Condition(self.lock) + self._outgoing = deque() - self.autoreconnect = autoreconnect - self.maxretries = maxretries - self.timeout = timeout - self.retrysleep = retrysleep + self._sendhandlerthread = None + self._recvhandlerthread = None - self.quitexpected = False - self.log = log + # Initialize IRC environment variables + 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 = [] - ### Initialize IRC environment variables - self.motdgreet = None - self.motd = None - self.motdend = None + self._init() + for conf in addons: + try: + if type(conf) == dict: + self.addAddon(**conf) + else: + self.addAddon(conf) + except: + pass + + if autostart: + self.connect() + + def _init(self): + self.ipver = None + self.addr = None + self._connected = False + self._registered = False + self._connection = None + self._starttls = False + self.trynick = 0 + self.identity = None - self.users = [] - self.channels = [] + + self.serv = None + self.welcome = None + self.hostinfo = None + self.servcreated = None + self.servinfo = None + self.serv005 = None self.supports = {} + self.throttledata = [] + self.throttled = False + self.enabledcaps = [] + self.supportedcaps = [] + self._requestedcaps = [] + self._caplsrequested = False - self.lock = Lock() - self.loglock = Lock() - self.sendlock = Lock() - self.outgoing = Queue.Queue() - self.outgoingthread = None + @property + def motdgreet(self): + return self.identity.server.motdgreet - Thread.__init__(self) + @property + def motd(self): + return self.identity.server.motd + + @property + def motdend(self): + return self.identity.server.motdend + + @property + def connected(self): + return self._connected + + @property + def registered(self): + return self._registered def logwrite(self, *lines): - with self.loglock: + """logwrite(*lines) + + Writes one or more line to the log file, signed with a timestamp.""" + with self._loglock: ts = timestamp() for line in lines: - print >>self.log, "%s %s"%(ts, line) + print >>self.log, u"%s %s" % (ts, line) self.log.flush() - def logopen(self, filename): - with self.loglock: + 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 = open(filename, "a") - if type(self.log) == file and not self.log.closed: - print >>self.log, "%s ### Log file closed" % (ts) + newlog = codecs.open(filename, "a", encoding=encoding) + if isinstance(self.log, codecs.StreamReaderWriter) and not self.log.closed: if self.log not in (sys.stdout, sys.stderr): + print >>self.log, "%s ### Log file closed" % (ts) self.log.close() self.log = newlog print >>self.log, "%s ### Log file opened" % (ts) self.log.flush() - def event(self, method, modlist, exceptions=False, **params): - timestamp = reduce(lambda x, y: x+":"+y, [str(t).rjust(2, - "0") for t in time.localtime()[0:6]]) + # Used to call event handlers on all attached addons, when applicable. + def _event(self, addons, events, line=None, data=None, exceptions=False): handled = [] unhandled = [] errors = [] - for k, addon in enumerate(modlist): - if modlist.index(addon) < k: + for k, addon in enumerate(addons + [self]): + if addon in addons and addons.index(addon) < k: + # Duplicate continue - if method in dir(addon) and callable(getattr(addon, method)): + + if type(addon) == Config: + addon = addon.addon + + fellback = False # Switch this to True when a fallback is used so that we only call onOther once. + + # Iterate through all events. + for (method, args, fallback) in events: try: - getattr(addon, method)(self, **params) + f = getattr(addon, method) + 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 + + if type(f) == new.instancemethod: + argspec = inspect.getargspec(f.im_func) + else: + argspec = inspect.getargspec(f) + if argspec.keywords == None: + args = { + arg: val for arg, val in args.items() if arg in argspec.args} + try: + f(self, **args) except: + # print f, args exc, excmsg, tb = sys.exc_info() errors.append((addon, exc, excmsg, tb)) - self.logwrite(*["!!! Exception in addon %(addon)s" % vars()]+["!!! %s"%line for line in traceback.format_exc().split("\n")]) - print >>sys.stderr, "Exception in addon %(addon)s" % vars() - print >>sys.stderr, traceback.format_exc() + + 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) - else: - unhandled.append(addon) return (handled, unhandled, errors) - def addAddon(self, addon, trusted=False, **params): - if addon in self.addons: - raise BaseException("Addon already added.") + 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): + if fname in keys: + supportedargs = supported[fname] + elif re.match(r"^on(?:Send)?[A-Z]+$", fname): + supportedargs = ( + "line", "origin", "target", "targetprefix", "params", "extinfo") + elif re.match(r"^on\d{3}$", fname): + supportedargs = ( + "line", "origin", "target", "params", "extinfo") + else: + continue + func = getattr(addon, fname) + argspec = inspect.getargspec(func) + if type(func) == new.instancemethod: + funcargs = argspec.args[1:] + if argspec.defaults: + requiredargs = funcargs[:-len(argspec.defaults)] + else: + requiredargs = funcargs + contextarg = funcargs[0] + unsupported = [ + arg for arg in requiredargs[1:] if arg not in supportedargs] + if len(unsupported): + warnings.warn( + "Function '%s' requires unsupported arguments: %s" % + (func.__name__, ", ".join(unsupported)), AddonWarning) + self.logwrite( + "!!! AddonWarning: Function '%s' requires unsupported arguments: %s" % + (func.__name__, ", ".join(unsupported))) + if argspec.keywords == None: + unsupported = [ + arg for arg in supportedargs if arg not in funcargs[1:]] + if len(unsupported): + warnings.warn( + "Function '%s' does not accept supported arguments: %s" % + (func.__name__, ", ".join(unsupported)), AddonWarning) + self.logwrite( + "!!! AddonWarning: Function '%s' does not accept supported arguments: %s" % + (func.__name__, ", ".join(unsupported))) + + 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) + with self.lock: - self.event("onAddonAdd", [addon], exceptions=True, **params) - self.addons.append(addon) - if trusted: - self.trusted.append(addon) - - def insertAddon(self, index, addon, trusted=False, **params): - if addon in self.addons: - raise BaseException("Addon already added.") + 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)) + + def insertAddon(self, index, addon, **params): + """insertAddon(index, addon[, ...]) + + The 'list.insert' version of addAddon.""" + self.validateAddon(addon) + with self.lock: - self.event("onAddonAdd", [addon], exceptions=True, **params) - self.addons.insert(index, addon) - if trusted: - self.trusted.append(addon) + 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)) + + # 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) - def rmAddon(self, addon, **params): + Removes addon from self.addons.""" with self.lock: - self.addons.remove(addon) - self.event("onAddonRem", [addon], exceptions=True, **params) - if addon in self.trusted: - self.trusted.remove(addon) - - def run(self): - privmodeeventnames = dict(q=("Owner", "Deowner"), a=("Admin", "Deadmin"), o=("Op", "Deop"), h=("Halfop", "Dehalfop"), v=("Voice", "Devoice")) - maskmodeeventnames = dict(b=("Ban", "Unban"), e=( - "BanExcept", "UnbanExcept"), I=("Invite", "Uninvite")) - self.quitexpected = False - whoisstarted = False - nameslist = [] - wholist = [] - lists = {} - nameschan = None - server = self.server - if self.ipv6 and ":" in server: - server = "[%s]"%server - port = self.port + 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 hasattr(addon, "onAddonRem") and callable(addon.onAddonAdd): + 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: + ipvers = self.ipvers + + server = server if server else self.server + port = port if port else self.port + secure = secure if secure != None else self.secure + + with self._connecting: + if self.isAlive(): + if forcereconnect: + self.quit("Changing server...", blocking=True) + else: + raise AlreadyConnected + with self._sendline: + self._outgoing.clear() + self._recvhandlerthread = Thread(target=self._recvhandler, name="Receive Handler", kwargs=dict( + server=server, port=port, secure=secure, ipvers=ipvers)) + self._sendhandlerthread = Thread( + target=self._sendhandler, name="Send Handler") + self._recvhandlerthread.start() + self._sendhandlerthread.start() + if blocking: + self._connecting.wait() + if not self.connected: + 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 + + if hostname: + if ipver == socket.AF_INET6: + addrstr = "{hostname} ([{addr[0]}]:{addr[1]})".format( + **vars()) + else: + addrstr = "{hostname} ({addr[0]}:{addr[1]})".format( + **vars()) + else: + if ipver == socket.AF_INET6: + addrstr = "[{addr[0]}]:{addr[1]}".format(**vars()) + else: + addrstr = "{addr[0]}:{addr[1]}".format(**vars()) + self.logwrite( + "*** Attempting connection to {addrstr}.".format(**vars())) + self._event(self.getalladdons(), [ + ("onConnectAttempt", dict(), False)]) + try: + connection = socket.socket(ipver, socket.SOCK_STREAM) + if secure: + connection.settimeout(self.timeout) + self._connection = ssl.wrap_socket( + connection, cert_reqs=ssl.CERT_NONE) + else: + self._connection = connection + self._connection.settimeout(self.timeout) + self._connection.connect(addr) + except socket.error: + exc, excmsg, tb = sys.exc_info() + self.logwrite( + "*** Connection to {addrstr} failed: {excmsg}.".format(**vars())) + with self.lock: + self._event(self.getalladdons(), [ + ("onConnectFail", dict(exc=exc, excmsg=excmsg, tb=tb), False)]) + raise + else: + # Run onConnect on all addons to signal connection was established. + self._connected = True with self.lock: - self.event("onSessionOpen", self.addons+reduce(lambda x, y: x+y, [chan.addons for chan in self.channels], [])) + self._event( + self.getalladdons(), [("onConnect", dict(), False)]) + self.logwrite( + "*** Connection to {addrstr} established.".format(**vars())) + self.addr = addr + 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]: + self._connect(addr=addr, secure=secure, ipver=ipver) + else: + self._connect( + hostname=server, addr=addr, secure=secure, ipver=ipver) + except socket.error, msg: + if self._quitexpected: + sys.exit() + if msg.errno == 101: # Network is unreachable, will pass the exception on. + raise + if self.retrysleep > 0: + time.sleep(self.retrysleep) + if self._quitexpected: + sys.exit() + else: + return True + 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())) + else: + self.logwrite( + "*** Attempting to resolve {server}...".format(**vars())) - self.logwrite("### Log session started") + try: + addrs = socket.getaddrinfo( + server, port if port is not None else 6697 if self.secure else 6667, ipver) + except socket.gaierror, msg: + self.logwrite("*** Resolution failed: {msg}.".format(**vars())) + raise + + # Weed out duplicates + addrs = list( + set([sockaddr for family, socktype, proto, canonname, sockaddr in addrs if family == ipver])) + + n = len(addrs) + if n == 1: + addr = addrs[0] + self.logwrite( + "*** Name {server} resolves to {addr[0]}.".format(**vars())) + else: + self.logwrite( + "*** Name {server} resolves to {n} addresses, choosing one at random until success.".format(**vars())) + random.shuffle(addrs) - attempt = 1 - while True: # An entire connection lives within this while loop. When the connection fails, will try to reestablish, unless self.autoreconnect is set to False. - while True: # Enter retry loop - self.logwrite("*** Attempting connection to %(server)s:%(port)s." % vars()) + return self._tryaddrs(server, addrs, ipver, secure) - with self.lock: - self.event("onConnectAttempt", self.addons+reduce(lambda x, y: x+y, [chan.addons for chan in self.channels], [])) + 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) + except socket.gaierror, msg: + if msg.errno == -2: # Name or service not known. Again, just try next ipver. + continue + else: + raise + except socket.error, msg: + if msg.errno == 101: # Don't err out, just try next ipver. + continue + else: + raise + else: + if ret: + self.ipver = ipver + return True + 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! + if len(matches): + (origin, username, host, cmd, target, params, extinfo) = matches[0] + unhandled = [] + + if re.match(_intmatch, cmd): + cmd = int(cmd) # Code is a numerical response + else: + cmd = cmd.upper() - try: - if self.ssl: - s = socket.socket(socket.AF_INET6 if self.ipv6 else socket.AF_INET, socket.SOCK_STREAM) - s.settimeout(self.timeout) - self.connection = ssl.wrap_socket(s, cert_reqs=ssl.CERT_NONE) - else: - self.connection = socket.socket(socket.AF_INET6 if self.ipv6 else socket.AF_INET, socket.SOCK_STREAM) - self.connection.connect((self.server, self.port, 0, 0) if self.ipv6 else (self.server, self.port)) - except socket.error: - exc, excmsg, tb = sys.exc_info() - self.event("onConnectFail", self.addons+reduce(lambda x, y: x+y, [chan.addons for chan in self.channels], []), exc=exc, excmsg=excmsg, tb=tb) - self.logwrite("*** Connection to %(server)s:%(port)s failed: %(excmsg)s." % vars()) - else: - self.connected = True - self.connection.settimeout(self.timeout) + if cmd not in ("PING", "PONG") or not self.quietpingpong: + self.logwrite("<<< %s" % line) + + if origin == "" and cmd == "PING": + self.send(u"PONG :%s" % extinfo) - ### Setting up thread responsible for sending data back to IRC server. - self.outgoing._interrupted = False - self.outgoingthread = Outgoing(self) - self.outgoingthread.daemon = True - self.outgoingthread.start() + with self.lock: + data = dict(origin=origin, cmd=cmd, target=target, + targetprefix=None, params=params, extinfo=extinfo) + + if username and host and self._registered: + nickname = origin + origin = self.user(origin) + if origin.nick != nickname: + # Origin nickname case has changed + origin.user = nickname + if origin.username != username: + # Origin username has changed + origin.username = username + 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) + prefix = self.supports.get("PREFIX", _defaultprefix) + chantypes = self.supports.get("CHANTYPES", _defaultchantypes) + chanmatch = re.findall( + _targchanmatch % (re.escape(prefix[1]), re.escape(chantypes)), target) + + if chanmatch: + targetprefix, channame = chanmatch[0] + target = self.channel(channame) + if target.name != channame: + # Target channel name has changed case + 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 in ("PRIVMSG", "NOTICE", "MODE", "INVITE", "KILL") and self._registered: + targetprefix = "" + target = self.user(target) + + # Otherwise, target is just left as a string + else: + targetprefix = "" + + data = dict(origin=origin, cmd=cmd, target=target, + targetprefix=targetprefix, params=params, extinfo=extinfo) - ### Run onConnect on all addons to signal connection was established. - self.event("onConnect", self.addons+reduce(lambda x, y: x+y, [chan.addons for chan in self.channels], [])) - self.logwrite("*** Connection to %(server)s:%(port)s established." % vars()) - break + # Parse - if self.quitexpected: + # Takes the given data and runs it through a parse method to determine what addon methods should be called later, and prepares the arguments + # to be passed to each of these methods. + # This part does not update the IRC state. + parsename = ( + "parse%03d" if type(cmd) == int else "parse%s") % cmd + + # This is the case that there is a parse method specific to the + # given cmd. + if hasattr(self, parsename) and callable(getattr(self, parsename)): + parsemethod = getattr(self, parsename) + try: + ret = parsemethod( + origin, target, targetprefix, params, extinfo) + addons, events = ret if ret is not None else ( + self.addons, []) + except: + self.logerror( + u"There was an error in parsing the following line:", line) + return + else: + addons = self.addons + if type(cmd) == int: + events = [ + ("on%03d" % cmd, dict(line=line, origin=origin, target=target, params=params, extinfo=extinfo), True)] + else: + events = [ + ("on%s" % cmd.upper(), dict(line=line, origin=origin, target=target, targetprefix=targetprefix, params=params, extinfo=extinfo), 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, [("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, 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." + + try: + with self.lock: + self._event(self.getalladdons(), [ + ("onSessionOpen", dict(), False)]) + + self.logwrite("### Session started") + + ipvers = ipvers if type(ipvers) == tuple else (ipvers,) + + # Autoreconnect loop + while True: + attempt = 1 + + # Autoretry loop + while True: + servisip = False + for ipver in ipvers: # Check to see if address is a valid ip address instead of host name + try: + socket.inet_pton(ipver, server) + except socket.error: + continue # Not a valid ip address under this ipver. + # Is a valid ip address under this ipver. + if ipver == socket.AF_INET6: + self._tryaddrs( + server, [(server, port, 0, 0)], ipver, secure) + else: + ret = self._tryaddrs( + server, [(server, port)], ipver, secure) + servisip = True + break + # Otherwise, we assume server is a hostname + if not servisip: + ret = self._tryipvers(server, port, ipvers, secure) + if ret: + self.server = server + self.port = port + self.ipvers = ipvers + self.secure = secure + break + if self._quitexpected: sys.exit() - if attempt < self.maxretries or self.maxretries == -1: + if self.retrysleep > 0: time.sleep(self.retrysleep) - if self.quitexpected: + if self._quitexpected: + sys.exit() + if attempt < self.maxretries or self.maxretries < 0: + if self._quitexpected: sys.exit() attempt += 1 else: - self.logwrite("*** Maximum number of attempts reached. Giving up. (%(server)s:%(port)s)" % vars()) + self.logwrite( + "*** Maximum number of attempts reached. Giving up. (%(server)s:%(port)s)" % vars()) + with self._connecting: + self._connecting.notifyAll() sys.exit() - ### Connection succeeded + # Connection succeeded try: - ### Attempt initial registration. - nick = self.nick[0] - trynick = 0 - if self.passwd: - self.raw("PASS :%s" % self.passwd.split( - "\n")[0].rstrip()) - self.raw("NICK :%s" % nick.split("\n")[0].rstrip()) - self.raw("USER %s * * :%s" % (self.username.split("\n")[0].rstrip(), self.realname.split("\n")[0].rstrip())) - - ### Initialize buffers + pingreq = None + with self._sendline: + self._sendline.notify() + + # Attempt initial registration. + # nick=self.nick[0] + # if self.passwd: + #self.send(u"PASS %s" % self.passwd) + # self._trynick() + + # Initialize buffers linebuf = [] readbuf = "" while True: # Main loop of IRC connection. while len(linebuf) == 0: # Need Moar Data - read = self.connection.recv(512) - - ### If read was empty, connection is terminated. + read = self._connection.recv(512) + with self._sendline: + if pingreq and pingreq in self._outgoing: + self._outgoing.remove(pingreq) + pingreq = (time.time() + self.pinginterval, u"PING %s %s" % ( + (self.identity.nick, self.identity.server) if self.identity else ("*", self.server)), self) + self._outgoing.append(pingreq) + self._sendline.notify() + + # If read was empty, connection is terminated. if read == "": sys.exit() - ### If read was successful, parse away! + # If read was successful, parse away! readbuf += read lastlf = readbuf.rfind("\n") if lastlf >= 0: - linebuf.extend(string.split(readbuf[0:lastlf], - "\n")) - readbuf = readbuf[lastlf+1:] + linebuf.extend( + string.split(readbuf[0:lastlf], "\n")) + readbuf = readbuf[lastlf + 1:] - line = string.rstrip(linebuf.pop(0)) - - ### If received PING, then just pong back transparently. - ping = re.findall("^PING :?(.*)$", line) - if len(ping): - with self.lock: - self.connection.send("PONG :%s\n" % ping[0]) - continue - - self.logwrite("<<< %(line)s" % vars()) - - ### Attempts to match against pattern ":src cmd target params :extinfo" - matches = re.findall(r"^:(.+?)(?:!(.+?)@(.+?))?\s+(.+?)(?:\s+(.+?)(?:\s+(.+?))??)??(?:\s+:(.*))?$", line) - - ### We have a match! - if len(matches): - parsed = (origin, username, host, cmd, target, params, extinfo) = matches[0] - unhandled = [] - - if re.match("^\\d+$", cmd): - cmd = int(cmd) # Code is a numerical response - else: - cmd = cmd.upper() - - if not self.registered: - if type(cmd) == int and target != "*": # Registration complete! - with self.lock: - self.registered = True - self.identity = self.user(target) - self.serv = origin - self.event("onRegistered", self.addons+reduce(lambda x, y: x+y, [chan.addons for chan in self.channels], [])) - - elif cmd == 433 and target == "*": # Server reports nick taken, so we need to try another. - trynick += 1 - (q, s) = divmod(trynick, len(self.nick)) - nick = self.nick[s] - if q > 0: - nick += str(q) - self.raw("NICK :%s" % nick.split("\n")[0].rstrip()) - if not self.registered: # Registration is not yet complete - continue + line = linebuf.pop(0).rstrip("\r") + line = autodecode(line) + self._procrecvline(line) - if username and host: - nickname = origin - origin = self.user(origin) - if origin.nick != nickname: - ### Origin nickname has changed - origin.user = nickname - if origin.username != username: - ### Origin username has changed - origin.username = username - if origin.host != host: - ### Origin host has changed - origin.host = host - - chanmatch = re.findall(r"([%s]?)([%s]\S*)"%(re.escape(self.supports.get("PREFIX", ("ohv", "@%+"))[1]), re.escape(self.supports.get("CHANTYPES", "#"))), target) - if chanmatch: - targetprefix, channame = chanmatch[0] - target = self.channel(channame) - if target.name != channame: - ### Target channel name has changed - target.name = channame - elif len(target) and target[0] != "$" and cmd != "NICK": - targetprefix = "" - target = self.user(target) - - data = dict(origin=origin, cmd=cmd, target=target, targetprefix=targetprefix, params=params, extinfo=extinfo) - - ### Major codeblock here! Track IRC state. - ### Send line to addons first - with self.lock: - self.event("onRecv", self.addons, line=line, - **data) - if cmd == 1: - (handled, unhandled, exceptions) = self.event("onWelcome", self.addons+reduce(lambda x, y: x+y, [chan.addons for chan in self.channels], []), origin=origin, msg=extinfo) - self.welcome = extinfo # Welcome message - elif cmd == 2: - (handled, unhandled, exceptions) = self.event("onYourHost", self.addons+reduce(lambda x, y: x+y, [chan.addons for chan in self.channels], []), origin=origin, msg=extinfo) - self.hostinfo = extinfo # Your Host - elif cmd == 3: - (handled, unhandled, exceptions) = self.event("onServerCreated", self.addons+reduce(lambda x, y: x+y, [chan.addons for chan in self.channels], []), origin=origin, msg=extinfo) - self.servcreated = extinfo # Server Created - elif cmd == 4: - (handled, unhandled, exceptions) = self.event("onServInfo", self.addons+reduce(lambda x, y: x+y, [chan.addons for chan in self.channels], []), origin=origin, servinfo=params) - self.servinfo = params # What is this code? - elif cmd == 5: # Server Supports - support = dict(re.findall("([A-Za-z0-9]+)(?:=(\\S*))?", params)) - if "CHANMODES" in support: - support["CHANMODES"] = support["CHANMODES"].split(",") - if "PREFIX" in support: - matches = re.findall("\\((.*)\\)(.*)", support["PREFIX"]) - if matches: - support["PREFIX"] = matches[0] - else: - del support["PREFIX"] # Might as well delete the info if it doesn't match expected pattern - (handled, unhandled, exceptions) = self.event("onSupports", self.addons+reduce(lambda x, y: x+y, [chan.addons for chan in self.channels], []), origin=origin, supports=support) - self.supports.update(support) - if "serv005" in dir(self) and type(self.serv005) == list: - self.serv005.append(params) - else: - self.serv005 = [params] - elif cmd == 8: # Snomask - snomask = params.lstrip("+") - (handled, unhandled, exceptions) = self.event("onSnoMask", self.addons+reduce(lambda x, y: x+y, [chan.addons for chan in self.channels], []), origin=origin, snomask=snomask) - self.identity.snomask = snomask - if "s" not in self.identity.modes: - self.snomask = "" - elif cmd == 221: # User Modes - modes = (params if params else extinfo).lstrip("+") - (handled, unhandled, exceptions) = self.event("onUserModes", self.addons+reduce(lambda x, y: x+y, [chan.addons for chan in self.channels], []), origin=origin, snomask=modes) - self.identity.modes = modes - if "s" not in self.identity.modes: - self.snomask = "" - elif cmd == 251: # Net Stats - (handled, unhandled, exceptions) = self.event("onNetStats", self.addons, origin=origin, netstats=extinfo) - self.netstats = extinfo - elif cmd == 252: - opcount = int(params) - (handled, unhandled, exceptions) = self.event("onOpCount", self.addons, origin=origin, opcount=opcount) - self.opcount = opcount - elif cmd == 254: - chancount = int(params) - (handled, unhandled, exceptions) = self.event("onChanCount", self.addons, origin=origin, chancount=chancount) - self.chancount = chancount - - elif cmd == 305: # Returned from away status - (handled, unhandled, exceptions) = self.event("onReturn", self.addons, origin=origin, msg=extinfo) - self.identity.away = False - - elif cmd == 306: # Entered away status - (handled, unhandled, exceptions) = self.event("onAway", self.addons, origin=origin, msg=extinfo) - self.identity.away = True - - elif cmd == 311: # Start of WHOIS data - nickname, username, host, star = params.split() - user = self.user(nickname) - (handled, unhandled, exceptions) = self.event("onWhoisStart", self.addons, origin=origin, user=user, nickname=nickname, username=username, host=host, realname=extinfo) - user.nick = nickname - user.username = username - user.host = host - - elif cmd == 301: # Away Message - user = self.user(params) - (handled, unhandled, exceptions) = self.event("onWhoisAway", self.addons, origin=origin, user=user, nickname=params, awaymsg=extinfo) - user.away = True - user.awaymsg = extinfo - - elif cmd == 303: # ISON Reply - users = [self.user(user) for user in extinfo.split(" ")] - (handled, unhandled, exceptions) = self.event("onIsonReply", self.addons, origin=origin, isonusers=users) - - elif cmd == 307: # Is a registered nick - (handled, unhandled, exceptions) = self.event("onWhoisRegisteredNick", self.addons, origin=origin, user=self.user(params), nickname=params, msg=extinfo) - elif cmd == 378: # Connecting From - (handled, unhandled, exceptions) = self.event("onWhoisConnectingFrom", self.addons, origin=origin, user=self.user(params), nickname=params, msg=extinfo) - elif cmd == 319: # Channels - (handled, unhandled, exceptions) = self.event("onWhoisChannels", self.addons, origin=origin, user=self.user(params), nickname=params, chanlist=extinfo.split(" ")) - elif cmd == 310: # Availability - (handled, unhandled, exceptions) = self.event("onWhoisAvailability", self.addons, origin=origin, user=self.user(params), nickname=params, msg=extinfo) - elif cmd == 312: # Server - nickname, server = params.split(" ") - user = self.user(nickname) - (handled, unhandled, exceptions) = self.event("onWhoisServer", self.addons, origin=origin, user=user, nickname=nickname, server=server, servername=extinfo) - user.server = server - elif cmd == 313: # IRC Op - user = self.user(params) - (handled, unhandled, exceptions) = self.event("onWhoisOp", self.addons, origin=origin, user=user, nickname=params, msg=extinfo) - user.ircop = True - user.ircopmsg = extinfo - elif cmd == 317: # Idle and Signon times - nickname, idletime, signontime = params.split(" ") - user = self.user(nickname) - (handled, unhandled, exceptions) = self.event("onWhoisTimes", self.addons, origin=origin, user=user, nickname=nickname, idletime=int(idletime), signontime=int(signontime), msg=extinfo) - user.idlesince = int(time.time())-int(idletime) - user.signontime = int(signontime) - elif cmd == 671: # SSL - user = self.user(params) - (handled, unhandled, exceptions) = self.event("onWhoisSSL", self.addons, origin=origin, user=user, nickname=params, msg=extinfo) - user.ssl = True - elif cmd == 379: # User modes - (handled, unhandled, exceptions) = self.event("onWhoisModes", self.addons, origin=origin, user=self.user(params), nickname=params, msg=extinfo) - elif cmd == 330: # Logged in as - nickname, loggedinas = params.split(" ") - user = self.user(nickname) - (handled, unhandled, exceptions) = self.event("onWhoisLoggedInAs", self.addons, origin=origin, user=user, nickname=nickname, loggedinas=loggedinas, msg=extinfo) - user.loggedinas = loggedinas - elif cmd == 318: # End of WHOIS - (handled, unhandled, exceptions) = self.event("onWhoisEnd", self.addons, origin=origin, user=self.user(params), nickname=params, msg=extinfo) - - elif cmd == 321: # Start LIST - (handled, unhandled, exceptions) = self.event("onListStart", self.addons, origin=origin, params=params, extinfo=extinfo) - elif cmd == 322: # LIST item - (chan, pop) = params.split(" ", 1) - (handled, unhandled, exceptions) = self.event("onListEntry", self.addons, origin=origin, channel=self.channel(chan), population=int(pop), extinfo=extinfo) - elif cmd == 323: # End of LIST - (handled, unhandled, exceptions) = self.event("onListEnd", self.addons, origin=origin, endmsg=extinfo) - - elif cmd == 324: # Channel Modes - modeparams = params.split() - channame = modeparams.pop(0) - channel = self.channel(channame) - self.event("onRecv", channel.addons, line=line, **data) - if channel.name != channame: - channel.name = channame # Server seems to have changed the idea of the case of the channel name - setmodes = modeparams.pop(0) - modedelta = [] - for mode in setmodes: - if mode == "+": - continue - elif mode in self.supports["CHANMODES"][2]: - param = modeparams.pop(0) - modedelta.append(("+%s"%mode, param)) - elif mode in self.supports["CHANMODES"][3]: - modedelta.append(("+%s"%mode, None)) - (handled, unhandled, exceptions) = self.event("onChannelModes", self.addons+channel.addons, channel=channel, modedelta=modedelta) - for ((modeset, mode), param) in modedelta: - if mode in self.supports["CHANMODES"][2]: - channel.modes[mode] = param - elif mode in self.supports["CHANMODES"][3]: - channel.modes[mode] = True - - elif cmd == 329: # Channel created - channame, created = params.split() - created = int(created) - channel = self.channel(channame) - self.event("onRecv", channel.addons, line=line, **data) - (handled, unhandled, exceptions) = self.event("onChanCreated", self.addons+channel.addons, channel=channel, created=created) - channel.created = int(created) - - elif cmd == 332: # Channel Topic - channame = params - channel = self.channel(channame) - self.event("onRecv", channel.addons, line=line, **data) - (handled, unhandled, exceptions) = self.event("onTopic", self.addons+channel.addons, origin=origin, channel=channel, topic=extinfo) - channel.topic = extinfo - - elif cmd == 333: # Channel Topic info - (channame, nick, dt) = params.split() - channel = self.channel(channame) - self.event("onRecv", channel.addons, line=line, **data) - (handled, unhandled, exceptions) = self.event("onTopicInfo", self.addons+channel.addons, origin=origin, channel=channel, topicsetby=nick, topictime=int(dt)) - channel.topicsetby = nick - channel.topictime = int(dt) - - elif cmd == 352: # WHO reply - (channame, username, host, serv, nick, flags) = params.split() - try: - (hops, realname) = extinfo.split(" ", 1) - except ValueError: - hops = extinfo - realname = None - - if channame[0] in self.supports.get("CHANTYPES", "#"): - channel = self.channel(channame) - else: - channel = None - - user = self.user(nick) - - self.event("onRecv", channel.addons, line=line, **data) - (handled, unhandled, exceptions) = self.event("onWhoEntry", self.addons+channel.addons, origin=origin, channel=channel, user=user, channame=channame, username=username, host=host, serv=serv, nick=nick, flags=flags, hops=int(hops), realname=realname) - user.hops = hops - user.realname = realname - user.username = username - user.host = host - user.server = serv - user.away = "G" in flags - user.ircop = "*" in flags - if type(channel) == Channel: - if user not in channel.users: - channel.users.append(user) - if channel not in user.channels: - user.channels.append(channel) - for (mode, prefix) in zip(*self.supports["PREFIX"]): - if prefix in flags: - if mode in channel.modes.keys() and user not in channel.modes[mode]: - channel.modes[mode].append(user) - elif mode not in channel.modes.keys(): - channel.modes[mode] = [user] - - elif cmd == 315: # End of WHO reply - (handled, unhandled, exceptions) = self.event("onWhoEnd", self.addons+channel.addons, origin=origin, param=params, endmsg=extinfo) - - elif cmd == 353: # NAMES reply - (flag, channame) = params.split() - channel = self.channel(channame) - self.event("onRecv", channel.addons, line=line, **data) - - if "PREFIX" in self.supports: - names = re.findall(r"([%s]*)([^@!\s]+)(?:!(\S+)@(\S+))?"%re.escape(self.supports["PREFIX"][1]), extinfo) - else: - names = re.findall(r"()([^@!\s]+)(?:!(\S+)@(\S+))?", extinfo) # Still put it into tuple form for compatibility in the next structure - (handled, unhandled, exceptions) = self.event("onNames", self.addons+channel.addons, origin=origin, channel=channel, flag=flag, channame=channame, nameslist=names) - - for (symbs, nick, username, host) in names: - user = self.user(nick) - if user.nick != nick: - user.nick = nick - if username and user.username != username: - user.username = username - if host and user.host != host: - user.host = host - with channel.lock: - if channel not in user.channels: - user.channels.append(channel) - if user not in channel.users: - channel.users.append(user) - if "PREFIX" in self.supports: - for symb in symbs: - mode = self.supports["PREFIX"][0][self.supports["PREFIX"][1].index(symb)] - if mode not in channel.modes: - channel.modes[mode] = [user] - elif user not in channel.modes[mode]: - channel.modes[mode].append(user) - - elif cmd == 366: # End of NAMES reply - channel = self.channel(params) - (handled, unhandled, exceptions) = self.event("onNamesEnd", self.addons+channel.addons, origin=origin, channel=channel, channame=params, endmsg=extinfo) - - elif cmd == 372: # MOTD line - (handled, unhandled, exceptions) = self.event("onMOTDLine", self.addons, origin=origin, motdline=extinfo) - self.motd.append(extinfo) - elif cmd == 375: # Begin MOTD - (handled, unhandled, exceptions) = self.event("onMOTDStart", self.addons, origin=origin, motdgreet=extinfo) - self.motdgreet = extinfo - self.motd = [] - elif cmd == 376: - (handled, unhandled, exceptions) = self.event("onMOTDEnd", self.addons, origin=origin, motdend=extinfo) - self.motdend = extinfo # End of MOTD - - elif cmd == 386 and "q" in self.supports["PREFIX"][0]: # Channel Owner (Unreal) - (channame, owner) = params.split() - channel = self.channel(channame) - self.event("onRecv", channel.addons, line=line, **data) - if channel.name != channame: - channel.name = channame # Server seems to have changed the idea of the case of the channel name - user = self.user(owner) - if user.nick != owner: - user.nick = owner - if "q" in channel.modes: - if user not in channel.modes["q"]: - channel.modes["q"].append(user) - else: - channel.modes["q"] = [user] - - elif cmd == 388 and "a" in self.supports["PREFIX"][0]: # Channel Admin (Unreal) - (channame, admin) = params.split() - channel = self.channel(channame) - self.event("onRecv", channel.addons, line=line, **data) - if channel.name != channame: - channel.name = channame # Server seems to have changed the idea of the case of the channel name - user = self.user(admin) - if user.nick != admin: - user.nick = admin - if "a" in channel.modes: - if user not in channel.modes["a"]: - channel.modes["a"].append(user) - else: - channel.modes["a"] = [user] - - elif cmd == "NICK": - newnick = extinfo if len(extinfo) else target - - addons = reduce(lambda x, y: x+y, [chan.addons for chan in origin.channels], []) - self.event("onRecv", addons, line=line, **data) - (handled, unhandled, exceptions) = self.event("onNickChange", self.addons+addons, user=origin, newnick=newnick) - if origin == self.identity: - (handled, unhandled, exceptions) = self.event("onMeNickChange", self.addons+addons, newnick=newnick) - - for u in self.users: - if u.nick.lower() == newnick.lower(): - self.users.remove(u) # 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 u in channel.users: - channel.users.remove(u) - origin.nick = newnick - - elif cmd == "JOIN": - channel = target if type(target) == Channel else self.channel(extinfo) - self.event("onRecv", channel.addons, line=line, **data) - (handled, unhandled, exceptions) = self.event("onJoin", self.addons+channel.addons, user=origin, channel=channel) - - if origin == self.identity: # This means the bot is entering the room, - # and will reset all the channel data, on the assumption that such data may have changed. - # Also, the bot must request modes - channel.topic = "" - channel.topicmod = "" - channel.modes = {} - channel.users = [] - self.event("onMeJoin", self.addons+channel.addons, channel=channel) - self.raw("MODE %s" % channel.name) - self.raw("WHO %s" % channel.name) - if "CHANMODES" in self.supports.keys(): - self.raw("MODE %s :%s" % (channel.name, self.supports["CHANMODES"][0])) - - if channel not in origin.channels: - origin.channels.append(channel) - if origin not in channel.users: - channel.users.append(origin) - - elif cmd == "KICK": - kicked = self.user(params) - if kicked.nick != params: - kicked.nick = params - - self.event("onRecv", target.addons, line=line, **data) - if origin == self.identity: - self.event("onMeKick", self.addons+target.addons, channel=target, kicked=kicked, kickmsg=extinfo) - if kicked == self.identity: - self.event("onMeKicked", self.addons+target.addons, kicker=origin, channel=target, kickmsg=extinfo) - (handled, unhandled, exceptions) = self.event("onKick", self.addons+target.addons, kicker=origin, channel=target, kicked=kicked, kickmsg=extinfo) - - if target in kicked.channels: - kicked.channels.remove(target) - if kicked in target.users: - target.users.remove(kicked) - if "PREFIX" in self.supports: - for mode in self.supports["PREFIX"][0]: - if mode in target.modes and kicked in target.modes[mode]: - target.modes[mode].remove(kicked) - - elif cmd == "PART": - self.event("onRecv", target.addons, line=line, **data) - if origin == self.identity: - self.event("onMePart", self.addons+target.addons, channel=target, partmsg=extinfo) - (handled, unhandled, exceptions) = self.event("onPart", self.addons+target.addons, user=origin, channel=target, partmsg=extinfo) - - if target in origin.channels: - origin.channels.remove(target) - if origin in target.users: - target.users.remove(origin) - if "PREFIX" in self.supports: - for mode in self.supports["PREFIX"][0]: - if mode in target.modes and origin in target.modes[mode]: - target.modes[mode].remove(origin) - - elif cmd == "QUIT": - channels = list(origin.channels) - addons = reduce(lambda x, y: x+y, [chan.addons for chan in origin.channels], []) - self.event("onRecv", addons, line=line, **data) - (handled, unhandled, exceptions) = self.event("onQuit", self.addons+addons, user=origin, quitmsg=extinfo) - for channel in origin.channels: - with channel.lock: - if origin in channel.users: - channel.users.remove(origin) - if "PREFIX" in self.supports: - for mode in self.supports["PREFIX"][0]: - if mode in channel.modes and origin in channel.modes[mode]: - channel.modes[mode].remove(origin) - origin.channels = [] - - elif cmd == "MODE": - if type(target) == Channel: - self.event("onRecv", target.addons, line=line, **data) - modedelta = [] - modeparams = params.split() - setmodes = modeparams.pop(0) - modeset = "+" - for mode in setmodes: - if mode in "+-": - modeset = mode - else: - if mode in self.supports["CHANMODES"][0]+self.supports["CHANMODES"][1]: - param = modeparams.pop(0) - modedelta.append(("%s%s"%(modeset, mode), param)) - if mode in maskmodeeventnames.keys(): - if modeset == "+": - eventname = maskmodeeventnames[mode][0] - if modeset == "-": - eventname = maskmodeeventnames[mode][1] - matchesbot = glob.fnmatch.fnmatch("%s!%s@%s".lower()%(self.identity.nick, self.identity.username, self.identity.host), param.lower()) - self.event("on%s"%eventname, self.addons+target.addons, user=origin, channel=target, banmask=param) - if matchesbot: - self.event("onMe%s"%eventname, self.addons+target.addons, user=origin, channel=target, banmask=param) - elif mode in self.supports["CHANMODES"][2]: - if modeset == "+": - param = modeparams.pop(0) - modedelta.append(("%s%s"%(modeset, mode), param)) - else: - modedelta.append(("%s%s"%(modeset, mode), None)) - elif mode in self.supports["CHANMODES"][3]: - modedelta.append(("%s%s"%(modeset, mode), None)) - elif "PREFIX" in self.supports and mode in self.supports["PREFIX"][0]: - modenick = modeparams.pop(0) - modeuser = self.user(modenick) - if mode in privmodeeventnames.keys(): - if modeset == "+": - eventname = privmodeeventnames[mode][0] - if modeset == "-": - eventname = privmodeeventnames[mode][1] - self.event("on%s"%eventname, self.addons+target.addons, user=origin, channel=target, modeuser=modeuser) - if modeuser == self.identity: - self.event("onMe%s"%eventname, self.addons+target.addons, user=origin, channel=target) - modedelta.append(("%s%s"%(modeset, mode), modeuser)) - (handled, unhandled, exceptions) = self.event("onChanModeSet", self.addons+target.addons, user=origin, channel=target, modedelta=modedelta) - with target.lock: - for ((modeset, mode), param) in modedelta: - if mode in self.supports["CHANMODES"][0]: - if modeset == "+": - if mode in target.modes: - if param.lower() not in [mask.lower() for (mask, setby, settime) in target.modes[mode]]: - target.modes[mode].append((param, origin, int(time.time()))) - else: - target.modes[mode] = [(param, origin, int(time.time()))] - else: - if mode in target.modes.keys(): - if mode == "b": # Inspircd mode is case insentive when unsetting the mode - masks = [mask.lower() for (mask, setby, settime) in target.modes[mode]] - if param.lower() in masks: - index = masks.index(param.lower()) - #print "Index: %d"%index - del target.modes[mode][index] - else: - masks = [mask for (mask, setby, settime) in target.modes[mode]] - if param in masks: - index = masks.index(param) - del target.modes[mode][index] - elif mode in self.supports["CHANMODES"][1]: - if modeset == "+": - target.modes[mode] = param - else: - target.modes[mode] = None - elif mode in self.supports["CHANMODES"][2]: - if modeset == "+": - target.modes[mode] = param - else: - target.modes[mode] = None - elif mode in self.supports["CHANMODES"][3]: - if modeset == "+": - target.modes[mode] = True - else: - target.modes[mode] = False - elif "PREFIX" in self.supports and mode in self.supports["PREFIX"][0]: - if modeset == "+": - if mode in target.modes and param not in target.modes[mode]: - target.modes[mode].append(param) - if mode not in target.modes: - target.modes[mode] = [param] - elif mode in target.modes and param in target.modes[mode]: - target.modes[mode].remove(param) - elif type(target) == User: - modeparams = (params if params else extinfo).split() - setmodes = modeparams.pop(0) - modeset = "+" - for mode in setmodes: - if mode in "+-": - modeset = mode - continue - if modeset == "+": - if mode not in target.modes: - target.modes += mode - if mode == "s" and len(modeparams): - snomask = modeparams.pop(0) - for snomode in snomask: - if snomode in "+-": - snomodeset = snomode - continue - if snomodeset == "+" and snomode not in target.snomask: - target.snomask += snomode - if snomodeset == "-" and snomode in target.snomask: - target.snomask = target.snomask.replace(snomode, "") - if modeset == "-": - if mode in target.modes: - target.modes = target.modes.replace(mode, "") - if mode == "s": - target.snomask = "" - - elif cmd == "TOPIC": - self.event("onRecv", target.addons, line=line, **data) - (handled, unhandled, exceptions) = self.event("onTopicSet", self.addons+target.addons, user=origin, channel=target, topic=extinfo) - - with target.lock: - target.topic = extinfo - target.topicsetby = origin - target.topictime = int(time.time()) - - elif cmd == "INVITE": - channel = self.channel(extinfo if extinfo else params) - self.event("onRecv", channel.addons, line=line, **data) - (handled, unhandled, exceptions) = self.event("onInvite", self.addons+channel.addons, user=origin, channel=channel) - - elif cmd == "PRIVMSG": - if type(target) == Channel: - self.event("onRecv", target.addons, line=line, **data) - - ### CTCP handling - ctcp = re.findall("^\x01(.*?)(?:\\s+(.*?)\\s*)?\x01$", extinfo) - if ctcp: - (ctcptype, ext) = ctcp[0] - if ctcptype.upper() == "ACTION": - if type(target) == Channel: - (handled, unhandled, exceptions) = self.event("onChanAction", self.addons+target.addons, user=origin, channel=target, targetprefix=targetprefix, action=ext) - elif target == self.identity: - (handled, unhandled, exceptions) = self.event("onPrivAction", self.addons, user=origin, action=ext) - else: - if type(target) == Channel: - (handled, unhandled, exceptions) = self.event("onChanCTCP", self.addons+target.addons, user=origin, channel=target, targetprefix=targetprefix, ctcptype=ctcptype, params=ext) - elif target == self.identity: - (handled, unhandled, exceptions) = self.event("onPrivCTCP", self.addons, user=origin, ctcptype=ctcptype, params=ext) - if ctcptype.upper() == "VERSION": - origin.ctcpreply("VERSION", self.ctcpversion()) - if ctcptype.upper() == "TIME": - tformat = time.ctime() - tz = time.tzname[0] - origin.ctcpreply("TIME", "%(tformat)s %(tz)s" % vars()) - if ctcptype.upper() == "PING": - origin.ctcpreply("PING", "%(ext)s" % vars()) - if ctcptype.upper() == "FINGER": - origin.ctcpreply("FINGER", "%(ext)s" % vars()) - else: - if type(target) == Channel: - (handled, unhandled, exceptions) = self.event("onChanMsg", self.addons+target.addons, user=origin, channel=target, targetprefix=targetprefix, msg=extinfo) - elif target == self.identity: - (handled, unhandled, exceptions) = self.event("onPrivMsg", self.addons, user=origin, msg=extinfo) - - elif cmd == "NOTICE": - if type(target) == Channel: - self.event("onRecv", target.addons, line=line, **data) - - ### CTCP handling - ctcp = re.findall("^\x01(.*?)(?:\\s+(.*?)\\s*)?\x01$", extinfo) - if ctcp and target == self.identity: - (ctcptype, ext) = ctcp[0] - (handled, unhandled, exceptions) = self.event("onCTCPReply", self.addons, origin=origin, ctcptype=ctcptype, params=ext) - else: - if type(target) == Channel: - (handled, unhandled, exceptions) = self.event("onChanNotice", self.addons+target.addons, origin=origin, channel=target, targetprefix=targetprefix, msg=extinfo) - elif target == self.identity: - (handled, unhandled, exceptions) = self.event("onPrivNotice", self.addons, origin=origin, msg=extinfo) - - elif cmd == 367: # Channel Ban list - (channame, mask, setby, settime) = params.split() - channel = self.channel(channame) - self.event("onRecv", channel.addons, line=line, **data) - (handled, unhandled, exceptions) = self.event("onBanListEntry", self.addons+channel.addons, origin=origin, channel=channel, mask=mask, setby=setby, settime=int(settime)) - if "b" in channel.modes.keys(): - if mask.lower() not in [m.lower() for (m, s, t) in channel.modes["b"]]: - channel.modes["b"].append((mask, setby, int(settime))) - else: - channel.modes["b"] = [(mask, setby, int(settime))] - elif cmd == 368: - channel = self.channel(params) - self.event("onRecv", channel.addons, line=line, **data) - (handled, unhandled, exceptions) = self.event("onBanListEnd", self.addons+channel.addons, origin=origin, channel=channel, endmsg=extinfo) - - elif cmd == 346: # Channel Invite list - (channame, mask, setby, settime) = params.split() - channel = self.channel(channame) - self.event("onRecv", channel.addons, line=line, **data) - (handled, unhandled, exceptions) = self.event("onInviteListEntry", self.addons+channel.addons, origin=origin, channel=channel, mask=mask, setby=setby, settime=int(settime)) - if "I" in channel.modes.keys(): - if mask.lower() not in [m.lower() for (m, s, t) in channel.modes["I"]]: - channel.modes["I"].append((mask, setby, int(settime))) - else: - channel.modes["I"] = [(mask, setby, int(settime))] - elif cmd == 347: - channel = self.channel(params) - self.event("onRecv", channel.addons, line=line, **data) - (handled, unhandled, exceptions) = self.event("onInviteListEnd", self.addons+channel.addons, origin=origin, channel=channel, endmsg=extinfo) - - elif cmd == 348: # Channel Ban Exception list - (channame, mask, setby, settime) = params.split() - channel = self.channel(channame) - self.event("onRecv", channel.addons, line=line, **data) - (handled, unhandled, exceptions) = self.event("onBanExceptListEntry", self.addons+channel.addons, origin=origin, channel=channel, mask=mask, setby=setby, settime=int(settime)) - if "e" in channel.modes.keys(): - if mask.lower() not in [m.lower() for (m, s, t) in channel.modes["e"]]: - channel.modes["e"].append((mask, setby, int(settime))) - else: - channel.modes["e"] = [(mask, setby, int(settime))] - elif cmd == 349: - channel = self.channel(params) - self.event("onRecv", channel.addons, line=line, **data) - (handled, unhandled, exceptions) = self.event("onBanExceptListEnd", self.addons+channel.addons, origin=origin, channel=channel, endmsg=extinfo) - - elif cmd == 910: # Channel Access List - (channame, mask, setby, settime) = params.split() - channel = self.channel(channame) - self.event("onRecv", channel.addons, line=line, **data) - (handled, unhandled, exceptions) = self.event("onAccessListEntry", self.addons+channel.addons, origin=origin, channel=channel, mask=mask, setby=setby, settime=int(settime)) - if "w" in channel.modes.keys(): - if mask.lower() not in [m.lower() for (m, s, t) in channel.modes["b"]]: - channel.modes["w"].append((mask, setby, int(settime))) - else: - channel.modes["w"] = [(mask, setby, int(settime))] - elif cmd == 911: - channel = self.channel(params) - self.event("onRecv", channel.addons, line=line, **data) - (handled, unhandled, exceptions) = self.event("onAccessListEnd", self.addons+channel.addons, origin=origin, channel=channel, endmsg=extinfo) - - elif cmd == 941: # Spam Filter list - (channame, mask, setby, settime) = params.split() - channel = self.channel(channame) - self.event("onRecv", channel.addons, line=line, **data) - (handled, unhandled, exceptions) = self.event("onSpamfilterListEntry", self.addons+channel.addons, origin=origin, channel=channel, mask=mask, setby=setby, settime=int(settime)) - if "g" in channel.modes.keys(): - if mask.lower() not in [m.lower() for (m, s, t) in channel.modes["g"]]: - channel.modes["g"].append((mask, setby, int(settime))) - else: - channel.modes["g"] = [(mask, setby, int(settime))] - elif cmd == 940: - channel = self.channel(params) - self.event("onRecv", channel.addons, line=line, **data) - (handled, unhandled, exceptions) = self.event("onSpamfilterListEnd", self.addons+channel.addons, origin=origin, channel=channel, endmsg=extinfo) - - elif cmd == 954: # Channel exemptchanops list - (channame, mask, setby, settime) = params.split() - channel = self.channel(channame) - self.event("onRecv", channel.addons, line=line, **data) - (handled, unhandled, exceptions) = self.event("onExemptChanOpsListEntry", self.addons+channel.addons, origin=origin, channel=channel, mask=mask, setby=setby, settime=int(settime)) - if "X" in channel.modes.keys(): - if mask.lower() not in [m.lower() for (m, s, t) in channel.modes["X"]]: - channel.modes["X"].append((mask, setby, int(settime))) - else: - channel.modes["X"] = [(mask, setby, int(settime))] - elif cmd == 953: - channel = self.channel(params) - self.event("onRecv", channel.addons, line=line, **data) - (handled, unhandled, exceptions) = self.event("onExemptChanOpsListEnd", self.addons+channel.addons, origin=origin, channel=channel, endmsg=extinfo) - - elif cmd == 728: # Channel quiet list - (channame, modechar, mask, setby, settime) = params.split() - channel = self.channel(channame) - self.event("onRecv", channel.addons, line=line, **data) - (handled, unhandled, exceptions) = self.event("onQuietListEntry", self.addons+channel.addons, origin=origin, channel=channel, modechar=modechar, mask=mask, setby=setby, settime=int(settime)) - if "q" in channel.modes.keys(): - if mask.lower() not in [m.lower() for (m, s, t) in channel.modes["q"]]: - channel.modes["q"].append((mask, setby, int(settime))) - else: - channel.modes["q"] = [(mask, setby, int(settime))] - elif cmd == 729: - channame, modechar = params.split() - channel = self.channel(channame) - self.event("onRecv", channel.addons, line=line, **data) - (handled, unhandled, exceptions) = self.event("onQuietListEnd", self.addons+channel.addons, channel=channel, endmsg=extinfo) - - elif cmd in (495, 384, 385, 386, 468, 470, 366, 315, 482, 484, 953, 368, 482, 349, 940, 911, 489, 490, 492, 520, 530): # Channels which appear in params - for param in params.split(): - if len(param) and param[0] in self.supports["CHANTYPES"]: - channel = self.channel(param) - self.event("onRecv", channel.addons, line=line, **data) - - elif type(cmd) == int: - (handled, unhandled, exceptions) = self.event("on%03d"%cmd, self.addons, line=line, origin=origin, target=target, params=params, extinfo=extinfo) - else: - (handled, unhandled, exceptions) = self.event("on%s"%cmd, self.addons, line=line, origin=origin, cmd=cmd, target=target, params=params, extinfo=extinfo) - - self.event("onUnhandled", unhandled, line=line, origin=origin, cmd=cmd, target=target, params=params, extinfo=extinfo) - - else: # Line does NOT match ":origin cmd target params :extinfo" - self.event("onRecv", self.addons, line=line) except SystemExit: # Connection lost normally. pass + except socket.error: # Connection lost due to either ping timeout or connection reset by peer. Not a fatal error. exc, excmsg, tb = sys.exc_info() - self.logwrite("*** Connection to %(server)s:%(port)s failed: %(excmsg)s." % vars()) + with self.lock: + self.logwrite( + "*** Connection to {self:uri} failed: {excmsg}.".format(**vars())) + self._event(self.getalladdons(), [ + ("onConnectFail", dict(exc=exc, excmsg=excmsg, tb=tb), False)]) + except: # Unknown exception, treated as FATAL. Try to quit IRC and terminate thread with exception. - ### Quit with a (hopefully) useful quit message, or die trying. - self.quitexpected = True + # Quit with a (hopefully) useful quit message, or die + # trying. + self._quitexpected = True try: - self.quit("%s" % traceback.format_exc() - .rstrip().split("\n")[-1]) + self.quit( + "%s" % traceback.format_exc().rstrip().split("\n")[-1]) except: pass raise - finally: # Post-connection operations after connection is lost, and must be executed, even if exception occurred. - with self.lock: - (handled, unhandled, exceptions) = self.event("onDisconnect", self.addons+reduce(lambda x, y: x+y, [chan.addons for chan in self.channels], []), expected=self.quitexpected) - self.connected = False - self.registered = False - self.identity = None - ### Tell outgoing thread to quit. - self.outgoing.interrupt() + finally: # Post-connection operations after connection is lost, and must be executed, even if exception occurred. + with self._sendline: # Notify _outgoingthread that the connection has been terminated. + self._outgoing.clear() + self._sendline.notify() + with self._disconnecting: + self._disconnecting.notifyAll() + self._event(self.getalladdons(), [ + ("onDisconnect", dict(expected=self._quitexpected), False)]) - ### Wait until the outgoing thread dies. - if self.outgoingthread and self.outgoingthread.isAlive(): - self.outgoingthread.join() - self.outgoingthread = None + self._init() try: - self.connection.close() + self._connection.close() except: pass self.logwrite("*** Connection Terminated.") - if self.quitexpected or not self.autoreconnect: + if self._quitexpected or not self.autoreconnect: + self._quitexpected = False sys.exit() - ### If we make it to this point, then it is because connection was lost unexpectedly, and will attempt to reconnect if self.autoreconnect is True. - time.sleep(self.retrysleep) - except SystemExit: 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" % vars() - print >>sys.stderr, traceback.format_exc() + self.logerror(u"FATAL Exception") sys.exit() finally: - self.logwrite("### Log session ended") - (handled, unhandled, exceptions) = self.event("onSessionClose", self.addons+reduce(lambda x, y: x+y, [chan.addons for chan in self.channels], [])) - Thread.__init__(self) # Makes thread restartable + self.logwrite("### Session ended") + self._event(self.getalladdons(), [ + ("onSessionClose", dict(), False)]) + + # Tell _sendhandler to quit + with self._sendline: + self._outgoing.append("quit") + self._sendline.notify() + + 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. + # These methods can also be used to determine event support by invoking + # them with no parameters. This allows for addition of event supports. + # Each is expected to return a tuple (addons, [(method, args, fallback), ...]). + # 'addons' refers to the list of addons whose methods should be called. + # [(method, args, fallback), ...] is a list of methods and parameters to be called, as well as a flag to determine when a fallback is permitted. + # 'method' refers to the name of the method to be invoked in the addons + # 'args' is a dict of arguments that should be passed as parameters to event. + # '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)]) + + def parse002(self, origin=None, target=None, targetprefix=None, params=None, extinfo=None): + return (self.getalladdons(), [("onYourHost", dict(origin=origin, msg=extinfo), True)]) + + def parse003(self, origin=None, target=None, targetprefix=None, params=None, extinfo=None): + return (self.getalladdons(), [("onServerCreated", dict(origin=origin, msg=extinfo), True)]) + + def parse004(self, origin=None, target=None, targetprefix=None, params=None, extinfo=None): + return (self.getalladdons(), [("onServInfo", dict(origin=origin, servinfo=params), True)]) + + def parse005(self, origin=None, target=None, targetprefix=None, params=None, extinfo=None): # Server Supports + if origin == None: + return (None, [("onSupports", dict(origin=None, supports=None, msg=None), True)]) + support = dict(re.findall("([A-Za-z0-9]+)(?:=(\\S*))?", params)) + if support.has_key("CHANMODES"): + support["CHANMODES"] = support["CHANMODES"].split(",") + if support.has_key("PREFIX"): + matches = re.findall(_prefixmatch, support["PREFIX"]) + if matches: + support["PREFIX"] = matches[0] + else: + del support["PREFIX"] + # Might as well delete the info if it doesn't match + # expected pattern + return (self.getalladdons(), [("onSupports", dict(origin=origin, supports=support, msg=extinfo), True)]) + + def parse008(self, origin=None, target=None, targetprefix=None, params=None, extinfo=None): # Snomask + if origin == None: + return (None, [("onSnoMask", dict(origin=None, snomask=None), True)]) + snomask = params.lstrip("+") + return (self.getalladdons(), [("onSnoMask", dict(origin=origin, snomask=snomask), True)]) + + def parse221(self, origin=None, target=None, targetprefix=None, params=None, extinfo=None): # User Modes + if origin == None: + return (self.getalladdons(), [("onUserModes", dict(origin=None, modes=None), True)]) + modes = (params if params else extinfo).lstrip("+") + return (self.getalladdons(), [("onUserModes", dict(origin=origin, modes=modes), True)]) + + def parse251(self, origin=None, target=None, targetprefix=None, params=None, extinfo=None): # Net Stats + return (self.addons, [("onNetStats", dict(origin=origin, netstats=extinfo), True)]) + + def parse252(self, origin=None, target=None, targetprefix=None, params=None, extinfo=None): # Operator count + if origin == None: + return (None, [("onOpCount", dict(origin=None, opcount=None), True)]) + opcount = int(params) + return (self.addons, [("onOpCount", dict(origin=origin, opcount=opcount), True)]) + + def parse254(self, origin=None, target=None, targetprefix=None, params=None, extinfo=None): # Channel Count + if origin == None: + return (self.addons, [("onChanCount", dict(origin=None, chancount=None), True)]) + chancount = int(params) + 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(), [("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(), [("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: + return (None, [("onWhoisStart", dict(origin=None, user=None, nickname=None, username=None, host=None, realname=None), True)]) + nickname, username, host, star = params.split() + user = self.user(nickname) + return (self.addons, [("onWhoisStart", dict(origin=origin, user=user, nickname=nickname, username=username, host=host, realname=extinfo), True)]) + + def parse301(self, origin=None, target=None, targetprefix=None, params=None, extinfo=None): # Away Message + if origin == None: + return (None, [("onWhoisAway", dict(origin=None, user=None, nickname=None, awaymsg=None), True)]) + user = self.user(params) + return (self.addons, [("onWhoisAway", dict(origin=origin, user=user, nickname=params, awaymsg=extinfo), True)]) + + def parse303(self, origin=None, target=None, targetprefix=None, params=None, extinfo=None): # ISON Reply + if origin == None: + return (None, [("onIsonReply", dict(origin=None, isonusers=None), True)]) + users = [self.user(user) for user in extinfo.split(" ")] + return (self.addons, [("onIsonReply", dict(origin=origin, isonusers=users), True)]) + + def parse307(self, origin=None, target=None, targetprefix=None, params=None, extinfo=None): # Is a registered nick + if origin == None: + return (None, [("onWhoisRegisteredNick", dict(origin=None, user=None, nickname=None, msg=None), True)]) + return (self.addons, [("onWhoisRegisteredNick", dict(origin=origin, user=self.user(params), nickname=params, msg=extinfo), True)]) + + def parse378(self, origin=None, target=None, targetprefix=None, params=None, extinfo=None): # Connecting From + if origin == None: + return (None, [("onWhoisConnectingFrom", dict(origin=None, user=None, nickname=None, msg=None), True)]) + return (self.addons, [("onWhoisConnectingFrom", dict(origin=origin, user=self.user(params), nickname=params, msg=extinfo), True)]) + + def parse319(self, origin=None, target=None, targetprefix=None, params=None, extinfo=None): # Channels + if origin == None: + return (None, [("onWhoisChannels", dict(origin=None, user=None, nickname=None, chanlist=None), True)]) + return (self.addons, [("onWhoisChannels", dict(origin=origin, user=self.user(params), nickname=params, chanlist=extinfo.split(" ")), True)]) + + def parse310(self, origin=None, target=None, targetprefix=None, params=None, extinfo=None): # Availability + if origin == None: + return (None, [("onWhoisAvailability", dict(origin=None, user=None, nickname=None, msg=None), True)]) + return (self.addons, [("onWhoisAvailability", dict(origin=origin, user=self.user(params), nickname=params, msg=extinfo), True)]) + + def parse312(self, origin=None, target=None, targetprefix=None, params=None, extinfo=None): # Server + if origin == None: + 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 + if origin == None: + return (None, [("onWhoisOp", dict(origin=None, user=None, nickname=None, msg=None), True)]) + user = self.user(params) + return (self.addons, [("onWhoisOp", dict(origin=origin, user=user, nickname=params, msg=extinfo), True)]) + + def parse317(self, origin=None, target=None, targetprefix=None, params=None, extinfo=None): # Idle and Signon times + if origin == None: + return (None, [("onWhoisTimes", dict(origin=None, user=None, nickname=None, idletime=None, signontime=None, msg=None), True)]) + nickname, idletime, signontime = params.split(" ") + user = self.user(nickname) + return (self.addons, [("onWhoisTimes", dict(origin=origin, user=user, nickname=nickname, idletime=int(idletime), signontime=int(signontime), msg=extinfo), True)]) + + def parse671(self, origin=None, target=None, targetprefix=None, params=None, extinfo=None): # SSL + if origin == None: + return (None, [("onWhoisSSL", dict(origin=None, user=None, nickname=None, msg=None), True)]) + user = self.user(params) + return (self.addons, [("onWhoisSSL", dict(origin=origin, user=user, nickname=params, msg=extinfo), True)]) + + def parse379(self, origin=None, target=None, targetprefix=None, params=None, extinfo=None): # User modes + if origin == None: + return (None, [("onWhoisModes", dict(origin=None, user=None, nickname=None, msg=None), True)]) + return (self.addons, [("onWhoisModes", dict(origin=origin, user=self.user(params), nickname=params, msg=extinfo), True)]) + + def parse330(self, origin=None, target=None, targetprefix=None, params=None, extinfo=None): # Logged in as + if origin == None: + return (None, [("onWhoisLoggedInAs", dict(origin=None, user=None, nickname=None, loggedinas=None, msg=None), True)]) + nickname, loggedinas = params.split(" ") + user = self.user(nickname) + return (self.addons, [("onWhoisLoggedInAs", dict(origin=origin, user=user, nickname=nickname, loggedinas=loggedinas, msg=extinfo), True)]) + + def parse318(self, origin=None, target=None, targetprefix=None, params=None, extinfo=None): # End of WHOIS + if origin == None: + return (None, [("onWhoisEnd", dict(origin=None, user=None, nickname=None, msg=None), True)]) + try: + user = self.user(params) + except InvalidName: + user = params + 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 (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: + return (None, [("onListEntry", dict(origin=None, channel=None, population=None, extinfo=None), True)]) + (chan, pop) = params.split(" ", 1) + try: + return (self.addons, [("onListEntry", dict(origin=origin, channel=self.channel(chan), population=int(pop), extinfo=extinfo), True)]) + except: + 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 (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: + return (None, [("onChannelModes", dict(origin=None, channel=None, modedelta=None), True)]) + modeparams = params.split() + channame = modeparams.pop(0) + channel = self.channel(channame) + + chanmodes = self.supports.get("CHANMODES", _defaultchanmodes) + setmodes = modeparams.pop(0) + modedelta = [] + for mode in setmodes: + if mode == "+": + continue + elif mode in chanmodes[2]: + param = modeparams.pop(0) + modedelta.append(("+%s" % mode, param)) + elif mode in chanmodes[3]: + modedelta.append(("+%s" % mode, None)) + return (self.addons + channel.addons, [("onChannelModes", dict(origin=origin, channel=channel, modedelta=modedelta), True)]) + + def parse329(self, origin=None, target=None, targetprefix=None, params=None, extinfo=None): # Channel created + if origin == None: + return (None, [("onChanCreated", dict(origin=None, channel=None, created=None), True)]) + channame, created = params.split() + created = int(created) + channel = self.channel(channame) + return (self.addons + channel.addons, [("onChanCreated", dict(origin=origin, channel=channel, created=created), True)]) + + def parse332(self, origin=None, target=None, targetprefix=None, params=None, extinfo=None): # Channel Topic + if origin == None: + return (None, [("onTopic", dict(origin=None, channel=None, topic=None), True)]) + channame = params + channel = self.channel(channame) + return (self.addons + channel.addons, [("onTopic", dict(origin=origin, channel=channel, topic=extinfo), True)]) + + def parse333(self, origin=None, target=None, targetprefix=None, params=None, extinfo=None): # Channel Topic info + if origin == None: + return (None, [("onTopicInfo", dict(origin=None, channel=None, topicsetby=None, topictime=None), True)]) + (channame, nick, dt) = params.split() + channel = self.channel(channame) + return (self.addons + channel.addons, [("onTopicInfo", dict(origin=origin, channel=channel, topicsetby=nick, topictime=int(dt)), True)]) + + def parse352(self, origin=None, target=None, targetprefix=None, params=None, extinfo=None): # WHO reply + if origin == None: + return (None, [("onWhoEntry", dict(origin=None, channel=None, user=None, channame=None, username=None, host=None, serv=None, nick=None, flags=None, hops=None, realname=None), True)]) + (channame, username, host, serv, nick, flags) = params.split() + try: + (hops, realname) = extinfo.split(" ", 1) + except ValueError: + hops = extinfo + realname = None + + chantypes = self.supports.get("CHANTYPES", _defaultchantypes) + if re.match(_chanmatch % re.escape(chantypes), channame): + channel = self.channel(channame) + else: + 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)]) + else: + return (self.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)]) + + def parse315(self, origin=None, target=None, targetprefix=None, params=None, extinfo=None): # End of WHO reply + if origin == None: + return (None, [("onWhoEnd", dict(origin=None, param=None, endmsg=None), True)]) + chantypes = self.supports.get("CHANTYPES", _defaultchantypes) + if re.match(_chanmatch % re.escape(chantypes), params): + channel = self.channel(params) + return (self.addons + channel.addons, [("onWhoEnd", dict(origin=origin, param=params, endmsg=extinfo), True)]) + else: + return (self.addons, [("onWhoEnd", dict(origin=origin, param=params, endmsg=extinfo), True)]) + + def parse353(self, origin=None, target=None, targetprefix=None, params=None, extinfo=None): # NAMES reply + if origin == None: + return (None, [("onNames", dict(origin=None, channel=None, flag=None, channame=None, nameslist=None), True)]) + (flag, channame) = params.split() + channel = self.channel(channame) + + if self.supports.has_key("PREFIX"): + names = re.findall(r"([%s]*)([^@!\s]+)(?:!(\S+)@(\S+))?" % + re.escape(self.supports["PREFIX"][1]), extinfo) + else: + names = re.findall(r"()([^@!\s]+)(?:!(\S+)@(\S+))?", extinfo) + # Still put it into tuple form for compatibility + # in the next structure + return (self.addons + channel.addons, [("onNames", dict(origin=origin, channel=channel, flag=flag, channame=channame, nameslist=names), True)]) + + def parse366(self, origin=None, target=None, targetprefix=None, params=None, extinfo=None): # End of NAMES reply + if origin == None: + return (None, [("onNamesEnd", dict(origin=None, channel=None, channame=None, endmsg=None), True)]) + channel = self.channel(params) + return (self.addons + channel.addons, [("onNamesEnd", dict(origin=origin, channel=channel, channame=params, endmsg=extinfo), True)]) + + def parse372(self, origin=None, target=None, targetprefix=None, params=None, extinfo=None): # MOTD line + return (self.addons, [("onMOTDLine", dict(origin=origin, motdline=extinfo), True)]) + self.motd.append(extinfo) + + def parse375(self, origin=None, target=None, targetprefix=None, params=None, extinfo=None): # Begin MOTD + return (self.addons, [("onMOTDStart", dict(origin=origin, motdgreet=extinfo), True)]) + + 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 ([], []) + + if origin == None: + return (None, [ + ("onNickChange", dict(user=None, newnick=None), True), + ("onMeNickChange", dict(newnick=None), False) + ]) + + newnick = extinfo if len(extinfo) else target + + addons = reduce( + lambda x, y: x + y, [channel.addons for channel in origin.channels if self.identity in channel.users], []) + + if origin == self.identity: + return (self.addons + addons, [ + ("onNickChange", dict(user=origin, newnick=newnick), True), + ("onMeNickChange", dict(newnick=newnick), False) + ]) + return (self.addons + addons, [("onNickChange", dict(user=origin, newnick=newnick), True)]) + + def parseJOIN(self, origin=None, target=None, targetprefix=None, params=None, extinfo=None, outgoing=False): + if outgoing: + return ([], []) + + if origin == None: + return (None, [ + ("onMeJoin", dict(channel=None, loggedinas=None, realname=None), False), + ("onJoin", dict(user=None, channel=None, loggedinas=None, realname=None), True) + ]) + + if type(target) == Channel: + channel = target + else: + 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, 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, loggedinas=loggedinas, realname=realname), True)]) + + def parseKICK(self, origin=None, target=None, targetprefix=None, params=None, extinfo=None, outgoing=False): + if outgoing: + return ([], []) + if origin == None: + return (None, [ + ("onMeKick", dict(channel=None, kicked=None, kickmsg=None), True), + ("onMeKicked", dict( + kicker=None, channel=None, kickmsg=None), True), + ("onKick", dict(kicker=None, channel=None, kicked=None, kickmsg=None), True) + ]) + events = [] + if origin == self.identity: + events.append( + ("onMeKick", dict(channel=target, kicked=kicked, kickmsg=extinfo), False)) + + kicked = self.user(params) + if kicked.nick != params: + kicked.nick = params + + if kicked == self.identity: + events.append( + ("onMeKicked", dict(kicker=origin, channel=target, kickmsg=extinfo), False)) + + events.append( + ("onKick", dict(kicker=origin, channel=target, kicked=kicked, kickmsg=extinfo), True)) + return (self.addons + target.addons, events) + + if target in kicked.channels: + kicked.channels.remove(target) + if kicked in target.users: + target.users.remove(kicked) + if self.supports.has_key("PREFIX"): + for mode in self.supports["PREFIX"][0]: + if target.modes.has_key(mode) and kicked in target.modes[mode]: + target.modes[mode].remove(kicked) + + def parsePART(self, origin=None, target=None, targetprefix=None, params=None, extinfo=None, outgoing=False): + if outgoing: + return ([], []) + if origin == None: + return (None, [ + ("onMePart", dict(channel=None, partmsg=None), True), + ("onPart", dict(user=None, channel=None, partmsg=None), True) + ]) + if origin == self.identity: + return (self.addons + target.addons, [ + ("onMePart", dict(channel=target, partmsg=extinfo), False), + ("onPart", dict(user=origin, channel=target, partmsg=extinfo), True) + ]) + else: + return (self.addons + target.addons, [("onPart", dict(user=origin, channel=target, partmsg=extinfo), True)]) + + def parseQUIT(self, origin=None, target=None, targetprefix=None, params=None, extinfo=None, outgoing=False): + if outgoing: + return ([], []) + if origin == None: + return (None, [("onQuit", dict(user=None, quitmsg=None), True)]) + + # Include addons for channels that both user and bot are in + # simultaneously. + addons = reduce( + lambda x, y: x + y, [channel.addons for channel in origin.channels if self.identity in channel.users], []) + return (self.addons + addons, [("onQuit", dict(user=origin, quitmsg=extinfo), True)]) + + def parseMODE(self, origin=None, target=None, targetprefix=None, params=None, extinfo=None, outgoing=False): + if outgoing: + return ([], []) + if origin == None: + events = [ + ("onChanModeSet", dict( + user=None, channel=None, modedelta=None), True), + ("onUserModeSet", dict(origin=None, modedelta=None), True) + ] + for (mode, (setname, unsetname)) in _maskmodeeventnames.items(): + events.append( + ("on%s" % setname, dict(user=None, channel=None, banmask=None), False)) + events.append( + ("onMe%s" % setname, dict(user=None, channel=None, banmask=None), False)) + events.append( + ("on%s" % unsetname, dict(user=None, channel=None, banmask=None), False)) + events.append( + ("onMe%s" % unsetname, dict(user=None, channel=None, banmask=None), False)) + for (mode, (setname, unsetname)) in _privmodeeventnames.items(): + events.append( + ("on%s" % setname, dict(user=None, channel=None, modeuser=None), False)) + events.append( + ("onMe%s" % setname, dict(user=None, channel=None), False)) + events.append( + ("on%s" % unsetname, dict(user=None, channel=None, banmask=None), False)) + events.append( + ("onMe%s" % unsetname, dict(user=None, channel=None), False)) + return (None, events) + if type(target) == Channel: + events = [] + modedelta = [] + modeparams = params.split() + setmodes = modeparams.pop(0) + modeset = "+" + chanmodes = self.supports.get("CHANMODES", _defaultchanmodes) + prefix = self.supports.get("PREFIX", _defaultprefix) + for mode in setmodes: + if mode in "+-": + modeset = mode + else: + if mode in chanmodes[0] + chanmodes[1]: + param = modeparams.pop(0) + modedelta.append(("%s%s" % (modeset, mode), param)) + if mode in _maskmodeeventnames.keys(): + if modeset == "+": + eventname = _maskmodeeventnames[mode][0] + if mode == "k": + target.key = param + if modeset == "-": + eventname = _maskmodeeventnames[mode][1] + if mode == "k": + target.key = None + matchesbot = glob.fnmatch.fnmatch( + "%s!%s@%s".lower() % (self.identity.nick, self.identity.username, self.identity.host), param.lower()) + events.append( + ("on%s" % eventname, dict(user=origin, channel=target, banmask=param), False)) + if matchesbot: + events.append( + ("onMe%s" % eventname, dict(user=origin, channel=target, banmask=param), False)) + elif mode in chanmodes[2]: + if modeset == "+": + param = modeparams.pop(0) + modedelta.append(("%s%s" % (modeset, mode), param)) + else: + modedelta.append(("%s%s" % (modeset, mode), None)) + elif mode in chanmodes[3]: + modedelta.append(("%s%s" % (modeset, mode), None)) + elif mode in prefix[0]: + modenick = modeparams.pop(0) + modeuser = self.user(modenick) + if mode in _privmodeeventnames.keys(): + if modeset == "+": + eventname = _privmodeeventnames[mode][0] + if modeset == "-": + eventname = _privmodeeventnames[mode][1] + events.append( + ("on%s" % eventname, dict(user=origin, channel=target, modeuser=modeuser), False)) + if modeuser == self.identity: + events.append( + ("onMe%s" % eventname, dict(user=origin, channel=target), False)) + modedelta.append(("%s%s" % (modeset, mode), modeuser)) + events.append( + ("onChanModeSet", dict(user=origin, channel=target, modedelta=modedelta), True)) + return (self.addons + target.addons, events) + elif target == self.identity: + modeparams = (params if params else extinfo).split() + setmodes = modeparams.pop(0) + modedelta = [] + modeset = "+" + for mode in setmodes: + if mode in "+-": + modeset = mode + continue + if modeset == "+": + if mode == "s": + if len(modeparams): + snomask = modeparams.pop(0) + snomaskdelta = [] + snomodeset = "+" + for snomode in snomask: + if snomode in "+-": + snomodeset = snomode + continue + snomaskdelta.append( + "%s%s" % (snomodeset, snomode)) + modedelta.append(("+s", snomaskdelta)) + else: + modedelta.append(("+s", [])) + else: + modedelta.append(("+%s" % mode, None)) + if modeset == "-": + modedelta.append(("-%s" % mode, None)) + return (self.addons, [("onUserModeSet", dict(origin=origin, modedelta=modedelta), True)]) + + def parseTOPIC(self, origin=None, target=None, targetprefix=None, params=None, extinfo=None, outgoing=False): + if outgoing: + return ([], []) + if origin == None: + return (None, [("onTopicSet", dict(user=None, channel=None, topic=None), True)]) + return (self.addons + target.addons, [("onTopicSet", dict(user=origin, channel=target, topic=extinfo), True)]) + + def parseINVITE(self, origin=None, target=None, targetprefix=None, params=None, extinfo=None, outgoing=False): + if outgoing: + return ([], []) + if origin == None: + return (None, [("onInvite", dict(user=None, channel=None), True)]) + channel = self.channel(extinfo if extinfo else params) + return (self.addons + channel.addons, [("onInvite", dict(user=origin, channel=channel), True)]) + + def parsePRIVMSG(self, origin=None, target=None, targetprefix=None, params=None, extinfo=None, outgoing=False): + if outgoing: + ctcp = re.findall(_ctcpmatch, extinfo) + if ctcp: + (ctcptype, ext) = ctcp[0] + if type(target) == User: + if ctcptype.upper() == "ACTION": + return (self.addons, [("onSendPrivAction", dict(origin=origin, user=target, action=ext), True)]) + return (self.addons, [("onSendCTCP", dict(origin=origin, user=target, ctcptype=ctcptype, params=ext), True)]) + elif type(target) == Channel: + if ctcptype.upper() == "ACTION": + return (self.addons, [("onSendChanAction", dict(origin=origin, channel=target, targetprefix=targetprefix, action=ext), True)]) + return (self.addons, [("onSendChanCTCP", dict(origin=origin, channel=target, targetprefix=targetprefix, ctcptype=ctcptype, params=ext), True)]) + else: + if type(target) == User: + return (self.addons, [("onSendPrivMsg", dict(origin=origin, user=target, msg=extinfo), True)]) + elif type(target) == Channel: + 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, 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( + origin=None, channel=None, targetprefix=None, msg=None), True), + ("onSendCTCP", dict(origin=None, user=None, ctcptype=None, params=None), True), + ("onSendPrivAction", dict( + origin=None, user=None, action=None), True), + ("onSendChanAction", dict( + origin=None, channel=None, targetprefix=None, action=None), True), + ("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, 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, 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, identified=identified), True)]) + elif target == self.identity: + 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: + ctcp = re.findall(_ctcpmatch, extinfo) + if ctcp: + (ctcptype, ext) = ctcp[0] + return (self.addons, [("onSendCTCPReply", dict(origin=origin, ctcptype=ctcptype, params=ext), True)]) + else: + if type(target) == Channel: + return (self.addons + target.addons, [("onSendChanNotice", dict(origin=origin, channel=target, targetprefix=targetprefix, msg=extinfo), True)]) + elif type(target) == User: + return (self.addons, [("onSendPrivNotice", dict(origin=origin, user=target, msg=extinfo), True)]) + if origin == None: + return (None, [ + ("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, 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) + if ctcp and target == self.identity: + (ctcptype, ext) = ctcp[0] + 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, identified=identified), True)]) + elif target == self.identity: + 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: + return (None, [("onBanListEntry", dict(origin=None, channel=None, mask=None, setby=None, settime=None), True)]) + (channame, mask, setby, settime) = params.split() + channel = self.channel(channame) + return (self.addons + channel.addons, [("onBanListEntry", dict(origin=origin, channel=channel, mask=mask, setby=setby, settime=int(settime)), True)]) + + def parse368(self, origin=None, target=None, targetprefix=None, params=None, extinfo=None): + if origin == None: + return (None, [("onBanListEnd", dict(origin=None, channel=None, endmsg=None), True)]) + channel = self.channel(params) + return (self.addons + channel.addons, [("onBanListEnd", dict(origin=origin, channel=channel, endmsg=extinfo), True)]) + + def parse346(self, origin=None, target=None, targetprefix=None, params=None, extinfo=None): # Channel Invite list + if origin == None: + return (None, [("onInviteListEntry", dict(origin=None, channel=None, mask=None, setby=None, settime=None), True)]) + (channame, mask, setby, settime) = params.split() + channel = self.channel(channame) + return (self.addons + channel.addons, [("onInviteListEntry", dict(origin=origin, channel=channel, mask=mask, setby=setby, settime=int(settime)), True)]) + + def parse347(self, origin=None, target=None, targetprefix=None, params=None, extinfo=None): + if origin == None: + return (None, [("onInviteListEnd", dict(origin=None, channel=None, endmsg=None), True)]) + channel = self.channel(params) + return (self.addons + channel.addons, [("onInviteListEnd", dict(origin=origin, channel=channel, endmsg=extinfo), True)]) + + def parse348(self, origin=None, target=None, targetprefix=None, params=None, extinfo=None): # Channel Ban Exception list + if origin == None: + return (None, [("onBanExceptListEntry", dict(origin=None, channel=None, mask=None, setby=None, settime=None), True)]) + (channame, mask, setby, settime) = params.split() + channel = self.channel(channame) + return (self.addons + channel.addons, [("onBanExceptListEntry", dict(origin=origin, channel=channel, mask=mask, setby=setby, settime=int(settime)), True)]) + + def parse349(self, origin=None, target=None, targetprefix=None, params=None, extinfo=None): + if origin == None: + return (None, [("onBanExceptListEnd", dict(origin=None, channel=None, endmsg=None), True)]) + channel = self.channel(params) + return (self.addons + channel.addons, [("onBanExceptListEnd", dict(origin=origin, channel=channel, endmsg=extinfo), True)]) + + def parse910(self, origin=None, target=None, targetprefix=None, params=None, extinfo=None): # Channel Access List + if origin == None: + return (None, [("onAccessListEntry", dict(origin=None, channel=None, mask=None, setby=None, settime=None), True)]) + (channame, mask, setby, settime) = params.split() + channel = self.channel(channame) + return (self.addons + channel.addons, [("onAccessListEntry", dict(origin=origin, channel=channel, mask=mask, setby=setby, settime=int(settime)), True)]) + + def parse911(self, origin=None, target=None, targetprefix=None, params=None, extinfo=None): + if origin == None: + return (None, [("onAccessListEnd", dict(origin=None, channel=None, endmsg=None), True)]) + channel = self.channel(params) + return (self.addons + channel.addons, [("onAccessListEnd", dict(origin=origin, channel=channel, endmsg=extinfo), True)]) + + def parse941(self, origin=None, target=None, targetprefix=None, params=None, extinfo=None): # Spam Filter list + if origin == None: + return (None, [("onSpamfilterListEntry", dict(origin=None, channel=None, mask=None, setby=None, settime=None), True)]) + (channame, mask, setby, settime) = params.split() + channel = self.channel(channame) + return (self.addons + channel.addons, [("onSpamfilterListEntry", dict(origin=origin, channel=channel, mask=mask, setby=setby, settime=int(settime)), True)]) + + def parse940(self, origin=None, target=None, targetprefix=None, params=None, extinfo=None): + if origin == None: + return (None, [("onSpamfilterListEnd", dict(origin=None, channel=None, endmsg=None), True)]) + channel = self.channel(params) + return (self.addons + channel.addons, [("onSpamfilterListEnd", dict(origin=origin, channel=channel, endmsg=extinfo), True)]) + + def parse954(self, origin=None, target=None, targetprefix=None, params=None, extinfo=None): # Channel exemptchanops list + if origin == None: + return (None, [("onExemptChanOpsListEntry", dict(origin=None, channel=None, mask=None, setby=None, settime=None), True)]) + (channame, mask, setby, settime) = params.split() + channel = self.channel(channame) + return (self.addons + channel.addons, [("onExemptChanOpsListEntry", dict(origin=origin, channel=channel, mask=mask, setby=setby, settime=int(settime)), True)]) + + def parse953(self, origin=None, target=None, targetprefix=None, params=None, extinfo=None): + if origin == None: + return (None, [("onExemptChanOpsListEnd", dict(origin=None, channel=None, endmsg=None), True)]) + channel = self.channel(params) + return (self.addons + channel.addons, [("onExemptChanOpsListEnd", dict(origin=origin, channel=channel, endmsg=extinfo), True)]) + + def parse728(self, origin=None, target=None, targetprefix=None, params=None, extinfo=None): # Channel quiet list + if origin == None: + return (None, [("onQuietListEntry", dict(origin=None, channel=None, modechar=None, mask=None, setby=None, settime=None), True)]) + (channame, modechar, mask, setby, settime) = params.split() + channel = self.channel(channame) + return (self.addons + channel.addons, [("onQuietListEntry", dict(origin=origin, channel=channel, modechar=modechar, mask=mask, setby=setby, settime=int(settime)), True)]) + + def parse729(self, origin=None, target=None, targetprefix=None, params=None, extinfo=None): + if origin == None: + return (None, [("onQuietListEnd", dict(channel=None, endmsg=None), True)]) + channame, modechar = params.split() + channel = self.channel(channame) + 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): + parsemethod = getattr(self, item) + addons, events = parsemethod() + for (event, args, fallback) in events: + supports[event] = tuple(args.keys()) + supports.update({"onConnect": (), + "onRegistered": (), + "onConnectAttempt": (), + "onConnectFail": ("exc", "excmsg", "tb"), + "onSessionOpen": (), + "onSessionClose": (), + "onDisconnect": ("expected",), + "onOther": ("line", "origin", "cmd", "target", "targetprefix", "params", "extinfo"), + "onUnhandled": ("line", "origin", "cmd", "target", "targetprefix", "params", "extinfo"), + "onRecv": ("line", "origin", "cmd", "target", "targetprefix", "params", "extinfo"), + "onSend": ("line", "origin", "cmd", "target", "targetprefix", "params", "extinfo"), + }) + 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 + + def onYourHost(self, context, origin, msg): + self.hostinfo = msg # Your Host + + def onServerCreated(self, context, origin, msg): + self.servcreated = msg # Server Created + + def onServInfo(self, context, origin, servinfo): + self.servinfo = servinfo # What is this code? + + def onSupports(self, context, origin, supports, msg): # Server Supports + protos = u" ".join( + [proto for proto in self.protoctl if proto in supports.keys()]) + if protos: + self.send(u"PROTOCTL {protos}".format(**vars())) + self.supports.update(supports) + + def onSnoMask(self, context, origin, snomask): # Snomask + self.identity.snomask = snomask + if "s" not in self.identity.modes: + self.identity.snomask = "" + + def onUserModes(self, context, origin, modes): # User Modes + self.identity.modes = modes + if "s" not in self.identity.modes: + self.identity.snomask = "" + + def onNetStats(self, context, origin, netstats): # Net Stats + self.netstats = netstats + + def onOpCount(self, context, origin, opcount): + self.opcount = opcount + + def onChanCount(self, context, origin, chancount): + self.chancount = chancount + + 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 onMeAway(self, context, origin, msg): # Entered away status + self.identity.away = True + + def onWhoisStart(self, context, origin, user, nickname, username, host, realname): # Start of WHOIS data + user.nick = nickname + user.username = username + user.host = host + + def onWhoisAway(self, context, origin, user, nickname, awaymsg): # Away Message + user.away = True + user.awaymsg = awaymsg + + def onWhoisServer(self, context, origin, user, nickname, server, servername): # Server + user.server = server + + def onWhoisOp(self, context, origin, user, nickname, msg): # IRC Op + user.ircop = True + user.ircopmsg = msg + + def onWhoisTimes(self, context, origin, user, nickname, idletime, signontime, msg): # Idle and Signon times + user.idlesince = int(time.time()) - idletime + user.signontime = signontime + + def onWhoisSSL(self, context, origin, user, nickname, msg): # SSL + user.secure = True + + def onWhoisLoggedInAs(self, context, origin, user, nickname, loggedinas, msg): # Logged in as + user.loggedinas = loggedinas + + def onChannelModes(self, context, origin, channel, modedelta): # Channel Modes + chanmodes = self.supports.get("CHANMODES", _defaultchanmodes) + for ((modeset, mode), param) in modedelta: + if mode in chanmodes[2]: + channel.modes[mode] = param + elif mode in chanmodes[3]: + channel.modes[mode] = True + + def onChanCreated(self, context, origin, channel, created): # Channel created + channel.created = created + + def onTopic(self, context, origin, channel, topic): # Channel Topic + channel.topic = topic + + def onTopicInfo(self, context, origin, channel, topicsetby, topictime): # Channel Topic info + channel.topicsetby = topicsetby + channel.topictime = topictime + + def onWhoEntry(self, context, origin, channel, user, channame, username, host, serv, nick, flags, hops, realname): # WHO reply + user.hops = hops + user.realname = realname + user.username = username + user.host = host + user.server = serv + user.away = "G" in flags + user.ircop = "*" in flags + if type(channel) == Channel: + if user not in channel.users: + channel.users.append(user) + if channel not in user.channels: + user.channels.append(channel) + for (mode, prefix) in zip(*self.supports.get("PREFIX", _defaultprefix)): + if prefix in flags: + if mode in channel.modes.keys() and user not in channel.modes[mode]: + channel.modes[mode].append(user) + elif mode not in channel.modes.keys(): + channel.modes[mode] = [user] + + def onNames(self, context, origin, channel, flag, channame, nameslist): # NAMES reply + for (symbs, nick, username, host) in nameslist: + user = self.user(nick) + if user.nick != nick: + user.nick = nick + if username and user.username != username: + user.username = username + if host and user.host != host: + user.host = host + with channel.lock: + if channel not in user.channels: + user.channels.append(channel) + if user not in channel.users: + channel.users.append(user) + prefix = self.supports.get("PREFIX", _defaultprefix) + for symb in symbs: + mode = prefix[0][prefix[1].index(symb)] + if not channel.modes.has_key(mode): + channel.modes[mode] = [user] + elif user not in channel.modes[mode]: + channel.modes[mode].append(user) + + def onMOTDLine(self, context, origin, motdline): # MOTD line + origin.motd.append(motdline) + + def onMOTDStart(self, context, origin, motdgreet): # Begin MOTD + origin.motdgreet = motdgreet + origin.motd = [] + + def onMOTDEnd(self, context, origin, motdend): + origin.motdend = motdend # End of MOTD + + # elif cmd==386 and "q" in self.supports["PREFIX"][0]: # Channel Owner (Unreal) + #(channame,owner)=params.split() + # channel=self.channel(channame) + #self._event("onRecv", channel.addons, **data) + # if channel.name!=channame: channel.name=channame ### Server seems to have changed the idea of the case of the channel name + # user=self.user(owner) + #if user.nick!=owner: user.nick=owner + # if channel.modes.has_key("q"): + #if user not in channel.modes["q"]: channel.modes["q"].append(user) + # else: channel.modes["q"]=[user] + + # elif cmd==388 and "a" in self.supports["PREFIX"][0]: # Channel Admin (Unreal) + #(channame,admin)=params.split() + # channel=self.channel(channame) + #self._event("onRecv", channel.addons, **data) + # if channel.name!=channame: channel.name=channame ### Server seems to have changed the idea of the case of the channel name + # user=self.user(admin) + #if user.nick!=admin: user.nick=admin + # if channel.modes.has_key("a"): + #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): + if self.lower(user.nick) == self.lower(newnick): + user.nick = newnick + else: + try: + other = self.users[newnick] + except KeyError: + pass + else: + 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) + 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 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: + channel.users.append(user) + + def onMeJoin(self, context, channel): + channel._init() + with channel._joining: + 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])) + + def onKick(self, context, kicker, channel, kicked, kickmsg): + if channel in kicked.channels: + kicked.channels.remove(channel) + if kicked in channel.users: + channel.users.remove(kicked) + prefix = self.supports.get("PREFIX", _defaultprefix) + for mode in prefix[0]: + if mode in channel.modes.keys() and kicked in channel.modes[mode]: + channel.modes[mode].remove(kicked) + + def onPart(self, context, user, channel, partmsg): + if channel in user.channels: + user.channels.remove(channel) + if user in channel.users: + channel.users.remove(user) + prefix = self.supports.get("PREFIX", _defaultprefix) + for mode in prefix[0]: + if mode in channel.modes.keys() and user in channel.modes[mode]: + channel.modes[mode].remove(user) + + def onMePart(self, context, channel, partmsg): + with channel._parting: + if channel._partrequested: + channel._partreply = "PART" + channel._parting.notify() + + def onMeKicked(self, context, kicker, channel, kickmsg): + with channel._parting: + if channel._partrequested: + channel._partreply = "KICK" + channel._parting.notify() + + def onQuit(self, context, user, quitmsg): + channels = list(user.channels) + for channel in channels: + with channel.lock: + if user in channel.users: + channel.users.remove(user) + prefix = self.supports.get("PREFIX", _defaultprefix) + for mode in prefix[0]: + if mode in channel.modes.keys() and user in channel.modes[mode]: + channel.modes[mode].remove(user) + user._init() + + def onChanModeSet(self, context, user, channel, modedelta): + chanmodes = self.supports.get("CHANMODES", _defaultchanmodes) + prefix = self.supports.get("PREFIX", _defaultprefix) + with channel.lock: + for ((modeset, mode), param) in modedelta: + if mode in chanmodes[0] + prefix[0]: + if mode not in channel.modes.keys(): + channel.modes[mode] = [] + if mode in chanmodes[0]: + if modeset == "+": + if param.lower() not in [mask.lower() for (mask, setby, settime) in channel.modes[mode]]: + channel.modes[mode].append( + (param, user, int(time.time()))) + else: + if mode == "b": # Inspircd mode is case insentive when unsetting the mode + masks = [mask.lower() + for (mask, setby, settime) in channel.modes[mode]] + if param.lower() in masks: + index = masks.index(param.lower()) + del channel.modes[mode][index] + else: + masks = [ + mask for (mask, setby, settime) in channel.modes[mode]] + if param in masks: + index = masks.index(param) + del channel.modes[mode][index] + elif mode in chanmodes[1]: + if modeset == "+": + channel.modes[mode] = param + else: + channel.modes[mode] = None + elif mode in chanmodes[2]: + if modeset == "+": + channel.modes[mode] = param + else: + channel.modes[mode] = None + elif mode in chanmodes[3]: + if modeset == "+": + channel.modes[mode] = True + else: + channel.modes[mode] = False + elif mode in prefix[0]: + if modeset == "+": + if param not in channel.modes[mode]: + channel.modes[mode].append(param) + elif param in channel.modes[mode]: + channel.modes[mode].remove(param) + + def onUserModeSet(self, context, origin, modedelta): + for ((modeset, mode), param) in modedelta: + if modeset == "+": + if mode not in self.identity.modes: + self.identity.modes += mode + if mode == "s": + for snomodeset, snomode in param: + if snomodeset == "+" and snomode not in self.identity.snomask: + self.identity.snomask += snomode + if snomodeset == "-" and snomode in self.identity.snomask: + self.identity.snomask = self.identity.snomask.replace( + snomode, "") + if modeset == "-": + if mode in self.identity.modes: + self.identity.modes = self.identity.modes.replace(mode, "") + if mode == "s": + self.identity.snomask = "" + + def onTopicSet(self, context, user, channel, topic): + with channel.lock: + channel.topic = topic + channel.topicsetby = user + channel.topictime = int(time.time()) + + def onCTCP(self, context, user, ctcptype, params): + if ctcptype.upper() == "VERSION": + user.ctcpreply("VERSION", self.ctcpversion()) + elif ctcptype.upper() == "TIME": + tformat = time.ctime() + tz = time.tzname[0] + user.ctcpreply("TIME", "%(tformat)s %(tz)s" % vars()) + elif ctcptype.upper() == "PING": + user.ctcpreply("PING", params) + elif ctcptype.upper() == "FINGER": + user.ctcpreply("FINGER", params) + + def onChanCTCP(self, context, user, channel, targetprefix, ctcptype, params): + self.onCTCP(context, user, ctcptype, params) + + def onBanListEntry(self, context, origin, channel, mask, setby, settime): # Channel Ban list + if "b" not in channel.modes.keys(): + channel.modes["b"] = [] + if mask.lower() not in [m.lower() for (m, s, t) in channel.modes["b"]]: + channel.modes["b"].append((mask, setby, int(settime))) + + def onInviteListEntry(self, context, origin, channel, mask, setby, settime): # Channel Invite Exception list + if "I" not in channel.modes.keys(): + channel.modes["I"] = [] + if mask.lower() not in [m.lower() for (m, s, t) in channel.modes["I"]]: + channel.modes["I"].append((mask, setby, int(settime))) + + def onBanExceptListEntry(self, context, origin, channel, mask, setby, settime): # Channel Invite Exception list + if "e" not in channel.modes.keys(): + channel.modes["e"] = [] + if mask.lower() not in [m.lower() for (m, s, t) in channel.modes["e"]]: + channel.modes["e"].append((mask, setby, int(settime))) + + def onAccessListEntry(self, context, origin, channel, mask, setby, settime): # Channel Invite Exception list + if "w" not in channel.modes.keys(): + channel.modes["w"] = [] + if mask.lower() not in [m.lower() for (m, s, t) in channel.modes["w"]]: + channel.modes["w"].append((mask, setby, int(settime))) + + def onSpamfilterListEntry(self, context, origin, channel, mask, setby, settime): # Channel Invite Exception list + if "g" not in channel.modes.keys(): + channel.modes["g"] = [] + if mask.lower() not in [m.lower() for (m, s, t) in channel.modes["g"]]: + channel.modes["g"].append((mask, setby, int(settime))) + + def onExemptChanOpsListEntry(self, context, origin, channel, mask, setby, settime): # Channel Invite Exception list + if "X" not in channel.modes.keys(): + channel.modes["X"] = [] + if mask.lower() not in [m.lower() for (m, s, t) in channel.modes["X"]]: + channel.modes["X"].append((mask, setby, int(settime))) + + def onQuietListEntry(self, context, origin, channel, modechar, mask, setby, settime): # Channel quiet list (Freenode) + if modechar not in channel.modes.keys(): + channel.modes[modechar] = [] + if mask.lower() not in [m.lower() for (m, s, t) in channel.modes[modechar]]: + channel.modes[modechar].append((mask, setby, int(settime))) + + def onOther(self, context, line, origin, cmd, target, targetprefix, params, extinfo): + if cmd in (384, 403, 405, 471, 473, 474, 475, 476, 520, 477, 489, 495): # Channel Join denied + try: + channel = self.channel(params) + except InvalidName: + pass + else: + with channel._joining: + if channel._joinrequested: + channel._joinreply = (cmd, extinfo) + channel._joining.notify() + + elif cmd == 470: # Channel Join denied due to redirect + channelname, redirect = params.split() + try: + channel = self.channel(channelname) + except InvalidName: + pass + else: + with channel._joining: + if channel._joinrequested: + channel._joinreply = ( + cmd, "%s (%s)" % (extinfo, redirect)) + channel._joining.notify() + + # elif cmd in (495, 384, 385, 386, 468, 470, 366, 315, 482, 484, 953, 368, 482, 349, 940, 911, 489, 490, 492, 520, 530): # Channels which appear in params + # for param in params.split(): + # if len(param) and param[0] in self.supports["CHANTYPES"]: + # channel=self.channel(param) + #self._event("onRecv", channel.addons, **data) + + def _trynick(self): + (q, s) = divmod(self.trynick, len(self.nick) + if type(self.nick) in (list, tuple) else 1) + 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.trynick += 1 + + 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 + if "\r" in line or "\n" in line: + raise InvalidCharacter + if type(line) == str: + line = autodecode(line) + cmd = line.split(" ")[0].upper() + + if T == None: + T = time.time() + if cmd == "PRIVMSG": + # Hard-coding a throttling mechanism for PRIVMSGs only here. Will later build support for custom throttlers. + # The throttle will be triggered when it attempts to send a sixth PRIVMSG in a four-second interval. + # When the throttle is active, PRIVMSGs will be sent in at least one-second intervals. + # The throttle is deactivated when three seconds elapse without + # sending a PRIVMSG. + while len(self.throttledata) and self.throttledata[0] < T - 4: + del self.throttledata[0] + if not self.throttled: + if len(self.throttledata) >= 5: + self.throttled = True + T = self.throttledata[-1] + 1 + else: + if len(self.throttledata) == 0 or self.throttledata[-1] < T - 2: + self.throttled = False + else: + T = max(T, self.throttledata[-1] + 1) + self.throttledata.append(T) + elif cmd in ("WHO", "MODE"): + # Might as well throttle WHOs and MODEs too. + while len(self.throttledata) and self.throttledata[0] < T - 4: + del self.throttledata[0] + if not self.throttled: + if len(self.throttledata) >= 5: + self.throttled = True + T = self.throttledata[-1] + 0.125 + else: + if len(self.throttledata) == 0 or self.throttledata[-1] < T - 2: + self.throttled = False + else: + T = max(T, self.throttledata[-1] + 0.125) + self.throttledata.append(T) + with self._sendline: + self._outgoing.append((T, line, origin)) + self._sendline.notify() + + def _cancelsend(self, line, origin=None, T=None): + with self._sendline: + self._outgoing.remove((T, line, origin)) + self._sendline.notify() + + def _procsendline(self, line, origin=None): + """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 + (null, username, host, cmd, target, params, extinfo) = match[0] + cmd = cmd.upper() + with self.lock: + if cmd == "QUIT": + self._quitexpected = True + if self._connection == None: + return + origline = line + + # Modify line if it contains a password so that the password is not + # logged or sent to any potentially untrustworthy addons + if cmd == "PRIVMSG": + if target.upper() == "NICKSERV": + nscmd = re.findall( + r"^\s*(\S+)\s+(\S+)(?:\s*(\S+)(?:\s*(.+))?)?$", extinfo, re.I) + if nscmd: + nscmd = nscmd[0] + if nscmd[0].upper() in ("IDENTIFY", "REGISTER"): + extinfo = "%s ********" % nscmd[0] + line = "%s %s :%s" % (cmd, target, extinfo) + elif nscmd[0].upper() in ("GROUP", "GHOST", "RECOVER", "RELEASE"): + extinfo = "%s %s ********" % nscmd[:2] + line = "%s %s :%s" % (cmd, target, extinfo) + elif nscmd[0].upper() == "SET": + if nscmd[1].upper() == "PASSWORD": + extinfo = "%s %s ********" % nscmd[:2] + line = "%s %s :%s" % (cmd, target, extinfo) + elif nscmd[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) + if target.upper() == "CHANSERV": + cscmd = re.findall( + r"^\s*(\S+)\s+(\S+)\s+(\S+)(?:\s*(\S+)(?:\s*(.+))?)?$", extinfo, re.I) + if cscmd: + cscmd = cscmd[0] + if cscmd[0].upper() in ("IDENTIFY", "REGISTER"): + extinfo = "%s %s ********" % cscmd[:2] + line = "%s %s :%s" % (cmd, target, extinfo) + elif cscmd[0].upper() in ("GROUP", "GHOST", "RECOVER", "RELEASE"): + extinfo = "%s %s %s ********" % cscmd[:3] + line = "%s %s :%s" % (cmd, target, extinfo) + elif cscmd[0].upper() == "SET": + if cscmd[2].upper() == "PASSWORD": + extinfo = "%s %s %s ********" % cscmd[:3] + line = "%s %s :%s" % (cmd, target, extinfo) + 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) + elif cmd.upper() in ("NS", "NICKSERV"): + if target.upper() in ("IDENTIFY", "REGISTER"): + params = params.split(" ") + while "" in params: + params.remove("") + if len(params): + params[0] = "********" + params = " ".join(params) + line = "%s %s %s" % (cmd, target, params) + elif target.upper() in ("GROUP", "GHOST", "RECOVER", "RELEASE"): + params = params.split(" ") + while "" in params: + params.remove("") + if len(params) > 1: + params[1] = "********" + params = " ".join(params) + line = "%s %s %s" % (cmd, target, params) + elif target.upper() not in ("GLIST", "ACCESS", "SASET", "DROP", "SENDPASS", "ALIST", "INFO", "LIST", "LOGOUT", "STATUS", "UPDATE", "GETPASS", "FORBID", "SUSPEND", "UNSUSPEND", "OINFO"): + params = "" + target = "********" + line = "%s %s" % (cmd, target) + elif cmd.upper() == "OPER": + params = "********" + line = "%s %s %s" % (cmd, target, params) + elif cmd.upper() == "PASS": + extinfo = "********" + target = "" + line = "%s :%s" % (cmd, extinfo) + elif cmd.upper() == "IDENTIFY": + target = "********" + line = "%s %s" % (cmd, target) + + prefix = self.supports.get("PREFIX", _defaultprefix) + chantypes = self.supports.get("CHANTYPES", _defaultchantypes) + chanmatch = re.findall(_targchanmatch % + (re.escape(prefix[1]), re.escape(chantypes)), target) + + # Check to see if target matches a channel (optionally with prefix) + if chanmatch: + targetprefix, channame = chanmatch[0] + target = self.channel(channame) + if target.name != channame: + # Target channel name has changed + 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 in ("PRIVMSG", "NOTICE", "MODE", "INVITE", "CHGHOST", "CHGIDENT", "CHGNAME", "WHOIS", "KILL", "SAMODE", "SETHOST", "WHO"): + targetprefix = "" + target = self.user(target) + + # Otherwise, target is just left as a string + else: + targetprefix = "" + + parsename = ("parse%03d" if type(cmd) == int else "parse%s") % cmd + if hasattr(self, parsename): + parsemethod = getattr(self, parsename) + if callable(parsemethod): + try: + ret = parsemethod( + origin, target, targetprefix, params, extinfo, outgoing=True) + addons, events = ret if ret is not None else ( + self.events, []) + except: + self.logerror( + u"There was an error in parsing the following line:", line) + return + else: + addons = self.addons + if type(cmd) == unicode: + events = [( + "onSend%s" % cmd.upper(), dict(line=line, origin=origin if origin else self, + target=target, targetprefix=targetprefix, params=params, extinfo=extinfo), True)] + else: + events = [] + if addons == None: + addons = [] + + if cmd not in ("PING", "PONG") or not self.quietpingpong: # Supress pings and pongs if self.quietpingpong is set to True + self._event( + 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)]) + self.logwrite(">>> %s" % line) + self._connection.send("%s\n" % origline.encode('utf8')) + + def _sendhandler(self): + # Enforce that this function must only be run from within + # self._sendhandlerthread. + if currentThread() != self._sendhandlerthread: + raise RuntimeError, "This function is designed to run in its own thread." + + try: + while True: + with self._sendline: + if "quit" in self._outgoing: + sys.exit() + S = time.time() + if len(self._outgoing): + T, line, origin = min(self._outgoing) + if T > S: + # The next item in the queue (by time) is still + # scheduled to be sent later. We wait until then, + # or when another item is put into the queue, + # whichever is first. + self._sendline.wait(T - S) + continue + else: + # The next item in the queue (by time) should be + # sent now. + self._outgoing.remove((T, line, origin)) + else: + # The queue is empty, so we will wait until something + # is put into the queue, then restart the while loop. + self._sendline.wait() + continue + + try: + self._procsendline(line, origin=origin) + except socket.error: + exc, excmsg, tb = sys.exc_info() + with self.lock: + self.logwrite( + u"*** Connection to {self:uri} failed: {excmsg}.".format(**vars())) + self._event(self.getalladdons(), [ + ("onConnectFail", dict(exc=exc, excmsg=excmsg, tb=tb), False)]) + with self._sendline: + self._outgoing.clear() + try: + self._connection.close() + except: + pass + + except SystemExit: + pass + + except: + self._quitexpected = True + self.logerror("FATAL Exception in {self}".format(**vars())) + with self._sendline: + try: + self._connection.send( + "QUIT :%s\n" % tb.rstrip().split("\n")[-1]) + self._connection.shutdown(socket.SHUT_WR) + except: + pass + finally: + with self._sendline: + self._outgoing.clear() # Clear out _outgoing. + + 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() + + def start(self): + """For compatibility, when modules still expect irc.Connection to be a subclass of threading.Thread.""" + return self.connect() def __repr__(self): server = self.server - if self.ipv6 and ":" in server: - server = "[%s]"%server - port = self.port + if self.ipver == socket.AF_INET6 and ":" in server: + server = "[%s]" % server if self.identity: - nick = self.identity.nick - user = self.identity.username if self.identity.username else "*" - host = self.identity.host if self.identity.host else "*" + return "<IRC Context: {self.identity:full} on {self:uri}>".format(**locals()) else: - nick = "*" - user = "*" - host = "*" - if self.ssl and self.ipv6: - protocol = "ircs6" - elif self.ssl: - protocol = "ircs" - elif self.ipv6: - protocol = "irc6" + return "<IRC Context: *!*@* on {self:uri}>".format(**locals()) + + def __format__(self, fmt): + port = self.port if self.port is not None else 6697 if self.secure else 6667 + if fmt == "uri": + ssl = "s" if self.secure else "" + proto = "6" if self.ipver == socket.AF_INET6 else "" + if self.ipver == socket.AF_INET6 and ":" in self.server: + return "irc{ssl}{proto}://[{self.server}]:{port}".format(**locals()) + else: + return "irc{ssl}{proto}://{self.server}:{port}".format(**locals()) else: - protocol = "irc" - return "<IRC Context: %(nick)s!%(user)s@%(host)s on %(protocol)s://%(server)s:%(port)s>" % locals() - #else: return "<IRC Context: irc%(ssl)s://%(server)s:%(port)s>" % locals() + return repr(self) def oper(self, name, passwd, origin=None): - self.raw("OPER %s %s" % (re.findall("^([^\r\n\\s]*)", name)[0], - re.findall("^([^\r\n\\s]*)", passwd)[0]), origin=origin) + """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) def list(self, params="", origin=None): - if len(re.findall("^([^\r\n\\s]*)", params)[0]): - self.raw("LIST %s" % (re.findall( - "^([^\r\n\\s]*)", params)[0]), origin=origin) + """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) else: - self.raw("LIST", origin=origin) + self.send(u"LIST", origin=origin) - def getmotd(self, target="", origin=None): - if len(re.findall("^([^\r\n\\s]*)", target)[0]): - self.raw("MOTD %s" % (re.findall( - "^([^\r\n\\s]*)", target)[0]), origin=origin) + 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.raw("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.raw("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.raw("VERSION", origin=origin) + self.send(u"VERSION", origin=origin) - def stats(self, query, target="", origin=None): - if len(re.findall("^([^\r\n\\s]*)", target)[0]): - self.raw("STATS %s %s" % (query, re.findall( - "^([^\r\n\\s]*)", target)[0]), origin=origin) + 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.raw("STATS %s"%query, origin=origin) + self.send(u"STATS %s" % query, origin=origin) + + def sendcapsrequest(self, capabilities, origin=None): + """sendcapsrequest(capabilities) - def quit(self, msg="", origin=None): + Request capabilities with "CAP REQ". Please use this method instead of using send(...).""" with self.lock: - if self.connected: - if len(re.findall("^([^\r\n]*)", msg)[0]): - self.raw("QUIT :%s" % re.findall("^([^\r\n]*)", - msg)[0], origin=origin) - else: - self.raw("QUIT", origin=origin) + for cap in capabilities: + if cap not in self._requestedcaps: + self._requestedcaps.append(cap) + self.send("CAP REQ {cap}".format(**vars()), origin=origin) + + 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) + else: + self.send(u"QUIT", origin=origin) + if blocking: + with self._disconnecting: + while self.connected: + self._disconnecting.wait() + self._recvhandlerthread.join() + self._sendhandlerthread.join() + + 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 addon - reply.append("%(__name__)s %(__version__)s, %(__author__)s" % - vars(self)) + # Prepare reply for this module + reply.append( + u"{self.__name__} {self.__version__}, {self.__author__}".format(**vars())) - ### Prepare reply for Python and OS versions + # Prepare reply for Python and OS versions pyver = sys.version.split("\n") - pyver[0] = "Python "+pyver[0] + pyver[0] = "Python " + pyver[0] reply.extend(pyver) reply.extend(platform.platform().split("\n")) - ### Prepare reply for extension addons + + # Prepare reply for each addons for addon in self.addons: try: - r = "%(__name__)s %(__version__)s" % vars(addon) - if "__extinfo__" in vars(addon): - r += ", %(__extinfo__)s" % vars() - reply.append(r) + if hasattr(addon, "__extinfo__"): + reply.append( + u"{addon.__name__} {addon.__version__}, {addon.__extinfo__}".format(**vars())) + else: + reply.append( + u"{addon.__name__} {addon.__version__}".format(**vars())) except: pass - return reduce(lambda x, y: "%s; %s" % (x, y), reply) + return u"; ".join(reply) def raw(self, line, origin=None): - if "\r" in line or "\n" in line: - raise InvalidCharacter - self.outgoing.put((line, origin)) + """raw(line[, origin]) + + Deprecated. Use send() instead.""" + self.send(line, origin=origin) + + def user(self, nick, init=False): + """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 user(self, nick): - users = [user for user in self.users if user.nick.lower( - ) == nick.lower()] - if len(users): - return users[0] + def channel(self, name, init=False): + """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: - 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 - - def channel(self, name): - channels = [chan for chan in self.channels if name.lower( - ) == chan.name.lower()] - if len(channels): - return channels[0] + 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()] + supports.sort() + supports = " ".join(supports) + lines = [] + while len(supports) > 196: + index = supports.rfind(" ", 0, 196) + slice = supports[:index] + lines.append( + u":{self.serv} 005 {self.identity.nick} {slice} :are supported by this server".format(**vars())) + supports = supports[index + 1:] + if supports: + lines.append( + u":{self.serv} 005 {self.identity.nick} {supports} :are supported by this server".format(**vars())) + return lines + + def fmtgreeting(self): + """fmtgreeting() --> list + + Formats a valid greeting from known information (Responses 001 through 004).""" + lines = [] + if self.welcome: + lines.append( + u":{self.serv} 001 {self.identity.nick} :{self.welcome}".format(**vars())) + if self.hostinfo: + lines.append( + u":{self.serv} 002 {self.identity.nick} :{self.hostinfo}".format(**vars())) + if self.servcreated: + lines.append( + u":{self.serv} 003 {self.identity.nick} :{self.servcreated}".format(**vars())) + if self.servinfo: + lines.append( + u":{self.serv} 004 {self.identity.nick} {self.servinfo}".format(**vars())) + return lines + + def fmtusermodes(self): + """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): + """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( + u":{self.serv} 375 {self.identity.nick} :{self.motdgreet}".format(**vars())) + for motdline in self.motd: + lines.append( + u":{self.serv} 372 {self.identity.nick} :{motdline}".format(**vars())) + lines.append( + u":{self.serv} 376 {self.identity.nick} :{self.motdend}".format(**vars())) + return lines 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 + return [u":{self.serv} 422 {self.identity.nick} :MOTD File is missing".format(**vars())] class Channel(object): - def __init__(self, name, context): - if not re.match(r"^[%s]\S*$" % context.supports.get("CHANTYPES", "#"), name): - raise InvalidName(repr(name)) + + def __init__(self, name, context, key=None): + chantypes = context.supports.get("CHANTYPES", _defaultchantypes) + if not re.match(_chanmatch % re.escape(chantypes), name): + raise InvalidName, repr(name) self.name = name self.context = context + self.key = key + self.lock = Lock() + self._init() + self._joining = Condition(self.lock) + self._parting = Condition(self.lock) + self._joinrequested = False + self._joinreply = None + self._partrequested = False + self._partreply = None + + def _init(self): + for user in self.context.users._dict.values(): + if self in user.channels: + user.channels.remove(self) self.addons = [] self.topic = "" self.topicsetby = "" - self.topictime = () + self.topictime = None self.topicmod = "" self.modes = {} - self.users = [] + self.users = UserList(context=self.context) self.created = None - self.lock = Lock() def msg(self, msg, target="", origin=None): if target and target not in self.context.supports.get("PREFIX", ("ohv", "@%+"))[1]: raise InvalidPrefix for line in re.findall("([^\r\n]+)", msg): - self.context.raw("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): - self.context.raw("WHO %s" % (self.name), origin=origin) + def who(self, origin=None, blocking=False): + # Send WHO request to server + 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.raw("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. + secret = "s" in self.modes.keys() and self.modes["s"] + private = "p" in self.modes.keys() and self.modes["p"] + flag = "@" if secret else ("*" if private else "=") + + modes, symbols = self.context.supports.get("PREFIX", ("ohv", "@%+")) + 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", "@%+")))], self.context.lower(user.nick))) + elif sort == "nick": + users.sort(key=lambda user: self.context.lower(user.nick)) + if uhnames: + template = u"{prefixes}{user:full}" + else: + template = u"{prefixes}{user}" + + nameslist = [] + for user in users: + prefixes = u"".join( + [prefix if mode in self.modes.keys() and user in self.modes[mode] else "" for prefix, mode in zip(symbols, modes)]) + if not namesx: + prefixes = prefixes[:1] + nameslist.append(template.format(**vars())) + names = " ".join(nameslist) + + lines = [] + while len(names) > 196: + index = names.rfind(" ", 0, 196) + slice = names[:index] + lines.append( + u":{self.context.identity.server} 353 {self.context.identity.nick} {flag} {self.name} :{slice}".format(**vars())) + names = names[index + 1:] + if len(names): + lines.append( + u":{self.context.identity.server} 353 {self.context.identity.nick} {flag} {self.name} :{names}".format(**vars())) + + lines.append( + u":{self.context.identity.server} 366 {self.context.identity.nick} {self.name} :End of /NAMES list.".format(**vars())) + return lines + + def fmttopic(self): + # Prepares 332 and 333 responses + if self.topic and self.topictime: + response332 = u":{self.context.identity.server} 332 {self.context.identity.nick} {self.name} :{self.topic}".format( + **vars()) + if type(self.topicsetby) == User: + response333 = u":{self.context.identity.server} 333 {self.context.identity.nick} {self.name} {self.topicsetby.nick} {self.topictime}".format( + **vars()) + else: + response333 = u":{self.context.identity.server} 333 {self.context.identity.nick} {self.name} {self.topicsetby} {self.topictime}".format( + **vars()) + return [response332, response333] + else: + return [u":{self.context.identity.server} 331 {self.context.identity.nick} {self.name} :No topic is set".format(**vars())] + + def fmtchancreated(self): + # Prepares 329 responses + return u":{self.context.identity.server} 329 {self.context.identity.nick} {self.name} {self.created}".format(**vars()) + + def fmtmodes(self): + items = self.modes.items() + chanmodes = self.context.supports.get("CHANMODES", _defaultchanmodes) + modes = "".join( + [mode for (mode, val) in items if mode not in chanmodes[0] + self.context.supports["PREFIX"][0] and val]) + params = " ".join( + [val for (mode, val) in items if mode in chanmodes[1] + chanmodes[2] and val]) + if modes and params: + return u":{self.context.identity.server} 324 {self.context.identity.nick} {self.name} +{modes} {params}".format(**vars()) + elif modes: + return u":{self.context.identity.server} 324 {self.context.identity.nick} {self.name} +{modes}".format(**vars()) + else: + return None def notice(self, msg, target="", origin=None): if target and target not in self.context.supports.get("PREFIX", ("ohv", "@%+"))[1]: raise InvalidPrefix for line in re.findall("([^\r\n]+)", msg): - self.context.raw("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.raw("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]): - self.msg("\01%s %s\01" % (act.upper(), re.findall( - "^([^\r\n]*)", msg)[0]), origin=origin) + self.msg("\01%s %s\01" % + (act.upper(), re.findall("^([^\r\n]*)", msg)[0]), origin=origin) else: self.msg("\01%s\01" % act.upper()) def ctcpreply(self, act, msg="", origin=None): if len(re.findall("^([^\r\n]*)", msg)[0]): - self.notice("\01%s %(msg)s\01" % (act.upper(), - re.findall("^([^\r\n]*)", msg)[0]), origin=origin) + self.notice("\01%s %(msg)s\01" % + (act.upper(), re.findall("^([^\r\n]*)", msg)[0]), origin=origin) else: self.notice("\01%s\01" % act.upper(), origin=origin) def me(self, msg="", origin=None): self.ctcp("ACTION", msg, origin=origin) - def part(self, msg="", origin=None): - if len(re.findall("^([^\r\n]*)", msg)[0]): - self.context.raw("PART %s :%s" % (self.name, - re.findall("^([^\r\n]*)", msg)[0]), origin=origin) - else: - self.context.raw("PART %s" % self.name, origin=origin) + def part(self, msg="", blocking=False, timeout=30, origin=None): + with self.context.lock: + if self.context.identity not in self.users: + # Bot is not on the channel + raise NotOnChannel + with self._parting: + try: + if self._partrequested: + raise ActionAlreadyRequested + self._partrequested = True + if len(re.findall("^([^\r\n]*)", msg)[0]): + 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) + + # Anticipated Numeric Replies: + + # ERR_NEEDMOREPARAMS ERR_NOSUCHCHANNEL + # ERR_NOTONCHANNEL + + if blocking: + endtime = time.time() + timeout + while True: + self._parting.wait(max(0, endtime - time.time())) + t = time.time() + if not self.context.connected: + raise NotConnected + elif self._partreply in ("PART", "KICK"): + return + elif type(self._partreply) == tuple and len(self._partreply) == 2: + cmd, extinfo = self._partreply + raise exceptcodes[cmd], extinfo + if t > endtime: + raise RequestTimedOut + finally: + self._partrequested = False + self._partreply = None def invite(self, user, origin=None): nickname = user.nick if type( user) == User else re.findall("^([^\r\n\\s]*)", user)[0] if nickname == "": raise InvalidName - self.context.raw("INVITE %s %s" % (nickname, self.name), origin=origin) - - def join(self, key="", origin=None): - if len(re.findall("^([^\r\n\\s]*)", key)[0]): - self.context.raw("JOIN %s %s" % (self.name, re.findall( - "^([^\r\n\\s]*)", key)[0]), origin=origin) - else: - self.context.raw("JOIN %s" % 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: + if self.context.identity in self.users: + # Bot is already on the channel + raise AlreadyJoined + if not self.context.connected: + raise NotConnected + with self._joining: + try: + if self._joinrequested: + raise ActionAlreadyRequested + self._joinrequested = True + if len(re.findall("^([^\r\n\\s]*)", key)[0]): + 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) + + # Anticipated Numeric Replies: + + # ERR_NEEDMOREPARAMS ERR_BANNEDFROMCHAN + # ERR_INVITEONLYCHAN ERR_BADCHANNELKEY + # ERR_CHANNELISFULL ERR_BADCHANMASK + # ERR_NOSUCHCHANNEL ERR_TOOMANYCHANNELS + # ERR_TOOMANYTARGETS ERR_UNAVAILRESOURCE + + if blocking: + endtime = time.time() + timeout + while True: + self._joining.wait(max(0, endtime - time.time())) + t = time.time() + if not self.context.connected: + raise NotConnected + elif self._joinreply == "JOIN": + return + elif type(self._joinreply) == tuple and len(self._joinreply) == 2: + cmd, extinfo = self._joinreply + raise exceptcodes[cmd], extinfo + if t > endtime: + raise RequestTimedOut + finally: + self._joinrequested = False + self._joinreply = None def kick(self, user, msg="", origin=None): nickname = user.nick if type( @@ -1266,25 +3206,38 @@ class Channel(object): if nickname == "": raise InvalidName if len(re.findall("^([^\r\n]*)", msg)[0]): - self.context.raw("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.raw("KICK %s %s" % (self.name, - nickname), origin=origin) + self.context.send(u"KICK %s %s" % + (self.name, nickname), origin=origin) def __repr__(self): - return "<Channel: "+self.name+"@"+self.context.server+"/"+str(self.context.port)+">" + return u"<Channel: {self.name} on {self.context:uri}>".format(**vars()) + + def __contains__(self, item): + return item in self.users + + def __format__(self, fmt): + return self.name + + def json(self): + return self.name class User(object): + def __init__(self, nick, context): - if not re.match(r"^\S+$", nick): + if not re.match(_nickmatch, nick): raise InvalidName self.nick = nick + self.context = context + self._init() + + def _init(self): self.username = "" self.host = "" - self.channels = [] - self.context = context + self.channels = ChanList(context=self.context) self.modes = "" self.snomask = "" self.server = None @@ -1293,210 +3246,457 @@ class User(object): self.ircopmsg = "" self.idlesince = None self.signontime = None - self.ssl = None + self.secure = None self.away = None + self.loggedinas = None def __repr__(self): - return "<User: %(nick)s!%(username)s@%(host)s>" % vars(self) + return (u"<User: %(nick)s!%(username)s@%(host)s>" % vars(self)).encode("utf8") + + def __format__(self, fmt): + if fmt == "full": + return u"{self.nick}!{self.username}@{self.host}".format(**locals()) + else: + return self.nick def msg(self, msg, origin=None): for line in re.findall("([^\r\n]+)", msg): - self.context.raw("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.raw("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]): - self.msg("\01%s %s\01" % (act.upper(), re.findall( - "^([^\r\n]*)", msg)[0]), origin=origin) + self.msg(u"\01%s %s\01" % + (act.upper(), re.findall("^([^\r\n]*)", msg)[0]), origin=origin) else: - self.msg("\01%s\01" % act.upper()) + self.msg(u"\01%s\01" % act.upper()) def ctcpreply(self, act, msg="", origin=None): if len(re.findall("^([^\r\n]*)", msg)[0]): - self.notice("\01%s %s\01" % (act.upper(), - re.findall("^([^\r\n]*)", msg)[0]), origin=origin) + self.notice("\01%s %s\01" % + (act.upper(), re.findall("^([^\r\n]*)", msg)[0]), origin=origin) else: self.notice("\01%s\01" % act.upper(), origin=origin) def me(self, msg="", origin=None): self.ctcp("ACTION", msg, origin=origin) + def json(self): + return self.nick -class Outgoing(Thread): - def __init__(self, IRC, throttle=0.25, lines=10, t=5): - self.IRC = IRC - self.throttle = throttle - self.lines = lines - self.time = t - #self.queue=Queue() - Thread.__init__(self) - def run(self): - try: - throttled = False - timestamps = [] - while True: +class Config(object): + + def __init__(self, addon, **kwargs): + self.addon = addon + self.__dict__.update(kwargs) + + def json(self): + if "onAddonAdd" in dir(self.addon) and type(self.addon.onAddonAdd) == new.instancemethod: + conf = OrderedDict(addon=self.addon) + try: + arginspect = inspect.getargspec(self.addon.onAddonAdd) + except: + raise TypeError( + repr(self.addon.onAddonAdd) + " is not JSON serializable") + + if arginspect.defaults: + requiredargs = arginspect.args[ + 2:len(arginspect.args) - len(arginspect.defaults)] + argswithdefaults = arginspect.args[ + len(arginspect.args) - len(arginspect.defaults):] + defaultvalues = arginspect.defaults + else: + requiredargs = arginspect.args[2:] + argswithdefaults = [] + defaultvalues = [] + + for key in requiredargs: try: - q = self.IRC.outgoing.get() - except Queue.Interrupted: - break - if q == "quit" or not self.IRC.connected: - break - line, origin = q - match = re.findall("^(.+?)(?:\\s+(.+?)(?:\\s+(.+?))??)??(?:\\s+:(.*))?$", line, re.I) - (cmd, target, params, extinfo) = match[0] - timestamp = reduce(lambda x, y: x+":"+y, [str(t).rjust( - 2, "0") for t in time.localtime()[0:6]]) - with self.IRC.lock: - try: - self.IRC.connection.send("%(line)s\n" % vars()) - except socket.error: - try: - self.IRC.connection.shutdown(0) - except: - pass - raise + conf[key] = getattr(self, key) + except AttributeError: + print key + raise TypeError( + repr(self) + " is not JSON serializable (Cannot recover required argument '%s')" % key) - ### Modify line if it contains a password so that the password is not logged or sent to any potentially untrustworthy addons - #if re.match("^(.+?)(?:\\s+(.+?)(?:\\s+(.+?))??)??(?:\\s+:(.*))?$", line, re.I): - if cmd.upper() == "PRIVMSG": - if target.upper() == "NICKSERV": - nscmd = re.findall(r"^\s*(\S+)\s+(\S+)(?:\s*(\S+)(?:\s*(.+))?)?$", extinfo, re.I) - if nscmd: - nscmd = nscmd[0] - if nscmd[0].upper() in ("IDENTIFY", "REGISTER"): - extinfo = "%s ********"%nscmd[0] - line = "%s %s :%s"%(cmd, target, extinfo) - elif nscmd[0].upper() in ("GROUP", "GHOST", "RECOVER", "RELEASE"): - extinfo = "%s %s ********"%nscmd[:2] - line = "%s %s :%s"%(cmd, target, extinfo) - elif nscmd[0].upper() == "SET": - if nscmd[1].upper() == "PASSWORD": - extinfo = "%s %s ********"%nscmd[:2] - line = "%s %s :%s"%(cmd, target, extinfo) - elif nscmd[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) - if target.upper() == "CHANSERV": - cscmd = re.findall(r"^\s*(\S+)\s+(\S+)\s+(\S+)(?:\s*(\S+)(?:\s*(.+))?)?$", extinfo, re.I) - if cscmd: - cscmd = cscmd[0] - if cscmd[0].upper() in ("IDENTIFY", "REGISTER"): - extinfo = "%s %s ********"%cscmd[:2] - line = "%s %s :%s"%(cmd, target, extinfo) - elif cscmd[0].upper() in ("GROUP", "GHOST", "RECOVER", "RELEASE"): - extinfo = "%s %s %s ********"%cscmd[:3] - line = "%s %s :%s"%(cmd, target, extinfo) - elif cscmd[0].upper() == "SET": - if cscmd[2].upper() == "PASSWORD": - extinfo = "%s %s %s ********"%cscmd[:3] - line = "%s %s :%s"%(cmd, target, extinfo) - 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) - - chanmatch = re.findall("([%s]?)([%s].+)"%(re.escape(self.IRC.supports.get("PREFIX", ("ohv", "@%+"))[1]), re.escape(self.IRC.supports.get("CHANTYPES", "#"))), target) - if chanmatch: - targetprefix, channame = chanmatch[0] - target = self.IRC.channel(channame) - if target.name != channame: - ### Target channel name has changed - target.name = channame - elif len(target) and target[0] != "$" and cmd != "NICK": - targetprefix = "" - target = self.IRC.user(target) - - ctcp = re.findall("^\x01(.*?)(?:\\s+(.*?)\\s*)?\x01$", - extinfo) - if ctcp: - (ctcptype, ext) = ctcp[0] - if ctcptype.upper() == "ACTION": - if type(target) == Channel: - self.IRC.event("onSendChanAction", self.IRC.addons+target.addons, origin=origin, channel=target, targetprefix=targetprefix, action=ext) - elif type(target) == User: - self.IRC.event("onSendPrivAction", self.IRC.addons, origin=origin, user=target, action=ext) - else: - if type(target) == Channel: - self.IRC.event("onSendChanCTCP", self.IRC.addons+target.addons, origin=origin, channel=target, targetprefix=targetprefix, ctcptype=ctcptype, params=ext) - elif type(target) == User: - self.IRC.event("onSendPrivCTCP", self.IRC.addons, origin=origin, user=target, ctcptype=ctcptype, params=ext) - else: - if type(target) == Channel: - self.IRC.event("onSendChanMsg", self.IRC.addons+target.addons, origin=origin, channel=target, targetprefix=targetprefix, msg=extinfo) - elif type(target) == User: - self.IRC.event("onSendPrivMsg", self.IRC.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() == "NS": - if target.upper() in ("IDENTIFY", "REGISTER"): - params = params.split(" ") - while "" in params: - params.remove("") - if len(params): - params[0] = "********" - params = " ".join(params) - line = "%s %s %s"%(cmd, target, params) - elif target.upper() in ("GROUP", "GHOST", "RECOVER", "RELEASE"): - params = params.split(" ") - while "" in params: - params.remove("") - if len(params) > 1: - params[1] = "********" - params = " ".join(params) - line = "%s %s %s"%(cmd, target, params) - elif target.upper() not in ("GLIST", "ACCESS", "SASET", "DROP", "SENDPASS", "ALIST", "INFO", "LIST", "LOGOUT", "STATUS", "UPDATE", "GETPASS", "FORBID", "SUSPEND", "UNSUSPEND", "OINFO"): - params = "" - target = "********" - line = "%s %s"%(cmd, target) - elif cmd.upper() == "OPER": - params = "********" - line = "%s %s %s"%(cmd, target, params) - elif cmd.upper() == "PASS": - extinfo = "********" - target = "" - line = "%s :%s"%(cmd, extinfo) - elif cmd.upper() == "IDENTIFY": - target = "********" - line = "%s %s"%(cmd, target) - self.IRC.event("onSend", self.IRC.addons, origin=origin, line=line, cmd=cmd, target=target, params=params, extinfo=extinfo) - self.IRC.logwrite(">>> %(line)s" % vars()) - if cmd.upper() == "QUIT": - self.IRC.quitexpected = True - timestamps.append(time.time()) - while timestamps[0] < timestamps[-1]-self.time-0.1: - del timestamps[0] - if throttled: - if len(timestamps) < 2: - throttled = False + for key, default in zip(argswithdefaults, defaultvalues): + try: + value = getattr(self, key) + if value != default: + conf[key] = getattr(self, key) + except AttributeError: + pass + return conf + else: + return self.addon + + +class ChanList(list): + + 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 + if iterable: + chanlist = [] + 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: - if len(timestamps) >= self.lines: - throttled = True - if throttled: - time.sleep(max(timestamps[-1] + - self.throttle-time.time(), 0)) - except: - self.IRC.connection.send("QUIT :%s\n" % - traceback.format_exc().rstrip().split("\n")[-1]) - self.IRC.connection.close() - self.IRC.connection.shutdown(0) + 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: + 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." + if type(item) != Channel: + raise TypeError, "Only channel objects are permitted in list" + if self.context and item.context != self.context: + raise ValueError, "Channel object does not belong to context." + list.append(self, item) + 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: + 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." + if type(item) != Channel: + raise TypeError, "Only channel objects are permitted in list" + if self.context and item.context != self.context: + raise ValueError, "Channel object does not belong to context." + list.insert(self, index, item) + 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): + 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) + else: + self.context.send(u"JOIN %s" % self, origin=origin) + + def part(self, partmsg=None, origin=None): + if not self.context: + raise ValueError, "No context defined." + if partmsg: + self.context.send(u"PART %s :%s" % + (",".join([channel.name for channel in self]), partmsg), origin=origin) + else: + self.context.send(u"PART %s" % self, origin=origin) -class Pinger(Thread): - def __init__(self, connection, lock=None): - self.connection = connection - self.lock = lock - self.daemon = True - Thread.__init__(self) + def msg(self, msg, origin=None): + if not self.context: + raise ValueError, "No context defined." + self.context.send(u"PRIVMSG %s :%s" % (self, msg), origin=origin) - def run(self): - pass + 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, 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 + if iterable: + 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: + 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." + if type(item) != User: + raise TypeError, "Only user objects are permitted in list" + if self.context and item.context != self.context: + raise ValueError, "User object does not belong to context." + list.append(self, item) + 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: + 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." + if type(item) != User: + raise TypeError, "Only user objects are permitted in list" + if self.context and item.context != self.context: + raise ValueError, "User object does not belong to context." + list.insert(self, index, item) + 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): + """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) + + 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): + + def __init__(self, name, context): + self.name = name + self.context = context + self.lock = Lock() + self._init() + + def _init(self): + self.stats = {} + self.users = UserList(context=self.context) + self.created = None + 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() @@ -13,15 +13,28 @@ import Queue import ssl import urllib2 import irc +import codecs -modemapping = dict( - Y="ircop", q="owner", a="admin", o="op", h="halfop", v="voice") +modemapping = dict(Y="ircop", q="owner", + a="admin", o="op", h="halfop", v="voice") + + +def LoggerReload(log): + newlog = Logger(logroot=log.logroot) + with newlog.rotatelock, log.rotatelock: + newlog.logs = log.logs + log.logs = {} + for context, conf in log.conf.items(): + context.rmAddon(log) + context.addAddon(newlog, label=conf.label) + return newlog 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]) @@ -31,11 +44,11 @@ class Logger(Thread): while len(path) > 1: path[0] = os.path.join(*path[:2]) del path[1] - #print path + # print path os.mkdir(path[0]) self.logs = {} - self.labels = {} + self.conf = {} self.rotatelock = Lock() Thread.__init__(self) @@ -45,114 +58,134 @@ class Logger(Thread): def run(self): try: Y, M, D, h, m, s, w, d, dst = time.localtime() - nextrotate = int(time.mktime((Y, M, D+1, 0, 0, 0, 0, 0, -1))) + nextrotate = int(time.mktime((Y, M, D + 1, 0, 0, 0, 0, 0, -1))) while True: while nextrotate > time.time(): # May need to do this in a loop in case the following time.sleep command wakes up a second too early. - time.sleep(max(0.1, min((nextrotate-time.time(), 3600)))) + time.sleep(max(0.1, min((nextrotate - time.time(), 3600)))) with self.rotatelock: 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 IRC in self.labels.keys(): - if IRC.connected: - with IRC.lock: + + logroot = os.path.expanduser(self.logroot) + for context in self.conf.keys(): + if context.connected: + with context.lock: try: - self.rotateLog(IRC) + self.rotateLog(context) except: exc, excmsg, tb = sys.exc_info() - IRC.logwrite(*["!!! [Logger] Exception in module %(module)s" % vars()]+["!!! [Logger] %s" % tbline for tbline in traceback.format_exc().split("\n")]) - if IRC.identity: - for channel in IRC.identity.channels: + context.logwrite(*["!!! [Logger] Exception in module %(module)s" % vars()] + [ + "!!! [Logger] %s" % tbline for tbline in traceback.format_exc().split("\n")]) + if context.identity: + for channel in context.identity.channels: try: self.rotateLog(channel) except: exc, excmsg, tb = sys.exc_info() - IRC.logwrite(*["!!! [Logger] Exception in module %(module)s" % vars()]+["!!! [Logger] %s" % tbline for tbline in traceback.format_exc().split("\n")]) - for user in IRC.users: + context.logwrite(*["!!! [Logger] Exception in module %(module)s" % vars()] + [ + "!!! [Logger] %s" % tbline for tbline in traceback.format_exc().split("\n")]) + for user in context.users: if user in self.logs.keys(): try: self.closeLog(user) except: exc, excmsg, tb = sys.exc_info() - IRC.logwrite(*["!!! [Logger] Exception in module %(module)s" % vars()]+["!!! [Logger] %s" % tbline for tbline in traceback.format_exc().split("\n")]) - IRC.logopen(os.path.join(self.logroot, self.labels[IRC], "rawdata-%04d.%02d.%02d.log"%now[:3])) - nextrotate = int(time.mktime((Y, M, D+1, 0, 0, 0, 0, 0, -1))) + 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(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): + 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) - def onAddonAdd(self, IRC, label): - if label in self.labels.values(): - raise BaseException("Label already exists") - if IRC 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[IRC] = label - if IRC.connected: - self.openLog(IRC) - if IRC.identity: - for channel in IRC.identity.channels: + contextroot = os.path.join(logroot, label) + if not os.path.isdir(contextroot): + os.mkdir(contextroot) + + if context.connected: + self.openLog(context) + if context.identity: + for channel in context.identity.channels: self.openLog(channel) now = time.localtime() - timestamp = reduce(lambda x, y: x+":"+y, [str(t).rjust(2, - "0") for t in now[0:6]]) - IRC.logopen(os.path.join(self.logroot, self.labels[IRC], - "rawdata-%04d.%02d.%02d.log"%now[:3])) - - def onAddonRem(self, IRC): - if IRC.connected: + timestamp = reduce(lambda x, y: x + ":" + y, [ + str(t).rjust(2, "0") for t in now[0:6]]) + context.logopen( + os.path.join(contextroot, "rawdata-%04d.%02d.%02d.log" % now[:3])) + self.conf[context] = conf + return conf + + def onAddonRem(self, context): + if context.connected: for channel in self.logs.keys(): - if channel in IRC.channels: + if channel in context.channels: if not self.logs[channel].closed: self.closeLog(channel) for user in self.logs.keys(): - if user in IRC.users: + if user in context.users: if not self.logs[user].closed: self.closeLog(user) - if not self.logs[IRC].closed: - self.closeLog(IRC) - del self.labels[IRC] + if context in self.logs.keys() and not self.logs[context].closed: + self.closeLog(context) + del self.conf[context] def openLog(self, window): + logroot = os.path.expanduser(self.logroot) + with self.rotatelock: if not self.isAlive(): self.start() + + if window in self.logs.keys(): + # Don't do anything + return + now = time.localtime() - timestamp = reduce(lambda x, y: x+":"+y, [str(t).rjust(2, - "0") for t in now[0:6]]) + timestamp = reduce(lambda x, y: x + ":" + y, [ + str(t).rjust(2, "0") for t in now[0:6]]) if type(window) == irc.Connection: - log = self.logs[window] = open(os.path.join(self.logroot, self.labels[window], "console-%04d.%02d.%02d.log"%now[:3]), "a") + log = self.logs[window] = codecs.open( + 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] = open(os.path.join(self.logroot, label, "channel-%s-%04d.%02d.%02d.log"%((urllib2.quote(window.name.lower()).replace("/", "%2f"),)+now[:3])), "a") + 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() - if window.context.identity in window.users: + if window in window.context.identity.channels: if window.topic: - print >>log, "%s <<< :%s 332 %s %s :%s" % (irc.timestamp(), window.context.serv, window.context.identity.nick, window.name, window.topic) - if window.topicsetby and window.topictime: - print >>log, "%s <<< :%s 333 %s %s %s %s" % (irc.timestamp(), window.context.serv, window.context.identity.nick, window.name, window.topicsetby, window.topictime) + for line in window.fmttopic(): + print >>log, "%s <<< %s" % (irc.timestamp(), line) if window.users: - secret = "s" in window.modes.keys() and window.modes["s"] - private = "p" in window.modes.keys() and window.modes["p"] - namesusers = [] - modes, symbols = window.context.supports["PREFIX"] - print >>log, "%s <<< :%s 353 %s %s %s :%s" % (irc.timestamp(), - window.context.serv, - window.context.identity.nick, - "@" if secret else ("*" if private else "="), - window.name, - " ".join(["".join([symbols[k] if modes[k] in window.modes.keys() and user in window.modes[modes[k]] else "" for k in xrange(len(modes))])+user.nick for user in window.users])) + for line in window.fmtnames(sort="mode"): + print >>log, "%s <<< %s" % (irc.timestamp(), line) if window.modes: - modes = window.modes.keys() - modestr = "".join([mode for mode in modes if mode not in window.context.supports["CHANMODES"][0]+window.context.supports["PREFIX"][0] and window.modes[mode]]) - params = " ".join([window.modes[mode] for mode in modes if mode in window.context.supports["CHANMODES"][1]+window.context.supports["CHANMODES"][2] and window.modes[mode]]) - print >>log, "%s <<< :%s 324 %s %s +%s %s" % (irc.timestamp(), window.context.serv, window.context.identity.nick, window.name, modestr, params) + print >>log, "%s <<< %s" % ( + irc.timestamp(), window.fmtmodes()) if window.created: - print >>log, "%s <<< :%s 329 %s %s %s" % (irc.timestamp(), window.context.serv, window.context.identity.nick, window.name, window.created) + print >>log, "%s <<< %s" % ( + 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 @@ -161,16 +194,17 @@ class Logger(Thread): del self.logs[other] self.logs[window] = log if window not in self.logs.keys(): - log = self.logs[window] = open(logname, "a") + log = self.logs[window] = codecs.open( + logname, "a", encoding="utf8") else: log = self.logs[window] print >>log, "%s ### Log file opened" % (irc.timestamp()) log.flush() def closeLog(self, window): - if window in self.logs.keys() and type(self.logs[window]) == file and not self.logs[window].closed: - print >>self.logs[window], "%s ### Log file closed" % ( - irc.timestamp()) + if window in self.logs.keys() and isinstance(self.logs[window], codecs.StreamReaderWriter) and not self.logs[window].closed: + print >>self.logs[ + window], "%s ### Log file closed" % (irc.timestamp()) self.logs[window].close() if window in self.logs.keys(): del self.logs[window] @@ -179,193 +213,216 @@ class Logger(Thread): self.closeLog(window) self.openLog(window) - def onConnectAttempt(self, IRC): - if IRC not in self.logs.keys() or (not self.logs[IRC]) or self.logs[IRC].closed: - self.openLog(IRC) + def onConnectAttempt(self, context): + if context not in self.logs.keys() or (not self.logs[context]) or self.logs[context].closed: + self.openLog(context) ts = irc.timestamp() - print >>self.logs[IRC], "%s *** Attempting connection to %s:%s." % ( - ts, IRC.server, IRC.port) + print >>self.logs[context], "%s *** Attempting connection to %s:%s." % ( + ts, context.server, context.port) - def onConnect(self, IRC): - if IRC not in self.logs.keys() or (not self.logs[IRC]) or self.logs[IRC].closed: - self.openLog(IRC) + def onConnect(self, context): + if context not in self.logs.keys() or (not self.logs[context]) or self.logs[context].closed: + self.openLog(context) ts = irc.timestamp() - print >>self.logs[IRC], "%s *** Connection to %s:%s established." % ( - ts, IRC.server, IRC.port) + print >>self.logs[context], "%s *** Connection to %s:%s established." % ( + ts, context.server, context.port) - def onConnectFail(self, IRC, exc, excmsg, tb): - ### Called when a connection attempt fails. - if IRC not in self.logs.keys() or (not self.logs[IRC]) or self.logs[IRC].closed: - self.openLog(IRC) + def onConnectFail(self, context, exc, excmsg, tb): + # Called when a connection attempt fails. + if context not in self.logs.keys() or (not self.logs[context]) or self.logs[context].closed: + self.openLog(context) ts = irc.timestamp() - print >>self.logs[IRC], "%s *** Connection to %s:%s failed: %s." % ( - ts, IRC.server, IRC.port, excmsg) + print >>self.logs[context], "%s *** Connection to %s:%s failed: %s." % ( + ts, context.server, context.port, excmsg) - def onDisconnect(self, IRC, expected=False): + def onDisconnect(self, context, expected=False): ts = irc.timestamp() for window in self.logs.keys(): - if type(window) in (irc.Channel, irc.User) and window.context == IRC: - print >>self.logs[window], "%s *** Connection to %s:%s terminated." % (ts, IRC.server, IRC.port) + if type(window) in (irc.Channel, irc.User) and window.context == context: + print >>self.logs[window], "%s *** Connection to %s:%s terminated." % ( + ts, context.server, context.port) self.logs[window].flush() self.closeLog(window) - print >>self.logs[IRC], "%s *** Connection %s:%s terminated." % ( - ts, IRC.server, IRC.port) - self.logs[IRC].flush() - self.closeLog(IRC) - - def onJoin(self, IRC, user, channel): - ### Called when somebody joins a channel, includes bot. - ts = irc.timestamp() - if user == IRC.identity: + print >>self.logs[context], "%s *** Connection %s:%s terminated." % ( + ts, context.server, context.port) + self.logs[context].flush() + self.closeLog(context) + + def onJoin(self, context, user, channel): + # Called when somebody joins a channel, includes bot. + if user == context.identity: self.openLog(channel) + ts = irc.timestamp() print >>self.logs[channel], "%s <<< :%s!%s@%s JOIN %s" % ( ts, user.nick, user.username, user.host, channel.name) self.logs[channel].flush() - def onChanMsg(self, IRC, user, channel, targetprefix, msg): - ### Called when someone sends a PRIVMSG to channel. + def onChanMsg(self, context, user, channel, targetprefix, msg): + # Called when someone sends a PRIVMSG to channel. ts = irc.timestamp() if type(user) == irc.User: - classes = " ".join([modemapping[mode] for mode in IRC.supports["PREFIX"][0] if mode in channel.modes.keys() and user in channel.modes[mode]]) + classes = " ".join([modemapping[mode] + for mode in context.supports["PREFIX"][0] if mode in channel.modes.keys() and user in channel.modes[mode]]) if classes: - print >>self.logs[channel], "%s %s <<< :%s!%s@%s PRIVMSG %s%s :%s" % (ts, classes, user.nick, user.username, user.host, targetprefix, channel.name, msg) + print >>self.logs[channel], "%s %s <<< :%s!%s@%s PRIVMSG %s%s :%s" % ( + ts, classes, user.nick, user.username, user.host, targetprefix, channel.name, msg) else: - print >>self.logs[channel], "%s <<< :%s!%s@%s PRIVMSG %s%s :%s" % (ts, user.nick, user.username, user.host, targetprefix, channel.name, msg) + print >>self.logs[channel], "%s <<< :%s!%s@%s PRIVMSG %s%s :%s" % ( + ts, user.nick, user.username, user.host, targetprefix, channel.name, msg) elif type(user) in (str, unicode): classes = "server" - print >>self.logs[channel], "%s %s <<< :%s PRIVMSG %s%s :%s" % (ts, - classes, user, targetprefix, channel.name, msg) + print >>self.logs[channel], "%s %s <<< :%s PRIVMSG %s%s :%s" % ( + ts, classes, user, targetprefix, channel.name, msg) self.logs[channel].flush() - def onChanAction(self, IRC, user, channel, targetprefix, action): - self.onChanMsg(IRC, user, channel, targetprefix, - "\x01ACTION %s\x01"%action) + def onChanAction(self, context, user, channel, targetprefix, action): + self.onChanMsg(context, user, channel, + targetprefix, "\x01ACTION %s\x01" % action) - def onChanNotice(self, IRC, origin, channel, targetprefix, msg): - ### Called when someone sends a NOTICE to channel. + def onChanNotice(self, context, origin, channel, targetprefix, msg): + # Called when someone sends a NOTICE to channel. ts = irc.timestamp() if type(origin) == irc.User: - classes = " ".join([modemapping[mode] for mode in IRC.supports["PREFIX"][0] if mode in channel.modes.keys() and origin in channel.modes[mode]]) + classes = " ".join([modemapping[mode] + for mode in context.supports["PREFIX"][0] if mode in channel.modes.keys() and origin in channel.modes[mode]]) if classes: - print >>self.logs[channel], "%s %s <<< :%s!%s@%s NOTICE %s%s :%s" % (ts, classes, origin.nick, origin.username, origin.host, targetprefix, channel.name, msg) + print >>self.logs[channel], "%s %s <<< :%s!%s@%s NOTICE %s%s :%s" % ( + ts, classes, origin.nick, origin.username, origin.host, targetprefix, channel.name, msg) else: - print >>self.logs[channel], "%s <<< :%s!%s@%s NOTICE %s%s :%s" % (ts, origin.nick, origin.username, origin.host, targetprefix, channel.name, msg) + print >>self.logs[channel], "%s <<< :%s!%s@%s NOTICE %s%s :%s" % ( + ts, origin.nick, origin.username, origin.host, targetprefix, channel.name, msg) elif type(origin) in (str, unicode): classes = "server" - print >>self.logs[channel], "%s %s <<< :%s NOTICE %s%s :%s" % (ts, - classes, origin, targetprefix, channel.name, msg) + print >>self.logs[channel], "%s %s <<< :%s NOTICE %s%s :%s" % ( + ts, classes, origin, targetprefix, channel.name, msg) self.logs[channel].flush() - def onPart(self, IRC, user, channel, partmsg): - ### Called when somebody parts the channel, includes bot. + def onPart(self, context, user, channel, partmsg): + # Called when somebody parts the channel, includes bot. ts = irc.timestamp() if partmsg: - print >>self.logs[channel], "%s <<< :%s!%s@%s PART %s :%s" % (ts, user.nick, user.username, user.host, channel.name, partmsg) + print >>self.logs[channel], "%s <<< :%s!%s@%s PART %s :%s" % ( + ts, user.nick, user.username, user.host, channel.name, partmsg) else: - print >>self.logs[channel], "%s <<< :%s!%s@%s PART %s" % (ts, - user.nick, user.username, user.host, channel.name) + print >>self.logs[channel], "%s <<< :%s!%s@%s PART %s" % ( + ts, user.nick, user.username, user.host, channel.name) self.logs[channel].flush() - if user == IRC.identity: + if user == context.identity: self.closeLog(channel) - def onKick(self, IRC, kicker, channel, kicked, kickmsg): - ### Called when somebody is kicked from the channel, includes bot. + def onKick(self, context, kicker, channel, kicked, kickmsg): + # Called when somebody is kicked from the channel, includes bot. ts = irc.timestamp() if kickmsg: - print >>self.logs[channel], "%s <<< :%s!%s@%s KICK %s %s :%s" % (ts, kicker.nick, kicker.username, kicker.host, channel.name, kicked.nick, kickmsg) + print >>self.logs[channel], "%s <<< :%s!%s@%s KICK %s %s :%s" % ( + ts, kicker.nick, kicker.username, kicker.host, channel.name, kicked.nick, kickmsg) else: - print >>self.logs[channel], "%s <<< :%s!%s@%s KICK %s %s" % (ts, user.nick, user.username, user.host, channel.name, kicked.nick) + print >>self.logs[channel], "%s <<< :%s!%s@%s KICK %s %s" % ( + ts, user.nick, user.username, user.host, channel.name, kicked.nick) self.logs[channel].flush() - if user == IRC.identity: + if kicked == context.identity: self.closeLog(channel) - def onSendChanMsg(self, IRC, origin, channel, targetprefix, msg): - ### Called when bot sends a PRIVMSG to channel. - ### The variable origin refers to a class instance voluntarily identifying itself as that which requested data be sent. + def onSendChanMsg(self, context, origin, channel, targetprefix, msg): + # Called when bot sends a PRIVMSG to channel. + # The variable origin refers to a class instance voluntarily + # identifying itself as that which requested data be sent. ts = irc.timestamp() - classes = " ".join([modemapping[mode] for mode in IRC.supports["PREFIX"][0] if mode in channel.modes.keys() and IRC.identity in channel.modes[mode]]) + classes = " ".join([modemapping[mode] + for mode in context.supports["PREFIX"][0] if mode in channel.modes.keys() and context.identity in channel.modes[mode]]) if classes: - print >>self.logs[channel], "%s %s >>> :%s!%s@%s PRIVMSG %s%s :%s" % (ts, classes, IRC.identity.nick, IRC.identity.username, IRC.identity.host, targetprefix, channel.name, msg) + print >>self.logs[channel], "%s %s >>> :%s!%s@%s PRIVMSG %s%s :%s" % ( + ts, classes, context.identity.nick, context.identity.username, context.identity.host, targetprefix, channel.name, msg) else: - print >>self.logs[channel], "%s >>> :%s!%s@%s PRIVMSG %s%s :%s" % (ts, IRC.identity.nick, IRC.identity.username, IRC.identity.host, targetprefix, channel.name, msg) + print >>self.logs[channel], "%s >>> :%s!%s@%s PRIVMSG %s%s :%s" % ( + ts, context.identity.nick, context.identity.username, context.identity.host, targetprefix, channel.name, msg) self.logs[channel].flush() - def onSendChanAction(self, IRC, origin, channel, targetprefix, action): - ### origin is the source of the channel message - ### Called when bot sends an action (/me) to channel. - ### The variable origin refers to a class instance voluntarily identifying itself as that which requested data be sent. - self.onSendChanMsg(IRC, origin, channel, targetprefix, - "\x01ACTION %s\x01"%action) + def onSendChanAction(self, context, origin, channel, targetprefix, action): + # origin is the source of the channel message + # Called when bot sends an action (/me) to channel. + # The variable origin refers to a class instance voluntarily + # identifying itself as that which requested data be sent. + self.onSendChanMsg( + context, origin, channel, targetprefix, "\x01ACTION %s\x01" % action) - def onPrivMsg(self, IRC, user, msg): - ### Called when someone sends a PRIVMSG to the bot. + def onPrivMsg(self, context, user, msg): + # Called when someone sends a PRIVMSG to the bot. if user not in self.logs.keys(): self.openLog(user) ts = irc.timestamp() - print >>self.logs[user], "%s <<< :%s!%s@%s PRIVMSG %s :%s" % (ts, user.nick, user.username, user.host, IRC.identity.nick, msg) + print >>self.logs[user], "%s <<< :%s!%s@%s PRIVMSG %s :%s" % ( + ts, user.nick, user.username, user.host, context.identity.nick, msg) self.logs[user].flush() - def onPrivNotice(self, IRC, origin, msg): - ### Called when someone sends a NOTICE to the bot. + def onPrivNotice(self, context, origin, msg): + # Called when someone sends a NOTICE to the bot. ts = irc.timestamp() if type(origin) == irc.User: if origin not in self.logs.keys(): self.openLog(origin) - print >>self.logs[origin], "%s <<< :%s!%s@%s NOTICE %s :%s" % (ts, origin.nick, origin.username, origin.host, IRC.identity.nick, msg) + ts = irc.timestamp() + print >>self.logs[origin], "%s <<< :%s!%s@%s NOTICE %s :%s" % ( + ts, origin.nick, origin.username, origin.host, context.identity.nick, msg) self.logs[origin].flush() else: - print >>self.logs[IRC], "%s <<< :%s NOTICE %s :%s" % ( - ts, origin, IRC.identity.nick, msg) - self.logs[IRC].flush() - - def onPrivAction(self, IRC, user, action): - ### Called when someone sends an action (/me) to the bot. - self.onPrivMsg(IRC, user, "\x01ACTION %s\x01"%action) - - def onSendPrivMsg(self, IRC, origin, user, msg): - ### Called when bot sends a PRIVMSG to a user. - ### The variable origin refers to a class instance voluntarily identifying itself as that which requested data be sent. + print >>self.logs[context], "%s <<< :%s NOTICE %s :%s" % ( + ts, origin, context.identity.nick, msg) + self.logs[context].flush() + + def onPrivAction(self, context, user, action): + # Called when someone sends an action (/me) to the bot. + self.onPrivMsg(context, user, "\x01ACTION %s\x01" % action) + + def onSendPrivMsg(self, context, origin, user, msg): + # Called when bot sends a PRIVMSG to a user. + # The variable origin refers to a class instance voluntarily + # identifying itself as that which requested data be sent. if user not in self.logs.keys(): self.openLog(user) ts = irc.timestamp() - print >>self.logs[user], "%s >>> :%s!%s@%s PRIVMSG %s :%s" % (ts, IRC.identity.nick, IRC.identity.username, IRC.identity.host, user.nick, msg) + print >>self.logs[user], "%s >>> :%s!%s@%s PRIVMSG %s :%s" % ( + ts, context.identity.nick, context.identity.username, context.identity.host, user.nick, msg) self.logs[user].flush() - def onSendPrivAction(self, IRC, origin, user, action): - ### Called when bot sends an action (/me) to a user. - ### The variable origin refers to a class instance voluntarily identifying itself as that which requested data be sent. - self.onSendPrivMsg(IRC, origin, user, "\x01ACTION %s\x01"%action) + def onSendPrivAction(self, context, origin, user, action): + # Called when bot sends an action (/me) to a user. + # The variable origin refers to a class instance voluntarily + # identifying itself as that which requested data be sent. + self.onSendPrivMsg(context, origin, user, "\x01ACTION %s\x01" % action) - def onNickChange(self, IRC, user, newnick): - ### Called when somebody changes nickname. + def onNickChange(self, context, user, newnick): + # Called when somebody changes nickname. ts = irc.timestamp() line = "%s <<< :%s!%s@%s NICK %s" % ( ts, user.nick, user.username, user.host, newnick) - ### Print nick change in each channel the user is in. + # Print nick change in each channel the user is in. for channel in user.channels: - print >>self.logs[channel], line - self.logs[channel].flush() + if channel in context.identity.channels: + print >>self.logs[channel], line + self.logs[channel].flush() - ### And in the query if open. + # And in the query if open. if user in self.logs.keys(): print >>self.logs[user], line self.logs[user].flush() - def onMeNickChange(self, IRC, newnick): - ### Called when the bot changes nickname. + def onMeNickChange(self, context, newnick): + # Called when the bot changes nickname. - ### Print nick change to all open queries, except for query with self (already done with onNickChange). + # Print nick change to all open queries, except for query with self + # (already done with onNickChange). ts = irc.timestamp() - line = "%s <<< :%s!%s@%s NICK %s" % (ts, IRC.identity.nick, - IRC.identity.username, IRC.identity.host, newnick) + line = "%s <<< :%s!%s@%s NICK %s" % ( + ts, context.identity.nick, context.identity.username, context.identity.host, newnick) for (window, log) in self.logs.items(): - if type(window) == irc.User and window != IRC.identity: + if type(window) == irc.User and window != context.identity: print >>log, line log.flush() - def onQuit(self, IRC, user, quitmsg): - ### Called when somebody quits IRC. + def onQuit(self, context, user, quitmsg): + # Called when somebody quits context. ts = irc.timestamp() if quitmsg: line = "%s <<< :%s!%s@%s QUIT :%s" % ( @@ -374,169 +431,178 @@ class Logger(Thread): line = "%s <<< :%s!%s@%s QUIT" % ( ts, user.nick, user.username, user.host) - ### Print quit in each channel the user was in. + # Print quit in each channel the user was in. for channel in user.channels: - if channel in self.logs.keys() and not self.logs[channel].closed: + if channel in context.identity.channels and channel in self.logs.keys() and not self.logs[channel].closed: print >>self.logs[channel], line self.logs[channel].flush() - ### And in the query if open. + # And in the query if open. if user in self.logs.keys(): print >>self.logs[user], line self.logs[user].flush() self.closeLog(user) - def onNames(self, IRC, origin, channel, flag, channame, nameslist): - ### Called when a NAMES list is received. + def onNames(self, context, origin, channel, flag, channame, nameslist): + # Called when a NAMES list is received. if channel in self.logs.keys() and not self.logs[channel].closed: log = self.logs[channel] else: - log = self.logs[IRC] + log = self.logs[context] ts = irc.timestamp() secret = "s" in channel.modes.keys() and channel.modes["s"] private = "p" in channel.modes.keys() and channel.modes["p"] modes, symbols = channel.context.supports["PREFIX"] - print >>log, "%s <<< :%s 353 %s %s %s :%s" % (ts, origin, IRC.identity.nick, flag, channame, - " ".join(["%s%s!%s@%s"%(prefix, nick, username, host) if username and host else "%s%s"%(prefix, nick) for (prefix, nick, username, host) in nameslist])) + print >>log, "%s <<< :%s 353 %s %s %s :%s" % (ts, origin, context.identity.nick, flag, channame, + " ".join(["%s%s!%s@%s" % (prefix, nick, username, host) if username and host else "%s%s" % (prefix, nick) for (prefix, nick, username, host) in nameslist])) log.flush() - def onNamesEnd(self, IRC, origin, channel, channame, endmsg): + def onNamesEnd(self, context, origin, channel, channame, endmsg): if channel in self.logs.keys() and not self.logs[channel].closed: log = self.logs[channel] else: - log = self.logs[IRC] + log = self.logs[context] ts = irc.timestamp() print >>log, "%s <<< :%s 366 %s %s :%s" % ( - ts, origin, IRC.identity.nick, channame, endmsg) + ts, origin, context.identity.nick, channame, endmsg) log.flush() - def onWhoisStart(self, IRC, origin, user, nickname, username, host, realname): - ### Called when a WHOIS reply is received. + def onWhoisStart(self, context, origin, user, nickname, username, host, realname): + # Called when a WHOIS reply is received. if user not in self.logs.keys(): self.openLog(user) - print >>self.logs[user], "%s <<< :%s 311 %s %s %s %s * :%s" % (irc.timestamp(), origin, IRC.identity.nick, nickname, username, host, realname) + print >>self.logs[user], "%s <<< :%s 311 %s %s %s %s * :%s" % ( + irc.timestamp(), origin, context.identity.nick, nickname, username, host, realname) - def onWhoisRegisteredNick(self, IRC, origin, user, nickname, msg): - ### Called when a WHOIS reply is received. + def onWhoisRegisteredNick(self, context, origin, user, nickname, msg): + # Called when a WHOIS reply is received. if user not in self.logs.keys(): self.openLog(user) print >>self.logs[user], "%s <<< :%s 307 %s %s :%s" % ( - irc.timestamp(), origin, IRC.identity.nick, nickname, msg) + irc.timestamp(), origin, context.identity.nick, nickname, msg) - def onWhoisAway(self, IRC, origin, user, nickname, awaymsg): - ### Called when a WHOIS reply is received. + def onWhoisAway(self, context, origin, user, nickname, awaymsg): + # Called when a WHOIS reply is received. if user not in self.logs.keys(): self.openLog(user) - print >>self.logs[user], "%s <<< :%s 301 %s %s :%s" % (irc.timestamp( - ), origin, IRC.identity.nick, nickname, awaymsg) + print >>self.logs[user], "%s <<< :%s 301 %s %s :%s" % ( + irc.timestamp(), origin, context.identity.nick, nickname, awaymsg) - def onWhoisConnectingFrom(self, IRC, origin, user, nickname, msg): - ### Called when a WHOIS reply is received. + def onWhoisConnectingFrom(self, context, origin, user, nickname, msg): + # Called when a WHOIS reply is received. if user not in self.logs.keys(): self.openLog(user) print >>self.logs[user], "%s <<< :%s 378 %s %s :%s" % ( - irc.timestamp(), origin, IRC.identity.nick, nickname, msg) + irc.timestamp(), origin, context.identity.nick, nickname, msg) - def onWhoisChannels(self, IRC, origin, user, nickname, chanlist): - ### Called when a WHOIS reply is received. + def onWhoisChannels(self, context, origin, user, nickname, chanlist): + # Called when a WHOIS reply is received. if user not in self.logs.keys(): self.openLog(user) - print >>self.logs[user], "%s <<< :%s 319 %s %s :%s" % (irc.timestamp(), - origin, IRC.identity.nick, nickname, " ".join(chanlist)) + print >>self.logs[user], "%s <<< :%s 319 %s %s :%s" % ( + irc.timestamp(), origin, context.identity.nick, nickname, " ".join(chanlist)) - def onWhoisAvailability(self, IRC, origin, user, nickname, msg): - ### Called when a WHOIS reply is received. + def onWhoisAvailability(self, context, origin, user, nickname, msg): + # Called when a WHOIS reply is received. if user not in self.logs.keys(): self.openLog(user) print >>self.logs[user], "%s <<< :%s 310 %s %s :%s" % ( - irc.timestamp(), origin, IRC.identity.nick, nickname, msg) + irc.timestamp(), origin, context.identity.nick, nickname, msg) - def onWhoisServer(self, IRC, origin, user, nickname, server, servername): - ### Called when a WHOIS reply is received. + def onWhoisServer(self, context, origin, user, nickname, server, servername): + # Called when a WHOIS reply is received. if user not in self.logs.keys(): self.openLog(user) - print >>self.logs[user], "%s <<< :%s 312 %s %s %s :%s" % (irc.timestamp(), origin, IRC.identity.nick, nickname, server, servername) + print >>self.logs[user], "%s <<< :%s 312 %s %s %s :%s" % ( + irc.timestamp(), origin, context.identity.nick, nickname, server, servername) - def onWhoisOp(self, IRC, origin, user, nickname, msg): + def onWhoisOp(self, context, origin, user, nickname, msg): if user not in self.logs.keys(): self.openLog(user) print >>self.logs[user], "%s <<< :%s 313 %s %s :%s" % ( - irc.timestamp(), origin, IRC.identity.nick, nickname, msg) + irc.timestamp(), origin, context.identity.nick, nickname, msg) - def onWhoisTimes(self, IRC, origin, user, nickname, idletime, signontime, msg): + def onWhoisTimes(self, context, origin, user, nickname, idletime, signontime, msg): if user not in self.logs.keys(): self.openLog(user) - print >>self.logs[user], "%s <<< :%s 317 %s %s %d %d :%s" % (irc.timestamp(), origin, IRC.identity.nick, nickname, idletime, signontime, msg) + print >>self.logs[user], "%s <<< :%s 317 %s %s %d %d :%s" % ( + irc.timestamp(), origin, context.identity.nick, nickname, idletime, signontime, msg) - def onWhoisSSL(self, IRC, origin, user, nickname, msg): + def onWhoisSSL(self, context, origin, user, nickname, msg): if user not in self.logs.keys(): self.openLog(user) print >>self.logs[user], "%s <<< :%s 671 %s %s :%s" % ( - irc.timestamp(), origin, IRC.identity.nick, nickname, msg) + irc.timestamp(), origin, context.identity.nick, nickname, msg) - def onWhoisModes(self, IRC, origin, user, nickname, msg): + def onWhoisModes(self, context, origin, user, nickname, msg): if user not in self.logs.keys(): self.openLog(user) print >>self.logs[user], "%s <<< :%s 339 %s %s :%s" % ( - irc.timestamp(), origin, IRC.identity.nick, nickname, msg) + irc.timestamp(), origin, context.identity.nick, nickname, msg) - def onWhoisLoggedInAs(self, IRC, origin, user, nickname, loggedinas, msg): + def onWhoisLoggedInAs(self, context, origin, user, nickname, loggedinas, msg): if user not in self.logs.keys(): self.openLog(user) - print >>self.logs[user], "%s <<< :%s 330 %s %s %s :%s" % (irc.timestamp(), origin, IRC.identity.nick, nickname, loggedinas, msg) + print >>self.logs[user], "%s <<< :%s 330 %s %s %s :%s" % ( + irc.timestamp(), origin, context.identity.nick, nickname, loggedinas, msg) - def onWhoisEnd(self, IRC, origin, user, nickname, msg): + def onWhoisEnd(self, context, origin, user, nickname, msg): if user not in self.logs.keys(): self.openLog(user) print >>self.logs[user], "%s <<< :%s 318 %s %s :%s" % ( - irc.timestamp(), origin, IRC.identity.nick, nickname, msg) + irc.timestamp(), origin, context.identity.nick, nickname, msg) self.logs[user].flush() - def onWhoEntry(self, IRC, **kwargs): - ### Called when a WHO list is received. + def onWhoEntry(self, context, **kwargs): + # Called when a WHO list is received. pass - def onWhoEnd(self, IRC, **kwargs): - ### Called when a WHO list is received. + def onWhoEnd(self, context, **kwargs): + # Called when a WHO list is received. pass - def onList(self, IRC, chanlistbegin, chanlist, endmsg): - ### Called when a channel list is received. + def onList(self, context, chanlistbegin, chanlist, endmsg): + # Called when a channel list is received. pass - def onTopic(self, IRC, origin, channel, topic): - ### Called when channel topic is received via 332 response. + def onTopic(self, context, origin, channel, topic): + # Called when channel topic is received via 332 response. ts = irc.timestamp() if channel in self.logs.keys() and not self.logs[channel].closed: log = self.logs[channel] else: - log = self.logs[IRC] + log = self.logs[context] print >>log, "%s <<< :%s 332 %s %s :%s" % ( - ts, origin, IRC.identity.nick, channel.name, topic) + ts, origin, context.identity.nick, channel.name, topic) log.flush() - def onTopicInfo(self, IRC, origin, channel, topicsetby, topictime): - ### Called when channel topic info is received via 333 response. + def onTopicInfo(self, context, origin, channel, topicsetby, topictime): + # Called when channel topic info is received via 333 response. ts = irc.timestamp() if channel in self.logs.keys() and not self.logs[channel].closed: log = self.logs[channel] else: - log = self.logs[IRC] - print >>log, "%s <<< :%s 333 %s %s %s %d" % (ts, origin, - IRC.identity.nick, channel.name, topicsetby, topictime) + log = self.logs[context] + print >>log, "%s <<< :%s 333 %s %s %s %d" % ( + ts, origin, context.identity.nick, channel.name, topicsetby, topictime) log.flush() - def onTopicSet(self, IRC, user, channel, topic): - ### Called when channel topic is changed. + def onTopicSet(self, context, user, channel, topic): + # Called when channel topic is changed. ts = irc.timestamp() - print >>self.logs[channel], "%s <<< :%s!%s@%s TOPIC %s :%s" % (ts, - user.nick, user.username, user.host, channel.name, topic) + if type(user) == irc.User: + print >>self.logs[channel], "%s <<< :%s!%s@%s TOPIC %s :%s" % ( + ts, user.nick, user.username, user.host, channel.name, topic) + else: + print >>self.logs[channel], "%s <<< :%s TOPIC %s :%s" % ( + ts, user, channel.name, topic) self.logs[channel].flush() - def onChanModeSet(self, IRC, user, channel, modedelta): - ### Called when channel modes are changed. - ### modedelta is a list of tuples of the format ("+x", parameter), ("+x", None) if no parameter is provided. + def onChanModeSet(self, context, user, channel, modedelta): + # Called when channel modes are changed. + # modedelta is a list of tuples of the format ("+x", parameter), ("+x", + # None) if no parameter is provided. ts = irc.timestamp() modestr = "" params = [] @@ -546,28 +612,31 @@ class Logger(Thread): modestr += sgn sign = sgn modestr += modechar - if param is not None: + if param != None: params.append(param.nick if type(param) == irc.User else param) if len(params): if type(user) == irc.User: - print >>self.logs[channel], "%s <<< :%s!%s@%s MODE %s %s %s" % (ts, user.nick, user.username, user.host, channel.name, modestr, " ".join(params)) + print >>self.logs[channel], "%s <<< :%s!%s@%s MODE %s %s %s" % ( + ts, user.nick, user.username, user.host, channel.name, modestr, " ".join(params)) else: - print >>self.logs[channel], "%s <<< :%s MODE %s %s %s" % (ts, user, channel.name, modestr, " ".join(params)) + print >>self.logs[channel], "%s <<< :%s MODE %s %s %s" % ( + ts, user, channel.name, modestr, " ".join(params)) else: if type(user) == irc.User: - print >>self.logs[channel], "%s <<< :%s!%s@%s MODE %s %s" % (ts, user.nick, user.username, user.host, channel.name, modestr) + print >>self.logs[channel], "%s <<< :%s!%s@%s MODE %s %s" % ( + ts, user.nick, user.username, user.host, channel.name, modestr) else: print >>self.logs[channel], "%s <<< :%s MODE %s %s" % ( ts, user, channel.name, modestr) self.logs[channel].flush() - def onChannelModes(self, IRC, channel, modedelta): - ### Called when channel modes are received via 324 response. + def onChannelModes(self, context, origin, channel, modedelta): + # Called when channel modes are received via 324 response. ts = irc.timestamp() if channel in self.logs.keys() and not self.logs[channel].closed: log = self.logs[channel] else: - log = self.logs[IRC] + log = self.logs[context] modestr = "" params = [] sign = "" @@ -576,27 +645,28 @@ class Logger(Thread): modestr += sgn sign = sgn modestr += modechar - if param is not None: + if param != None: params.append(param) if len(params): - print >>log, "%s <<< :%s 324 %s %s %s %s" % (ts, IRC.serv, IRC.identity.nick, channel.name, modestr, " ".join(params)) + print >>log, "%s <<< :%s 324 %s %s %s %s" % ( + ts, origin, context.identity.nick, channel.name, modestr, " ".join(params)) else: - print >>log, "%s <<< :%s 324 %s %s %s" % (ts, IRC.serv, - IRC.identity.nick, channel.name, modestr) + print >>log, "%s <<< :%s 324 %s %s %s" % ( + ts, origin, context.identity.nick, channel.name, modestr) log.flush() - def onChanCreated(self, IRC, channel, created): - ### Called when a 329 response is received. + def onChanCreated(self, context, origin, channel, created): + # Called when a 329 response is received. ts = irc.timestamp() if channel in self.logs.keys() and not self.logs[channel].closed: log = self.logs[channel] else: - log = self.logs[IRC] + log = self.logs[context] print >>log, "%s <<< :%s 329 %s %s %d" % ( - ts, IRC.serv, IRC.identity.nick, channel.name, created) + ts, origin, context.identity.nick, channel.name, created) log.flush() - def onUnhandled(self, IRC, line, origin, cmd, target, params, extinfo): + def onUnhandled(self, context, line, origin, cmd, target, params, extinfo, targetprefix): ts = irc.timestamp() - print >>self.logs[IRC], "%s <<< %s" % (ts, line) - self.logs[IRC].flush() + print >>self.logs[context], "%s <<< %s" % (ts, line) + self.logs[context].flush() diff --git a/modjson.py b/modjson.py new file mode 100644 index 0000000..d52237e --- /dev/null +++ b/modjson.py @@ -0,0 +1,839 @@ +import json +import inspect +import re +import importlib +import collections +import inspect +import new + +from json.decoder import errmsg +from json.encoder import py_encode_basestring_ascii, ESCAPE, ESCAPE_ASCII, HAS_UTF8, ESCAPE_DCT, INFINITY, FLOAT_REPR, encode_basestring, encode_basestring_ascii + +FLAGS = re.VERBOSE | re.MULTILINE | re.DOTALL + +NUMBER_RE = re.compile(r'(-?(?:0|[1-9]\d*))(\.\d+)?([eE][-+]?\d+)?', FLAGS) +REF_RE = re.compile( + r'<([A-Z0-9_]+(?:\[[0-9]+(?:,[0-9]+)*\])?(?:\.[A-Z0-9_]+(?:\[[0-9]+(?:,[0-9]+)*\])?)*)>', flags=FLAGS | re.I) +PATH_RE = re.compile( + r'([A-Z0-9_]+)(?:\[([0-9]+(?:,[0-9]+)*)\])?', flags=FLAGS | re.I) +WHITESPACE = re.compile(r'[ \t\n\r]*', FLAGS) +WHITESPACE_STR = ' \t\n\r' + + +match_number = NUMBER_RE.match +match_reference = REF_RE.match + + +datetime_regex = re.compile( + '\"dt\((\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})Z\)\"') +timedelta_regex = re.compile('\"td\((\d+)\)\"') + + + #parse_object = context.parse_object + #parse_array = context.parse_array + #parse_string = context.parse_string + #match_number = NUMBER_RE.match + #match_reference = REF_RE.match + #encoding = context.encoding + #strict = context.strict + #parse_float = context.parse_float + #parse_int = context.parse_int + #parse_constant = context.parse_constant + #object_hook = context.object_hook + #object_pairs_hook = context.object_pairs_hook +class ModJSONDecoder(json.JSONDecoder): + + def __init__(self, encoding=None, object_hook=None, parse_float=None, + parse_int=None, parse_constant=None, strict=True, + object_pairs_hook=None): + """``encoding`` determines the encoding used to interpret any ``str`` + objects decoded by this instance (utf-8 by default). It has no + effect when decoding ``unicode`` objects. + + Note that currently only encodings that are a superset of ASCII work, + strings of other encodings should be passed in as ``unicode``. + + ``object_hook``, if specified, will be called with the result + of every JSON object decoded and its return value will be used in + place of the given ``dict``. This can be used to provide custom + deserializations (e.g. to support JSON-RPC class hinting). + + ``object_pairs_hook``, if specified will be called with the result of + every JSON object decoded with an ordered list of pairs. The return + value of ``object_pairs_hook`` will be used instead of the ``dict``. + This feature can be used to implement custom decoders that rely on the + order that the key and value pairs are decoded (for example, + collections.OrderedDict will remember the order of insertion). If + ``object_hook`` is also defined, the ``object_pairs_hook`` takes + priority. + + ``parse_float``, if specified, will be called with the string + of every JSON float to be decoded. By default this is equivalent to + float(num_str). This can be used to use another datatype or parser + for JSON floats (e.g. decimal.Decimal). + + ``parse_int``, if specified, will be called with the string + of every JSON int to be decoded. By default this is equivalent to + int(num_str). This can be used to use another datatype or parser + for JSON integers (e.g. float). + + ``parse_constant``, if specified, will be called with one of the + following strings: -Infinity, Infinity, NaN. + This can be used to raise an exception if invalid JSON numbers + are encountered. + + If ``strict`` is false (true is the default), then control + characters will be allowed inside strings. Control characters in + this context are those with character codes in the 0-31 range, + including ``'\\t'`` (tab), ``'\\n'``, ``'\\r'`` and ``'\\0'``. + + """ + self.encoding = encoding + self.object_pairs_hook = object_pairs_hook + self.parse_float = parse_float or float + self.parse_int = parse_int or int + self.parse_constant = parse_constant or json.decoder._CONSTANTS.__getitem__ + self.strict = strict + self.parse_string = json.decoder.scanstring + self.object_dict_hook = None + + def object_hook(self, d): + if 'class' in d: + class_path = d.pop('class') + modname, clsname = class_path.rsplit(".", 1) + #module_name = d.pop('__module__') + module = __import__(modname) + class_ = getattr(module, clsname) + args = dict((key.encode('ascii'), value) + for key, value in d.items()) + inst = class_(**args) + else: + inst = d + return inst + + def parse_object(self, s_and_end, root, working, _w=WHITESPACE.match, _ws=WHITESPACE_STR): + s, end = s_and_end + pairs = [] + pairs_append = pairs.append + # Use a slice to prevent IndexError from being raised, the following + # check will raise a more specific ValueError if the string is empty + nextchar = s[end:end + 1] + # Normally we expect nextchar == '"' + if nextchar != '"': + if nextchar in _ws: + end = _w(s, end).end() + nextchar = s[end:end + 1] + # Trivial empty object + if nextchar == '}': + if self.object_dict_hook is not None: + result = self.object_dict_hook(working) + return result, end + 1 + if self.object_hook is not None: + working = self.object_hook(working) + return working, end + 1 + elif nextchar != '"': + raise ValueError(errmsg( + "Expecting property name enclosed in double quotes", s, end)) + end += 1 + while True: + key, end = self.parse_string(s, end) + + # To skip some function call overhead we optimize the fast paths where + # the JSON key separator is ": " or just ":". + if s[end:end + 1] != ':': + end = _w(s, end).end() + if s[end:end + 1] != ':': + raise ValueError(errmsg("Expecting ':' delimiter", s, end)) + end += 1 + + try: + if s[end] in _ws: + end += 1 + if s[end] in _ws: + end = _w(s, end + 1).end() + except IndexError: + pass + + try: + nextchar = s[end] + except IndexError: + raise ValueError(errmsg("Expecting object", s, end)) + + if nextchar == '{': + nextitem = {} + elif nextchar == '[': + nextitem = [] + else: + nextitem = None + working[key] = nextitem + + try: + value, end = self.scan_once(s, end, root, nextitem) + except StopIteration: + raise ValueError(errmsg("Expecting object", s, end)) + # pairs_append((key, value)) + working[key] = value + + try: + nextchar = s[end] + if nextchar in _ws: + end = _w(s, end + 1).end() + nextchar = s[end] + except IndexError: + nextchar = '' + end += 1 + + if nextchar == '}': + break + elif nextchar != ',': + raise ValueError(errmsg("Expecting ',' delimiter", s, end - 1)) + + try: + nextchar = s[end] + if nextchar in _ws: + end += 1 + nextchar = s[end] + if nextchar in _ws: + end = _w(s, end + 1).end() + nextchar = s[end] + except IndexError: + nextchar = '' + + end += 1 + if nextchar != '"': + raise ValueError(errmsg( + "Expecting property name enclosed in double quotes", s, end - 1)) + if self.object_pairs_hook is not None: + result = self.object_dict_hook(dict) + return result, end + if self.object_hook is not None: + working = self.object_hook(working) + return working, end + + def parse_array(self, s_and_end, root, working, _w=WHITESPACE.match, _ws=WHITESPACE_STR): + s, end = s_and_end + nextchar = s[end:end + 1] + if nextchar in _ws: + end = _w(s, end + 1).end() + nextchar = s[end:end + 1] + # Look-ahead for trivial empty array + if nextchar == ']': + return working, end + 1 + _append = working.append + while True: + try: + nextchar = s[end] + except IndexError: + raise ValueError(errmsg("Expecting object", s, end)) + + if nextchar == '{': + nextitem = {} + elif nextchar == '[': + nextitem = [] + else: + nextitem = None + _append(nextitem) + + try: + value, end = self.scan_once(s, end, root, nextitem) + except StopIteration: + raise ValueError(errmsg("Expecting object", s, end)) + if value is not nextitem: + del working[-1] + _append(value) + nextchar = s[end:end + 1] + if nextchar in _ws: + end = _w(s, end + 1).end() + nextchar = s[end:end + 1] + end += 1 + if nextchar == ']': + break + elif nextchar != ',': + raise ValueError(errmsg("Expecting ',' delimiter", s, end)) + try: + if s[end] in _ws: + end += 1 + if s[end] in _ws: + end = _w(s, end + 1).end() + except IndexError: + pass + + return working, end + + def scan_once(self, string, idx, root, working): + try: + nextchar = string[idx] + except IndexError: + raise StopIteration + + if nextchar == '"': + return self.parse_string(string, idx + 1) + elif nextchar == '{': + return self.parse_object((string, idx + 1), root, working) + elif nextchar == '[': + return self.parse_array((string, idx + 1), root, working) + elif nextchar == 'n' and string[idx:idx + 4] == 'null': + return None, idx + 4 + elif nextchar == 't' and string[idx:idx + 4] == 'true': + return True, idx + 4 + elif nextchar == 'f' and string[idx:idx + 5] == 'false': + return False, idx + 5 + m = match_number(string, idx) + if m is not None: + integer, frac, exp = m.groups() + if frac or exp: + res = self.parse_float(integer + (frac or '') + (exp or '')) + else: + res = self.parse_int(integer) + return res, m.end() + r = match_reference(string, idx) + if r is not None: + refname = r.groups() + obj = root + for name in refname[0].split("."): + name, indices = PATH_RE.match(name).groups() + if name: + if type(obj) == dict: + obj = obj[name] + elif type(obj) == list: + obj = obj[int(name)] + else: + obj = getattr(obj, name) + if indices: + for index in indices.split("."): + obj = obj[int(index)] + return obj, r.end() + elif nextchar == 'N' and string[idx:idx + 3] == 'NaN': + return self.parse_constant('NaN'), idx + 3 + elif nextchar == 'I' and string[idx:idx + 8] == 'Infinity': + return self.parse_constant('Infinity'), idx + 8 + elif nextchar == '-' and string[idx:idx + 9] == '-Infinity': + return self.parse_constant('-Infinity'), idx + 9 + else: + raise StopIteration + + def decode(self, s, _w=WHITESPACE.match): + """Return the Python representation of ``s`` (a ``str`` or ``unicode`` + instance containing a JSON document) + + """ + obj, end = self.raw_decode(s, idx=_w(s, 0).end()) + end = _w(s, end).end() + if end != len(s): + raise ValueError(errmsg("Extra data", s, end, len(s))) + return obj + + def raw_decode(self, s, idx=0): + """Decode a JSON document from ``s`` (a ``str`` or ``unicode`` + beginning with a JSON document) and return a 2-tuple of the Python + representation and the index in ``s`` where the document ended. + + This can be used to decode a JSON document from a string that may + have extraneous data at the end. + + """ + try: + nextchar = s[idx] + except IndexError: + raise ValueError(errmsg("Expecting object", s, idx)) + + if nextchar == '{': + root = {} + elif nextchar == '[': + root = [] + else: + root = None + + try: + obj, end = self.scan_once(s, idx, root, root) + except StopIteration: + raise ValueError("No JSON object could be decoded") + return obj, end + + +class ModJSONEncoder(object): + + """Extensible JSON <http://json.org> encoder for Python data structures. + + Supports the following objects and types by default: + + +-------------------+---------------+ + | Python | JSON | + +===================+===============+ + | dict | object | + +-------------------+---------------+ + | list, tuple | array | + +-------------------+---------------+ + | str, unicode | string | + +-------------------+---------------+ + | int, long, float | number | + +-------------------+---------------+ + | True | true | + +-------------------+---------------+ + | False | false | + +-------------------+---------------+ + | None | null | + +-------------------+---------------+ + + To extend this to recognize other objects, subclass and implement a + ``.default()`` method with another method that returns a serializable + object for ``o`` if possible, otherwise it should call the superclass + implementation (to raise ``TypeError``). + + """ + item_separator = ', ' + key_separator = ': ' + + def __init__(self, skipkeys=False, ensure_ascii=True, + check_circular=True, allow_nan=True, sort_keys=False, + indent=None, separators=None, encoding='utf-8', default=None): + """Constructor for JSONEncoder, with sensible defaults. + + If skipkeys is false, then it is a TypeError to attempt + encoding of keys that are not str, int, long, float or None. If + skipkeys is True, such items are simply skipped. + + If *ensure_ascii* is true (the default), all non-ASCII + characters in the output are escaped with \uXXXX sequences, + and the results are str instances consisting of ASCII + characters only. If ensure_ascii is False, a result may be a + unicode instance. This usually happens if the input contains + unicode strings or the *encoding* parameter is used. + + If check_circular is true, then lists, dicts, and custom encoded + objects will be checked for circular references during encoding to + prevent an infinite recursion (which would cause an OverflowError). + Otherwise, no such check takes place. + + If allow_nan is true, then NaN, Infinity, and -Infinity will be + encoded as such. This behavior is not JSON specification compliant, + but is consistent with most JavaScript based encoders and decoders. + Otherwise, it will be a ValueError to encode such floats. + + If sort_keys is true, then the output of dictionaries will be + sorted by key; this is useful for regression tests to ensure + that JSON serializations can be compared on a day-to-day basis. + + If indent is a non-negative integer, then JSON array + elements and object members will be pretty-printed with that + indent level. An indent level of 0 will only insert newlines. + None is the most compact representation. Since the default + item separator is ', ', the output might include trailing + whitespace when indent is specified. You can use + separators=(',', ': ') to avoid this. + + If specified, separators should be a (item_separator, key_separator) + tuple. The default is (', ', ': '). To get the most compact JSON + representation you should specify (',', ':') to eliminate whitespace. + + If specified, default is a function that gets called for objects + that can't otherwise be serialized. It should return a JSON encodable + version of the object or raise a ``TypeError``. + + If encoding is not None, then all input strings will be + transformed into unicode using that encoding prior to JSON-encoding. + The default is UTF-8. + + """ + + self.skipkeys = skipkeys + self.ensure_ascii = ensure_ascii + self.check_circular = check_circular + self.allow_nan = allow_nan + self.sort_keys = sort_keys + self.indent = indent + if separators is not None: + self.item_separator, self.key_separator = separators + if default is not None: + self.default = default + self.encoding = encoding + + if self.ensure_ascii: + self._encoder = encode_basestring_ascii + else: + self._encoder = encode_basestring + if self.encoding != 'utf-8': + def _encoder(o, _orig_encoder=self._encoder, _encoding=self.encoding): + if isinstance(o, str): + o = o.decode(_encoding) + return _orig_encoder(o) + self._encoder = _encoder + + def default(self, o, refs, path): + """Implement this method in a subclass such that it returns + a serializable object for ``o``, or calls the base implementation + (to raise a ``TypeError``). + + For example, to support arbitrary iterators, you could + implement default like this:: + + def default(self, o): + try: + iterable = iter(o) + except TypeError: + pass + else: + return list(iterable) + # Let the base class default method raise the TypeError + return JSONEncoder.default(self, o) + + """ + 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:]: + if type(index) == int: + numindices.append(str(index)) + else: + if numindices: + pathstr += "[%s]" % (",".join(numindices)) + numindices = [] + pathstr += ".%s" % index + if numindices: + pathstr += "[%s]" % (",".join(numindices)) + numindices = [] + if pathstr not in refs.keys(): + refs[pathstr] = o + + return conf + + def encode(self, o): + """Return a JSON string representation of a Python data structure. + + >>> JSONEncoder().encode({"foo": ["bar", "baz"]}) + '{"foo": ["bar", "baz"]}' + + """ + # This is for extremely simple cases and benchmarks. + if isinstance(o, basestring): + if isinstance(o, str): + _encoding = self.encoding + if (_encoding is not None + and not (_encoding == 'utf-8')): + o = o.decode(_encoding) + if self.ensure_ascii: + return encode_basestring_ascii(o) + else: + return encode_basestring(o) + # This doesn't pass the iterator directly to ''.join() because the + # exceptions aren't as detailed. The list call should be roughly + # equivalent to the PySequence_Fast that ''.join() would do. + + chunks = self.iterencode(o, {}, _one_shot=True) + if not isinstance(chunks, (list, tuple)): + chunks = list(chunks) + return ''.join(chunks) + + def iterencode(self, o, refs, _one_shot=False): + """Encode the given object and yield each string + representation as available. + + For example:: + + for chunk in JSONEncoder().iterencode(bigobject): + mysocket.write(chunk) + + """ + if self.check_circular: + markers = {} + else: + markers = None + + def floatstr(o, allow_nan=self.allow_nan, + _repr=FLOAT_REPR, _inf=INFINITY, _neginf=-INFINITY): + # Check for specials. Note that this type of test is processor + # and/or platform-specific, so do tests which don't depend on the + # internals. + + if o != o: + text = 'NaN' + elif o == _inf: + text = 'Infinity' + elif o == _neginf: + text = '-Infinity' + else: + return _repr(o) + + if not allow_nan: + raise ValueError( + "Out of range float values are not JSON compliant: " + + repr(o)) + + return text + + # if (_one_shot and c_make_encoder is not None + # and self.indent is None and not self.sort_keys): + #_iterencode = c_make_encoder( + #markers, self.default, _encoder, self.indent, + #self.key_separator, self.item_separator, self.sort_keys, + # self.skipkeys, self.allow_nan) + # else: + #_iterencode = _make_iterencode( + #markers, self.default, _encoder, self.indent, floatstr, + #self.key_separator, self.item_separator, self.sort_keys, + # self.skipkeys, _one_shot) + return self._iterencode(o, 0, markers, refs, ()) + + def _iterencode(self, o, _current_indent_level, markers, refs, path): + if isinstance(o, basestring): + yield self._encoder(o) + elif o is None: + yield 'null' + elif o is True: + yield 'true' + elif o is False: + yield 'false' + elif isinstance(o, (int, long)): + yield str(o) + elif isinstance(o, float): + yield _floatstr(o) + else: + ref = self._iterencode_ref( + o, _current_indent_level, markers, refs, path) + if ref: + yield ref + 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) and "json" not in dir(o): + for chunk in self._iterencode_dict(o, _current_indent_level, markers, refs, path): + yield chunk + else: + if markers is not None: + markerid = id(o) + if markerid in markers: + raise ValueError("Circular reference detected") + markers[markerid] = o + o = self.default(o, refs, path) + for chunk in self._iterencode(o, _current_indent_level, markers, refs, path): + yield chunk + if markers is not None: + del markers[markerid] + + def _iterencode_ref(self, o, _current_indent_level, markers, refs, path): + for key, value in refs.items(): + if value is o: + return "<%s>" % key + + def _iterencode_list(self, lst, _current_indent_level, markers, refs, path): + if path: + pathstr = str(path[0]) + numindices = [] + for index in path[1:]: + if type(index) == int: + numindices.append(str(index)) + else: + if numindices: + pathstr += "[%s]" % (",".join(numindices)) + numindices = [] + pathstr += ".%s" % index + if numindices: + pathstr += "[%s]" % (",".join(numindices)) + numindices = [] + if pathstr not in refs.keys(): + refs[pathstr] = lst + + if not lst: + yield '[]' + return + if markers is not None: + markerid = id(lst) + if markerid in markers: + raise ValueError("Circular reference detected") + markers[markerid] = lst + buf = '[' + if self.indent is not None: + _current_indent_level += 1 + newline_indent = '\n' + \ + (' ' * (self.indent * _current_indent_level)) + separator = self.item_separator + newline_indent + buf += newline_indent + else: + newline_indent = None + separator = self.item_separator + first = True + for (k, value) in enumerate(lst): + if first: + first = False + else: + buf = separator + if isinstance(value, basestring): + yield buf + self._encoder(value) + elif value is None: + yield buf + 'null' + elif value is True: + yield buf + 'true' + elif value is False: + yield buf + 'false' + elif isinstance(value, (int, long)): + yield buf + str(value) + elif isinstance(value, float): + yield buf + _floatstr(value) + else: + ref = self._iterencode_ref( + value, _current_indent_level, markers, refs, path) + if ref and False: + yield buf + ref + else: + yield buf + 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) and "json" not in dir(value): + chunks = self._iterencode_dict( + value, _current_indent_level, markers, refs, path + (k,)) + else: + chunks = self._iterencode( + value, _current_indent_level, markers, refs, path + (k,)) + for chunk in chunks: + yield chunk + if newline_indent is not None: + _current_indent_level -= 1 + yield '\n' + (' ' * (self.indent * _current_indent_level)) + yield ']' + if markers is not None: + del markers[markerid] + + def _iterencode_dict(self, dct, _current_indent_level, markers, refs, path): + if path: + pathstr = str(path[0]) + numindices = [] + for index in path[1:]: + if type(index) == int: + numindices.append(str(index)) + else: + if numindices: + pathstr += "[%s]" % (",".join(numindices)) + numindices = [] + pathstr += ".%s" % index + if numindices: + pathstr += "[%s]" % (",".join(numindices)) + numindices = [] + if pathstr not in refs.keys(): + refs[pathstr] = dct + + if not dct: + yield '{}' + return + if markers is not None: + markerid = id(dct) + if markerid in markers: + raise ValueError("Circular reference detected") + markers[markerid] = dct + yield '{' + if self.indent is not None: + _current_indent_level += 1 + newline_indent = '\n' + \ + (' ' * (self.indent * _current_indent_level)) + item_separator = self.item_separator + newline_indent + yield newline_indent + else: + newline_indent = None + item_separator = self.item_separator + first = True + if self.sort_keys: + items = sorted(dct.items(), key=lambda kv: kv[0]) + else: + items = dct.iteritems() + for key, value in items: + if isinstance(key, basestring): + pass + # JavaScript is weakly typed for these, so it makes sense to + # also allow them. Many encoders seem to do something like this. + elif isinstance(key, float): + key = _floatstr(key) + elif key is True: + key = 'true' + elif key is False: + key = 'false' + elif key is None: + key = 'null' + elif isinstance(key, (int, long)): + key = str(key) + elif self.skipkeys: + continue + else: + raise TypeError("key " + repr(key) + " is not a string") + if first: + first = False + else: + yield item_separator + yield self._encoder(key) + yield self.key_separator + if isinstance(value, basestring): + yield self._encoder(value) + elif value is None: + yield 'null' + elif value is True: + yield 'true' + elif value is False: + yield 'false' + elif isinstance(value, (int, long)): + yield str(value) + elif isinstance(value, float): + yield _floatstr(value) + else: + ref = self._iterencode_ref( + value, _current_indent_level, markers, refs, path) + if ref: + yield ref + else: + 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) and "json" not in dir(value): + chunks = self._iterencode_dict( + value, _current_indent_level, markers, refs, path + (key,)) + else: + chunks = self._iterencode( + value, _current_indent_level, markers, refs, path + (key,)) + for chunk in chunks: + yield chunk + if newline_indent is not None: + _current_indent_level -= 1 + yield '\n' + (' ' * (self.indent * _current_indent_level)) + yield '}' + if markers is not None: + del markers[markerid] diff --git a/quote.py b/quote.py new file mode 100644 index 0000000..ee18660 --- /dev/null +++ b/quote.py @@ -0,0 +1,55 @@ +import os +import random +import re +import codecs + + +class quote(object): + + def __init__(self, quotefile="quotes.txt", encoding="utf8"): + self.quotefile = quotefile + self.encoding = encoding + if os.path.isfile(quotefile): + with codecs.open(quotefile, encoding=encoding) as f: + self.quotes = [line for line in f] + else: + self.quotes = [] + + def onChanMsg(self, IRC, user, channel, targetprefix, msg): + matches = re.findall( + r"^!quote(?:\s+(--add|--rem|--flush))?(?:\s+(.+?)\s*)?$", msg) + if matches: + cmd, msg = matches[0] + if cmd == "" and msg == "": + if self.quotes: + channel.msg(random.choice(self.quotes)) + else: + channel.msg("%s: There are no quotes!" % user.nick) + elif cmd == "--add": + if msg: + if msg not in self.quotes: + self.quotes.append(msg) + channel.msg("%s: Quote added." % user.nick) + else: + channel.msg("%s: Quote already exists." % user.nick) + else: + channel.msg("%s: What am I adding?" % user.nick) + elif cmd == "--rem": + if msg: + if msg in self.quotes: + self.quotes.remove(msg) + channel.msg("%s: Quote removed." % user.nick) + else: + channel.msg("%s: Quote does not exist." % user.nick) + else: + channel.msg("%s: What am I removing?" % user.nick) + elif cmd == "--flush": + with codecs.open(self.quotefile, "w", encoding=self.encoding) as f: + for line in self.quotes: + print >>f, line + else: + channel.msg( + "I am sorry, %s, but I cannot do that." % user.nick) + + def onSendChanMsg(self, IRC, channel, targetprefix, msg, origin): + self.onChanMsg(IRC, IRC.identity, channel, targetprefix, msg) @@ -5,52 +5,48 @@ import time class SED(object): + def __init__(self, expiry=1800): self.__name__ = "SED Bot" self.__version__ = "0.0.2" self.expiry = expiry self.history = [] - self.trigger = r"^!?s([,/#])(.*)$" - self.pattern = r"^((?:[^%(delimiter)s]|\\.)*)%(delimiter)s((?:[^%(delimiter)s]|\\.)*)%(delimiter)s([igv]*)$" + self.pattern = r"^!?s([,/#])((?:.|\\\1)*)\1((?:.|\\\1)*)\1([ig]*)$" def onChanMsg(self, IRC, user, channel, targetprefix, msg): - matches = re.findall(self.trigger, msg) + matches = re.findall(self.pattern, msg) if matches: - delimiter, args = matches[0] - validate = re.findall(self.pattern%vars(), args) - if validate: - find, replace, flags = validate[0] - find = re.sub(r"\\([,/#\\])", r"\1", find) - replace = re.sub(r"\\([,/#\\])", r"\1", replace) - if "v" in flags: - channel.msg("Delimiter: '%s', Search pattern: '%s', Replacement: '%s', Flags: '%s'" % (delimiter, find, replace, flags), origin=self) - match = False - for t, IRC2, user2, channel2, targetprefix2, msg2, isaction in self.history.__reversed__(): - if channel != channel2: - continue - try: - if re.findall(find, msg2, flags=re.I if "i" in flags else 0): - sub = re.sub(find, replace, msg2, flags=re.I if "i" in flags else 0) - match = True - else: - continue - except: - channel.msg("%s: Invalid syntax" % - user.nick, origin=self) - raise - if isaction: - channel.msg("What %s really meant was: *%s %s" % (user2.nick, user2.nick, sub), origin=self) + separator, find, replace, flags = matches[0] + find = re.sub("\\\\([,/#\\\\])", "\\1", find) + replace = re.sub("\\\\(,/#\\\\)", "\\1", replace) + match = False + for t, IRC2, user2, channel2, msg2, isaction in self.history.__reversed__(): + if channel != channel2: + continue + try: + if re.findall(find, msg2): + sub = re.sub( + find, replace, msg2, flags=re.I if "i" in flags else 0) + match = True else: - channel.msg("What %s really meant to say was: %s" % (user2.nick, sub), origin=self) - break - if not match: - channel.msg("%s: I tried. I really tried! But I could not find the pattern: %s" % (user.nick, find), origin=self) - else: - channel.msg("%s: Invalid syntax. Did you forget a trailing delimiter?" % user.nick, origin=self) + continue + except: + channel.msg("%s: Invalid syntax" % user.nick, origin=self) + raise + if isaction: + channel.msg("What %s really meant was: *%s %s" % + (user2.nick, user2.nick, sub), origin=self) + else: + channel.msg("What %s really meant to say was: %s" % + (user2.nick, sub), origin=self) + break + if not match: + channel.msg( + "%s: I tried. I really tried! But I could not find the pattern: %s" % + (user.nick, find), origin=self) else: - self.history.append((time.time( - ), IRC, user, channel, targetprefix, msg, False)) - while len(self.history) and self.history[0][0] < time.time()-1800: + self.history.append((time.time(), IRC, user, channel, msg, False)) + while len(self.history) and self.history[0][0] < time.time() - 1800: del self.history[0] def onSendChanMsg(self, IRC, origin, channel, targetprefix, msg): @@ -58,9 +54,8 @@ class SED(object): self.onChanMsg(IRC, IRC.identity, channel, targetprefix, msg) def onChanAction(self, IRC, user, channel, targetprefix, action): - self.history.append((time.time( - ), IRC, user, channel, targetprefix, action, True)) - while len(self.history) and self.history[0][0] < time.time()-1800: + self.history.append((time.time(), IRC, user, channel, action, True)) + while len(self.history) and self.history[0][0] < time.time() - 1800: del self.history[0] def onSendChanAction(self, IRC, origin, channel, targetprefix, action): diff --git a/startirc.py b/startirc.py deleted file mode 100755 index c3a6cec..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="perseus.insomniairc.net", ipv6=False, ssl=True, log=open("/dev/null", "w")) - -ax = autoexec.Autoexec() -log = logger.Logger(logroot) - -### Be sure to generate your own cert.pem and key.pem files! -BNC = bouncer.Bouncer( - "", 16698, ssl=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()
\ No newline at end of file diff --git a/testaddon.py b/testaddon.py index 5379188..f0e14a7 100644 --- a/testaddon.py +++ b/testaddon.py @@ -1,213 +1,221 @@ class Addon(object): + def onAddonAdd(self, IRC, **params): - ### Stuff you want the Addon to do when added or inserted into the addon list of an IRC instance. + # Stuff you want the Addon to do when added or inserted into the addon + # list of an IRC instance. print "onAddonAdd:", IRC, params def onAddonRem(self, IRC): - ### Stuff you want the Addon to do when removed from the addon list of an IRC instance. + # Stuff you want the Addon to do when removed from the addon list of an + # IRC instance. print "onAddonRem:", IRC def onSessionOpen(self, IRC): - ### Called when the thread handling the IRC instance is started. + # Called when the thread handling the IRC instance is started. print "onSessionOpen:", IRC def onSessionClose(self, IRC): - ### Called when the thread handling the IRC instance is terminated. + # Called when the thread handling the IRC instance is terminated. print "onSessionClose:", IRC def onConnectAttempt(self, IRC): - ### Called when a connection attempt is made. + # Called when a connection attempt is made. print "onConnectAttempt:", IRC def onConnectFail(self, IRC, exc, excmsg, tb): - ### Called when a connection attempt fails. + # Called when a connection fails. print "onConnectFail:", IRC, exc, excmsg, tb def onConnect(self, IRC): - ### Called when a connection is established. + # Called when a connection is established. print "onConnect:", IRC def onRegistered(self, IRC): - ### Called when registration is complete. + # Called when registration is complete. print "onRegistered:", IRC - def onDisconnect(self, IRC): - ### Called when a connection is terminated. + def onDisconnect(self, IRC, expected): + # Called when a connection is terminated. print "onDisconnect:", IRC def onRecv(self, IRC, line, origin=None, cmd=None, target=None, targetprefix=None, params=None, extinfo=None): - ### Called when a line of data is received from the IRC server. If line also matches ":origin cmd target [params] :[extinfo]", - ### then other variables are provided, with origin and target automatically converted to Channel and User instances as needed. + # Called when a line of data is received from the IRC server. If line also matches ":origin cmd target [params] :[extinfo]", + # then other variables are provided, with origin and target + # automatically converted to Channel and User instances as needed. pass def onSend(self, IRC, origin, line, cmd=None, target=None, targetprefix=None, params=None, extinfo=None): - ### Called when a line of data is sent to the IRC server. If line also matches "cmd target [params] :[extinfo]", - ### then other variables are provided, with target automatically converted to Channel and User instances as needed. - ### The variable origin refers to a class instance voluntarily identifying itself as that which requested data be sent. + # Called when a line of data is sent to the IRC server. If line also matches "cmd target [params] :[extinfo]", + # then other variables are provided, with target automatically converted to Channel and User instances as needed. + # The variable origin refers to a class instance voluntarily + # identifying itself as that which requested data be sent. pass def onMOTD(self, IRC, motdgreet, motd, motdend): - ### Called when MOTD is received. + # Called when MOTD is received. print "onMOTD:", motdgreet for line in motd: print line print motdend def onJoin(self, IRC, user, channel): - ### Called when somebody joins a channel, includes bot. + # Called when somebody joins a channel, includes bot. print "onJoin:", user, channel def onMeJoin(self, IRC, channel): - ### Called when the bot enters the channel. + # Called when the bot enters the channel. print "onMeJoin:", channel def onChanMsg(self, IRC, user, channel, targetprefix, msg): - ### Called when someone sends a PRIVMSG to channel. - print "onChanMsg: [%s%s] <%s> %s" % ( - targetprefix, channel.name, user.nick, msg) + # Called when someone sends a PRIVMSG to channel. + print "onChanMsg: [%s%s] <%s> %s" % (targetprefix, channel.name, user.nick, msg) def onChanAction(self, IRC, user, channel, targetprefix, action): - ### Called when someone sends an action (/me) to channel. - print "onChanAction: [%s%s] *%s %s" % ( - targetprefix, channel.name, user.nick, action) + # Called when someone sends an action (/me) to channel. + print "onChanAction: [%s%s] *%s %s" % (targetprefix, channel.name, user.nick, action) def onSendChanMsg(self, IRC, origin, channel, targetprefix, msg): - ### Called when bot sends a PRIVMSG to channel. - ### The variable origin refers to a class instance voluntarily identifying itself as that which requested data be sent. - print "onSendChanMsg: [%s%s] <%s> %s" % ( - targetprefix, channel.name, IRC.identity.nick, msg) + # Called when bot sends a PRIVMSG to channel. + # The variable origin refers to a class instance voluntarily + # identifying itself as that which requested data be sent. + print "onSendChanMsg: [%s%s] <%s> %s" % (targetprefix, channel.name, IRC.identity.nick, msg) def onSendChanAction(self, IRC, origin, channel, targetprefix, action): - ### origin is the source of the channel message - ### Called when bot sends an action (/me) to channel. - ### The variable origin refers to a class instance voluntarily identifying itself as that which requested data be sent. - print "onSendChanAction: [%s%s] *%s %s" % ( - targetprefix, channel.name, IRC.identity.nick, action) + # origin is the source of the channel message + # Called when bot sends an action (/me) to channel. + # The variable origin refers to a class instance voluntarily + # identifying itself as that which requested data be sent. + print "onSendChanAction: [%s%s] *%s %s" % (targetprefix, channel.name, IRC.identity.nick, action) def onPrivMsg(self, IRC, user, msg): - ### Called when someone sends a PRIVMSG to the bot. + # Called when someone sends a PRIVMSG to the bot. print "onPrivMsg: <%s> %s" % (user.nick, msg) def onPrivAction(self, IRC, user, action): - ### Called when someone sends an action (/me) to the bot. + # Called when someone sends an action (/me) to the bot. print "onPrivAction: *%s %s" % (user.nick, action) def onSendPrivMsg(self, IRC, origin, user, msg): - ### Called when bot sends a PRIVMSG to a user. - ### The variable origin refers to a class instance voluntarily identifying itself as that which requested data be sent. + # Called when bot sends a PRIVMSG to a user. + # The variable origin refers to a class instance voluntarily + # identifying itself as that which requested data be sent. print "onSendPrivMsg: <%s> %s" % (IRC.identity.nick, msg) def onSendPrivAction(self, IRC, origin, user, action): - ### Called when bot sends an action (/me) to a user. - ### The variable origin refers to a class instance voluntarily identifying itself as that which requested data be sent. + # Called when bot sends an action (/me) to a user. + # The variable origin refers to a class instance voluntarily + # identifying itself as that which requested data be sent. print "onSendPrivAction: *%s %s" % (IRC.identity.nick, action) def onNickChange(self, IRC, user, newnick): - ### Called when somebody changes nickname. + # Called when somebody changes nickname. print "onNickChange:", user, newnick def onMeNickChange(self, IRC, newnick): - ### Called when the bot changes nickname. + # Called when the bot changes nickname. print "onMeNickChange:", newnick def onPart(self, IRC, user, channel, partmsg): - ### Called when somebody parts the channel, includes bot. + # Called when somebody parts the channel, includes bot. print "onPart:", user, channel, partmsg def onMePart(self, IRC, channel, partmsg): - ### Called when the bot parts the channel. + # Called when the bot parts the channel. print "onMePart:", channel, partmsg def onKick(self, IRC, kicker, channel, kicked, kickmsg): - ### Called when somebody is kicked from the channel, includes bot. + # Called when somebody is kicked from the channel, includes bot. print "onKick:", kicker, channel, kicked, kickmsg def onMeKick(self, IRC, channel, kicked, kickmsg): - ### Called when the bot kicks somebody from the channel. + # Called when the bot kicks somebody from the channel. print "onMeKick:", channel, kicked, kickmsg def onMeKicked(self, IRC, kicker, channel, kickmsg): - ### Called when the bot is kicked from the channel. + # Called when the bot is kicked from the channel. print "onMeKicked:", kicker, channel, kickmsg def onQuit(self, IRC, user, quitmsg): - ### Called when somebody quits IRC. + # Called when somebody quits IRC. print "onQuit:", user, quitmsg def onNames(self, IRC, channel, channame, endmsg, nameslist): - ### Called when a NAMES list is received. + # Called when a NAMES list is received. print "onNames:", channel, channame, endmsg, nameslist def onWhois(self, IRC, **whoisdata): - ### Called when a WHOIS reply is received. + # Called when a WHOIS reply is received. print "onWhois:", whoisdata def onWho(self, IRC, params, wholist, endmsg): - ### Called when a WHO list is received. + # Called when a WHO list is received. print "onWho:", params for item in wholist: print item print endmsg def onList(self, IRC, chanlistbegin, chanlist, endmsg): - ### Called when a channel list is received. + # Called when a channel list is received. print "onList:", chanlistbegin for item in chanlist: print item print endmsg def onTopic(self, IRC, channel, topic): - ### Called when channel topic is received via 332 response. + # Called when channel topic is received via 332 response. print "onTopic:", channel, topic def onTopicSet(self, IRC, user, channel, topic): - ### Called when channel topic is changed. + # Called when channel topic is changed. print "onChannelSet:", user, channel, topic def onChanModeSet(self, IRC, user, channel, modedelta): - ### Called when channel modes are changed. - ### modedelta is a list of tuples of the format ("+x", parameter), ("+x", None) if no parameter is provided. + # Called when channel modes are changed. + # modedelta is a list of tuples of the format ("+x", parameter), ("+x", + # None) if no parameter is provided. print "onChanModeSet:", user, channel, modedelta def onChannelModes(self, IRC, channel, modedelta): - ### Called when channel modes are received via 324 response. + # Called when channel modes are received via 324 response. print "onChannelModes:", channel, modedelta def onBan(self, IRC, user, channel, banmask): - ### Called when a ban is set. + # Called when a ban is set. print "onBan:", user, channel, banmask def onMeBan(self, IRC, user, channel, banmask): - ### Called when a ban matching bot is set. + # Called when a ban matching bot is set. print "Help! I'm being banned!", user, channel, banmask def onUnban(self, IRC, user, channel, banmask): - ### Called when a ban is removed. + # Called when a ban is removed. print "onUnban:", user, channel, banmask def onMeUnban(self, IRC, user, channel, banmask): - ### Called when a ban on the bot is removed. + # Called when a ban on the bot is removed. print "Squeee!!! I've been unbanned!", user, channel, banmask def onOp(self, IRC, user, channel, modeuser): - ### Called when user gives ops to modeuser is set. + # Called when user gives ops to modeuser is set. print "onOp:", user, channel, modeuser def onMeOp(self, IRC, user, channel): - ### Called when user ops bot. + # Called when user ops bot. print "onMeOp", user, channel def onDeop(self, IRC, user, channel, modeuser): - ### Called when user deops modeuser. + # Called when user deops modeuser. print "onDeop:", user, channel, modeuser def onMeDeop(self, IRC, user, channel): - ### Called when user deops bot. + # Called when user deops bot. print "onMeDeop", user, channel - ### There are a few other event handlers supported by the irc.Connection class. Furthermore, one can program - ### numeric-based handlers to handle numeric events that are not captured by a named event handler as follows: + # There are a few other event handlers supported by the irc.Connection class. Furthermore, one can program + # numeric-based handlers to handle numeric events that are not captured by + # a named event handler as follows: def onNNN(self, IRC, params, extinfo): - ### Where NNN is a three-digit (leading zeros if necessary) code used to handle a numeric NNN event. + # Where NNN is a three-digit (leading zeros if necessary) code used to + # handle a numeric NNN event. pass @@ -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 |