From 25ed9b71564b73b07d664e2396c641ffe74f45b1 Mon Sep 17 00:00:00 2001 From: Brian Sherson Date: Tue, 27 Aug 2013 20:39:32 -0700 Subject: --- autoexec.py | 69 +++- bouncer.py | 453 +++++++++++++++++++++++++- irc.py | 1021 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++- logger.py | 311 +++++++++++++++++- sedbot.py | 14 +- wallet.py | 38 ++- 6 files changed, 1894 insertions(+), 12 deletions(-) mode change 120000 => 100644 autoexec.py mode change 120000 => 100644 bouncer.py mode change 120000 => 100644 irc.py mode change 120000 => 100644 logger.py mode change 120000 => 100644 wallet.py diff --git a/autoexec.py b/autoexec.py deleted file mode 120000 index 8ed73e0..0000000 --- a/autoexec.py +++ /dev/null @@ -1 +0,0 @@ -../autoexec.py \ No newline at end of file diff --git a/autoexec.py b/autoexec.py new file mode 100644 index 0000000..8a8b7e7 --- /dev/null +++ b/autoexec.py @@ -0,0 +1,68 @@ +#!/usr/bin/python +import re + +class Autoexec(object): + def __init__(self): + self.networks={} + def onModuleAdd(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()] + 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 onModuleRem(self, IRC): + del self.networks[IRC] + 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 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 onRecv(self, IRC, line, data): + if data==None: + return + (label, onconnect, onregister, autojoin, usermodes, wallet, opername, opermodes, snomasks, operexec, operjoin)=self.networks[IRC] + (origin, ident, host, cmd, target, params, extinfo)=data + if cmd=="381" and opermodes: + 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) + +class NickServ(object): + def __init__(self): + self.networks={} + def onModuleAdd(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 onModuleRem(self, IRC): + del self.networks[IRC] + def onRecv(self, IRC, line, data): + if data==None: return + (origin, ident, host, cmd, target, params, extinfo)=data + label, wallet, autojoin=self.networks[IRC] + if target==IRC.identity.nick and origin=="NickServ" and re.match("This nickname is registered and protected.", extinfo) and wallet and "%s/NickServ/%s"%(label, target.lower()) in wallet.keys(): + IRC.user("NickServ").msg("identify %s" % wallet["%s/NickServ/%s"%(label, target.lower())]) + if cmd=="900" and autojoin: + IRC.raw("JOIN %s"%(",".join(autojoin)), origin=self) diff --git a/bouncer.py b/bouncer.py deleted file mode 120000 index d8ddf7e..0000000 --- a/bouncer.py +++ /dev/null @@ -1 +0,0 @@ -../bouncer4.py \ No newline at end of file diff --git a/bouncer.py b/bouncer.py new file mode 100644 index 0000000..2e6561a --- /dev/null +++ b/bouncer.py @@ -0,0 +1,452 @@ +#!/usr/bin/python +import socket, ssl, os, re, time, sys, string, hashlib, traceback +from threading import Thread, Lock +import Queue + +class Bouncer (Thread): + def __init__(self, addr="", port=16667, ssl=False, certfile=None, keyfile=None, ignore=None): + self.__name__="Bouncer for pyIRC" + self.__version__="1.0.0rc1" + self.__author__="Brian Sherson" + self.__date__="May 23, 2013" + #print "Initializing ListenThread..." + self.addr=addr + self.port=port + self.servers={} + self.passwd={} + self.socket=s=socket.socket() + self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + self.ssl=ssl + self.certfile=certfile + self.keyfile=keyfile + s.bind((self.addr,self.port)) + self.connections=[] + self.ignore=ignore + + ### 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.lock=Lock() + self.starttime=int(time.time()) + Thread.__init__ ( self ) + self.daemon=True + self.start() + def __repr__(self): + return "" % vars(self) + + def run(self): + 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) + try: + hostname, aliaslist, addresslist = socket.gethostbyaddr(addr[0]) + addr = (hostname, addr[1]) + except: + pass + #print ((self,"New client connecting from %s:%s"%addr)) + except socket.error: + #print "Shutting down Listener" + self.socket.close() + #raise + sys.exit() + bouncer=BouncerConnection(self, connection, addr) + #bouncer.daemon=True + #self.connections.append(bouncer) + #bouncer.start() + #ccrecv.start() + time.sleep(0.5) + try: + self.socket.close() + except: pass + def onRecv(self, IRC, line, data): + if type(self.ignore) not in (list, tuple) or all([not re.match(pattern, line) for pattern in self.ignore]): + if data: + (origin, ident, host, cmd, target, params, extinfo)=data + #print data + if re.match("^\\d+$",cmd): cmd=int(cmd) ### Code is a numerical response + if cmd in (352, 315): ### WHO reply + if len(self.whoexpected[IRC]) and self.whoexpected[IRC][0] in self.connections: + self.whoexpected[IRC][0].connection.send(line+"\n") + if cmd==315: ### End of WHO reply + del self.whoexpected[IRC][0] + elif cmd in (307, 311, 312, 313, 317, 318, 319, 330, 335, 336, 378, 379): ### WHO reply + if len(self.whoisexpected[IRC]) and self.whoisexpected[IRC][0] in self.connections: + self.whoisexpected[IRC][0].connection.send(line+"\n") + if cmd==318: ### End of WHOIS reply + del self.whoisexpected[IRC][0] + elif cmd in (321, 322, 323): ### LIST reply + if len(self.listexpected[IRC]) and self.listexpected[IRC][0] in self.connections: + self.listexpected[IRC][0].connection.send(line+"\n") + if cmd==323: ### End of LIST reply + del self.listexpected[IRC][0] + else: + for bouncer in self.connections: + #print bouncer.IRC + #print IRC + #print line + if bouncer.IRC==IRC: bouncer.connection.send(line+"\n") + def onSend(self, IRC, line, data, origin): + if type(self.ignore) not in (list, tuple) or all([not re.match(pattern, line) for pattern in self.ignore]): + (cmd, target, params, extinfo)=data + if cmd.upper() in ("PRIVMSG", "NOTICE"): + for bouncerconnection in self.connections: + if bouncerconnection==origin: ### Do NOT send the message back to the originating client. + continue + if bouncerconnection.IRC==IRC: ### Send the message to the other clients connected to the bouncer. + ctcp=re.findall("^\x01(.*?)(?:\\s+(.*?)\\s*)?\x01$",extinfo) + if ctcp: + (ctcptype,ext)=ctcp[0] + if ctcptype=="ACTION": + bouncerconnection.connection.send(":%s!%s@%s %s\n" % (bouncerconnection.IRC.identity.nick, bouncerconnection.IRC.identity.idnt, bouncerconnection.IRC.identity.host, line)) + ### Unless the message is a CTCP that is not ACTION. + else: + bouncerconnection.connection.send(":%s!%s@%s %s\n" % (bouncerconnection.IRC.identity.nick, bouncerconnection.IRC.identity.idnt, bouncerconnection.IRC.identity.host, line)) + elif cmd.upper()=="WHO": + #print origin, line + self.whoexpected[IRC].append(origin) + elif cmd.upper()=="WHOIS": + #print origin, line + self.whoisexpected[IRC].append(origin) + elif cmd.upper()=="LIST": + #print origin, line + self.listexpected[IRC].append(origin) + def onModuleAdd(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]=[] + self.whoisexpected[IRC]=[] + self.listexpected[IRC]=[] + + def onModuleRem(self, IRC): + for bouncerconnection in self.connections: + if bouncerconnection.IRC==IRC: + bouncerconnection.stop(quitmsg="Bouncer extension removed") + for (label, (connection, passwd, hashtype)) in self.servers.items(): + if connection==IRC: + del self.servers[label] + + def stop(self): + #self.quitmsg=quitmsg + #self.connection.send("ERROR :Closing link: (%s@%s) [%s]\n" % (self.IRC.identity.nick, self.addr[0], self.quitmsg)) + self.socket.shutdown(0) + def disconnectall(self, quitmsg="Disconnecting all sessions"): + for bouncerconnection in self.connections: + bouncerconnection.stop(quitmsg=quitmsg) + def onDisconnect(self, IRC): + self.whoexpected[IRC]=[] + self.whoisexpected[IRC]=[] + self.listexpected[IRC]=[] + for bouncerconnection in self.connections: + if bouncerconnection.IRC==IRC: + bouncerconnection.stop(quitmsg="IRC connection lost") + +class BouncerConnection (Thread): + def __init__(self, bouncer, connection, addr): + #print "Initializing ListenThread..." + self.bouncer=bouncer + self.connection=connection + self.host, self.port=self.addr=addr + self.IRC=None + self.pwd=None + self.nick=None + self.label=None + self.idnt=None + self.realname=None + self.addr=addr + self.quitmsg="Connection Closed" + + Thread.__init__ ( self ) + self.daemon=True + self.start() + + 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.idnt if self.IRC.identity.idnt else "*" + host=self.IRC.identity.host if self.IRC.identity.host else "*" + else: + nick="*" + ident="*" + host="*" + protocol="ircs" if self.IRC.ssl else "irc" + addr=self.host + return "" % locals() + + def stop(self, quitmsg="Disconnected"): + self.quitmsg=quitmsg + #self.connection.send("ERROR :Closing link: (%s@%s) [%s]\n" % (self.IRC.identity.nick, self.host, self.quitmsg)) + self.connection.shutdown(0) + + def run(self): + ### 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")) + + passwd=None + nick=None + user=None + + readbuf="" + linebuf=[] + + try: + while True: + ### Read data (appending) into readbuf, then break lines and append lines to linebuf + while len(linebuf)==0: + timestamp=reduce(lambda x,y: x+":"+y,[str(t).rjust(2,"0") for t in time.localtime()[0:6]]) + read=self.connection.recv(512) + if read=="" and len(linebuf)==0: ### No more data to process. + #self.quitmsg="Connection Closed" + sys.exit() + + readbuf+=read + lastlf=readbuf.rfind("\n") + + if lastlf>=0: + linebuf.extend(string.split(readbuf[0:lastlf], "\n")) + readbuf=readbuf[lastlf+1:] + + line=string.rstrip(linebuf.pop(0)) + match=re.findall("^(.+?)(?:\\s+(.+?)(?:\\s+(.+?))??)??(?:\\s+:(.*))?$", line, re.I) + + if len(match)==0: continue + (cmd, target, params, extinfo)=match[0] + + if not passwd: ### Bouncer expects a password + if cmd.upper()=="PASS": + passwd=target if target else extinfo + else: + self.quitmsg="Access Denied" + break + + elif not nick: ### Bouncer expects a NICK command + if cmd.upper()=="NICK": + nick=target if target else extinfo + else: + self.quitmsg="Access Denied" + break + + elif not self.idnt: ### Bouncer expects a USER command to finish registration + if cmd.upper()=="USER": + self.idnt=target + #print self.idnt + if self.idnt in self.bouncer.servers.keys(): + self.IRC, passwdhash, hashtype=self.bouncer.servers[self.idnt] + passmatch=hashlib.new(hashtype, passwd).hexdigest()==passwdhash + self.IRC.lock.acquire() + 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.quitmsg="Access Denied" + self.IRC.lock.release() + break + + ### If we have made it to this point, then access has been granted. + self.bouncer.connections.append(self) + labels=[bouncerconnection.label for bouncerconnection in self.bouncer.connections if bouncerconnection.IRC==self.IRC and bouncerconnection.label] + n=1 + while "*%s_%d"%(self.idnt, n) in labels: + n+=1 + self.label="*%s_%d"%(self.idnt, n) + + ### Request Version info. + self.connection.send(":$bouncer PRIVMSG %s :\x01VERSION\x01\n" % (self.IRC.identity.nick)) + + ### Send Greeting. + 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.servinfo)) + self.connection.send(":%s 004 %s %s\n" % (self.IRC.serv, self.IRC.identity.nick, self.IRC.serv004)) + + ### 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 + 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)) + self.connection.send(":%s 376 %s :%s\n" % (self.IRC.serv, self.IRC.identity.nick, self.IRC.motdend)) + + ### 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.idnt, 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.idnt, 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)) + + ### Announce connection to all other bouncer connections. + for bouncerconnection in self.bouncer.connections: + try: + bouncerconnection.connection.send(":%s!%s@%s JOIN :$bouncer\n" % (self.label, self.idnt, self.addr[0])) + bouncerconnection.connection.send(":*Bouncer* MODE $bouncer +o %s\n" % (self.label)) + except: + pass + self.IRC.lock.release() + else: ### User not found + self.quitmsg="Access Denied" + break + else: ### Client did not send USER command when expected + self.quitmsg="Access Denied" + break + + elif cmd.upper()=="QUIT": + self.quitmsg=extinfo + break + + elif cmd.upper()=="PING": + self.connection.send(":%s PONG %s :%s\n" % (self.IRC.serv, self.IRC.serv, self.IRC.identity.nick)) + + elif cmd.upper()=="WHO" and target.lower()=="$bouncer": + for bouncerconnection in self.bouncer.connections: + self.connection.send(":$bouncer 352 %s $bouncer %s %s $bouncer %s H@ :0 %s\n" % (self.IRC.identity.nick, bouncerconnection.idnt, bouncerconnection.host, bouncerconnection.label, bouncerconnection.IRC)) + self.connection.send(":$bouncer 315 %s $bouncer :End if /WHO list.\n" % (self.IRC.identity.nick)) + + elif cmd.upper() in ("PRIVMSG", "NOTICE"): + ### Check if CTCP + ctcp=re.findall("^\x01(.*?)(?:\\s+(.*?)\\s*)?\x01$",extinfo) + + + if target.lower()=="$bouncer": ### Message to internal bouncer control channel + if ctcp and cmd.upper()=="NOTICE": + (ctcptype, ext)=ctcp[0] ### Unpack CTCP info + if ctcptype=="VERSION": ### Client is sending back version reply + for bouncerconnection in self.bouncer.connections: + reply=":%s!%s@%s PRIVMSG $bouncer :Version reply: %s\n" % (self.label, self.idnt, self.addr[0], ext) + try: + bouncerconnection.connection.send(reply) + except: + pass + elif 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.connection.send(":%s!%s@%s %s\n" % (self.IRC.identity.nick, self.IRC.identity.idnt, 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]]) + self.connection.send(":%s 324 %s %s +%s %s\n" % (self.IRC.serv, self.IRC.identity.nick, channel.name, modestr, params)) + 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] + 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(): + 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) + + + + + + + + + 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) + finally: + if self.IRC and self.IRC.lock.locked(): self.IRC.lock.release() ### Release lock in case lock is locked. + try: + self.connection.send("ERROR :Closing link: (%s@%s) [%s]\n" % (self.IRC.identity.nick if self.IRC else "*", self.host, self.quitmsg)) + self.connection.shutdown(1) + self.connection.close() + except: + pass + + if self in self.bouncer.connections: + self.bouncer.connections.remove(self) + + ### 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.idnt, self.host, self.quitmsg)) + except: + pass diff --git a/irc.py b/irc.py deleted file mode 120000 index f2bc457..0000000 --- a/irc.py +++ /dev/null @@ -1 +0,0 @@ -../irc5.py \ No newline at end of file diff --git a/irc.py b/irc.py new file mode 100644 index 0000000..16df092 --- /dev/null +++ b/irc.py @@ -0,0 +1,1020 @@ +#!/usr/bin/python +from threading import Thread, Event, Lock +import re, time, sys, string, socket, os, platform, traceback, Queue, ssl + +class Connection(Thread): + def __init__(self, nick="ircbot", ident="python", realname="Python IRC Library", passwd=None, server="", 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.0.0rc2" + self.__author__="Brian Sherson" + self.__date__="August 26, 2013" + + if port==None: + self.port=6667 if not ssl else 6697 + else: + self.port=port + + if type(nick) in (str, unicode): self.nick=[nick] + elif type(nick) in (list, tuple): self.nick=nick + + self.realname=realname + self.idnt=ident + self.passwd=passwd + self.server=server + self.ssl=ssl + self.ipv6=ipv6 + self.identity=None + + self.connected=False + self.registered=False + self.connection=None + + self.autoreconnect=autoreconnect + self.maxretries=maxretries + self.timeout=timeout + self.retrysleep=retrysleep + + self.quitexpected=False + self.log=log + + self.modules=[] + + + ### Initialize IRC environment variables + self.motdgreet="" + self.motd=[] + self.identity=None + self.users=[] + self.channels=[] + self.supports={} + + self.lock=Lock() + self.loglock=Lock() + self.sendlock=Lock() + self.outgoing=Queue.Queue() + + Thread.__init__(self) + + def event(self, method, modlist, exceptions=False, **params): + #print method, modlist + timestamp=reduce(lambda x,y: x+":"+y,[str(t).rjust(2,"0") for t in time.localtime()[0:6]]) + for module in modlist: + #print module, dir(module) + #if modlist is not self.modules and module in self.modules: continue + if method in dir(module) and callable(getattr(module, method)): + try: + getattr(module, method)(self, **params) + except: + with self.loglock: + exc,excmsg,tb=sys.exc_info() + print >>self.log, "%(timestamp)s !!! Exception in module %(module)s" % vars() + for tbline in traceback.format_exc().split("\n"): + print >>self.log, "%(timestamp)s !!! %(tbline)s" % vars() + self.log.flush() + if exceptions: ### If set to true, we raise the exception. + raise + + def addModule(self, module, trusted=False, **params): + if module in self.modules: + raise BaseException, "Module already added." + with self.lock: + self.event("onModuleAdd", [module], exceptions=True, **params) + self.modules.append(module) + if trusted: + self.trusted.append(module) + + def insertModule(self, index, module, trusted=False, **params): + if module in self.modules: + raise BaseException, "Module already added." + with self.lock: + self.event("onModuleAdd", [module], exceptions=True, **params) + self.modules.insert(index, module) + if trusted: + self.trusted.append(module) + + def rmModule(self, module, **params): + with self.lock: + self.modules.remove(module) + self.event("onModuleRem", [module], exceptions=True, **params) + if module in self.trusted: + self.trusted.remove(module) + + def run(self): + self.quitexpected=False + server=self.server + if self.ipv6 and ":" in server: + server="[%s]"%server + port=self.port + try: + modules=list(self.modules) + for channel in self.channels: + for module in channel.modules: + if module not in modules: modules.append(module) + with self.lock: + self.event("onLogOpen", modules) + + with self.loglock: + timestamp=reduce(lambda x,y: x+":"+y,[str(t).rjust(2,"0") for t in time.localtime()[0:6]]) + print >>self.log, "%(timestamp)s ### Log session started" % vars() + self.log.flush() + + 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 + with self.loglock: + timestamp=reduce(lambda x,y: x+":"+y,[str(t).rjust(2,"0") for t in time.localtime()[0:6]]) + print >>self.log, "%(timestamp)s *** Attempting connection to %(server)s:%(port)s." % vars() + self.log.flush() + + with self.lock: + self.event("onConnectAttempt", self.modules) + + 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)) + self.connected=True + self.connection.settimeout(self.timeout) + + ### Setting up thread responsible for sending data back to IRC server. + outgoingthread=Outgoing(self) + outgoingthread.daemon=True + outgoingthread.start() + + ### Run onConnect on all modules to signal connection was established. + modules=list(self.modules) + for channel in self.channels: + for module in channel.modules: + if module not in modules: modules.append(module) + self.event("onConnect", modules) + with self.loglock: + timestamp=reduce(lambda x,y: x+":"+y,[str(t).rjust(2,"0") for t in time.localtime()[0:6]]) + print >>self.log, "%(timestamp)s *** Connection to %(server)s:%(port)s established." % vars() + self.log.flush() + break + except socket.error: + with self.loglock: + exc,excmsg,tb=sys.exc_info() + timestamp=reduce(lambda x,y: x+":"+y,[str(t).rjust(2,"0") for t in time.localtime()[0:6]]) + print >>self.log, "%(timestamp)s *** Connection to %(server)s:%(port)s failed: %(excmsg)s." % vars() + self.log.flush() + + if self.quitexpected: sys.exit() + if attempt>self.log, "%(timestamp)s *** Maximum number of attempts reached. Giving up. (%(server)s:%(port)s)" % vars() + self.log.flush() + break + + ### Connection succeeded + try: + ### Attempt initial registration. + nick=self.nick[0] + trynick=0 + if self.passwd: self.raw("PASS :%(passwd)s" % vars(self)) + self.raw("NICK :%(nick)s" % vars()) + self.raw("USER %(idnt)s * * :%(realname)s" % vars(self)) + + ### 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. + if read=="": sys.exit() + + ### 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:] + + 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 + + with self.loglock: + timestamp=reduce(lambda x,y: x+":"+y,[str(t).rjust(2,"0") for t in time.localtime()[0:6]]) + print >> self.log, "%(timestamp)s <<< %(line)s" % vars() + self.log.flush() + + ### Attempts to match against pattern ":src cmd target params :extinfo" + matches=re.findall("^:(.+?)(?:!(.+?)@(.+?))?\\s+(.+?)(?:\\s+(.+?)(?:\\s+(.+?))??)??(?:\\s+:(.*))?$",line) + + ### We have a match! + if len(matches): + parsed=(origin, ident, host, cmd, target, params, extinfo)=matches[0] + 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 + + modules=list(self.modules) + for channel in self.channels: + for module in channel.modules: + if module not in modules: modules.append(module) + self.event("onRegistered", modules) + + 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 :%(nick)s" % vars()) + if not self.registered: ### Registration is not yet complete + continue + + ### Major codeblock here! Track IRC state. + ### Send line to modules first + with self.lock: + self.event("onRecv", self.modules, line=line, data=parsed) + if cmd==1: self.welcome=extinfo # Welcome message + elif cmd==2: self.hostinfo=extinfo # Your Host + elif cmd==3: self.servinfo=extinfo # Server Created + elif cmd==4: self.serv004=params # What is this code? + elif cmd==5: # Server Supports + 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("\\((.*)\\)(.*)",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 + 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: # Channel Modes + self.identity.snomask=params.lstrip("+") + if "s" not in self.identity.modes: self.snomask="" + elif cmd==221: # Channel Modes + self.identity.modes=(params if params else extinfo).lstrip("+") + if "s" not in self.identity.modes: self.snomask="" + elif cmd==251: self.netstats=extinfo + elif cmd==252: self.opcount=int(params) + elif cmd==254: self.chancount=int(params) + elif cmd==311: # WHOIS data + pass + elif cmd==321: # Start LIST + self.chanlistbegin=(params,extinfo) + self.chanlist={} + elif cmd==322: # LIST item + (chan,pop)=params.split(" ",1) + self.chanlist[chan]=(pop,extinfo) + elif cmd==323: self.chanlistend=extinfo # End of LIST + elif cmd==324: # Channel Modes + modeparams=params.split() + channame=modeparams.pop(0) + channel=self.channel(channame) + self.event("onRecv", channel.modules, line=line, data=parsed) + 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) + for mode in setmodes: + if mode=="+": continue + elif mode in self.supports["CHANMODES"][2]: + param=modeparams.pop(0) + channel.modes[mode]=param + elif mode in self.supports["CHANMODES"][3]: channel.modes[mode]=True + elif cmd==329: # Channel created + channame, created=params.split() + channel=self.channel(channame) + self.event("onRecv", channel.modules, line=line, data=parsed) + if channel.name!=channame: channel.name=channame ### Server seems to have changed the idea of the case of the channel name + channel.created=int(created) + elif cmd==332: # Channel Topic + channame=params + channel=self.channel(channame) + self.event("onRecv", channel.modules, line=line, data=parsed) + if channel.name!=channame: channel.name=channame ### Server seems to have changed the idea of the case of the channel name + channel.topic=extinfo + elif cmd==333: # Channel Topic info + (channame,nick,dt)=params.split() + channel=self.channel(channame) + self.event("onRecv", channel.modules, line=line, data=parsed) + if channel.name!=channame: channel.name=channame ### Server seems to have changed the idea of the case of the channel name + channel.topicsetby=nick + channel.topictime=dt + elif cmd==346: # Invite + (channame,invite,nick,invtime)=params.split() + channel=self.channel(channame) + self.event("onRecv", channel.modules, line=line, data=parsed) + if channel.name!=channame: channel.name=channame ### Server seems to have changed the idea of the case of the channel name + if channel.modes.has_key("I"): + if invite.lower() not in [m.lower() for (m, b, t) in channel.modes["I"]]: channel.modes["I"].append((invite,nick,int(invtime))) + else: channel.modes["I"]=[(invite,nick,int(invtime))] + elif cmd==348: # Ban Exception + (channame,exception,nick,exctime)=params.split() + channel=self.channel(channame) + self.event("onRecv", channel.modules, line=line, data=parsed) + if channel.name!=channame: channel.name=channame ### Server seems to have changed the idea of the case of the channel name + if channel.modes.has_key("e"): + if exception.lower() not in [m.lower() for (m, b, t) in channel.modes["e"]]: channel.modes["e"].append((exception, nick, int(exctime))) + else: channel.modes["e"]=[(exception, nick, int(exctime))] + elif cmd==352: # WHO reply + (channame,ident,host,serv,nick,flags)=params.split() + (hops,realname)=extinfo.split(" ",1) + channel=self.channel(channame) + self.event("onRecv", channel.modules, line=line, data=parsed) + if channel.name!=channame: channel.name=channame ### Server seems to have changed the idea of the case of the channel name + user=self.user(nick) + if user.nick!=nick: user.nick=nick + user.hops=hops + user.realname=realname + user.idnt=ident + user.host=host + user.server=serv + user.away="G" in flags + user.ircop="*" in flags + 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==353: # NAMES reply + (devnull,channame)=params.split() + channel=self.channel(channame) + self.event("onRecv", channel.modules, line=line, data=parsed) + if channel.name!=channame: channel.name=channame ### Server seems to have changed the idea of the case of the channel name + if self.supports.has_key("PREFIX"): + names=re.findall("(["+re.escape(self.supports["PREFIX"][1])+"]*)(\\S+)",extinfo) + else: names=[("",name) for name in extinfo.split()] # Still put it into tuple form for compatibility in the next structure + for (symbs,nick) in names: + user=self.user(nick) + if user.nick!=nick: user.nick=nick + if channel not in user.channels: user.channels.append(channel) + if user not in channel.users: channel.users.append(user) + if self.supports.has_key("PREFIX"): + for symb in symbs: + mode=self.supports["PREFIX"][0][self.supports["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) + elif cmd==367: # Channel Ban + (channame,ban,nick,bantime)=params.split() + channel=self.channel(channame) + self.event("onRecv", channel.modules, line=line, data=parsed) + if channel.name!=channame: channel.name=channame ### Server seems to have changed the idea of the case of the channel name + if "b" in channel.modes.keys(): + if ban.lower() not in [m.lower() for (m, b, t) in channel.modes["b"]]: + channel.modes["b"].append((ban,nick,int(bantime))) + else: channel.modes["b"]=[(ban,nick,int(bantime))] + elif cmd==372: self.motd.append(extinfo) # MOTD item + elif cmd==375: # Begin MOTD + self.motdgreet=extinfo + self.motd=[] + elif cmd==376: self.motdend=extinfo # End of MOTD + elif cmd==386 and "q" in self.supports["PREFIX"][0]: # Channel Admin (Unreal) + (channame,admin)=params.split() + channel=self.channel(channame) + self.event("onRecv", channel.modules, line=line, data=parsed) + 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.modules, line=line, data=parsed) + 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] + elif cmd=="NICK": + user=self.user(origin) + modules=[] + for channel in user.channels: + for module in channel.modules: + if module not in modules: modules.append(module) + self.event(modules, line, parsed) + newnick=extinfo if len(extinfo) else target + #print user, newnick + ### Need to check if newnick is still listed + for u in self.users: + #print u + #print [u.nick, newnick, u.nick.lower(), newnick.lower()] + if u.nick.lower()==newnick.lower(): + with self.loglock: + print >>self.log, "%s *** Orphaned user %s!%s@%s was removed when %s!%s@%s changed his/her nick to %s."%(timestamp, u.nick, u.idnt, u.host, user.nick, user.idnt, user.host, newnick) + self.log.flush() + self.users.remove(u) + for channel in self.channels: + if u in channel.users: + channel.users.remove(u) + user.nick=newnick + elif cmd=="JOIN": + channame=target if len(target) else extinfo + user=self.user(origin) + if user.nick!=origin: user.nick=origin + if user.idnt!=ident: user.idnt=ident + if user.host!=host: user.host=host + + channel=self.channel(channame) + self.event("onRecv", channel.modules, line=line, data=parsed) + if channel.name!=channame: channel.name=channame ### Server seems to have changed the idea of the case of the channel name + + if user==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.raw("MODE %(channame)s" % vars()) + self.raw("WHO %(channame)s" % vars()) + self.raw("MODE %s :%s" % (channame, self.supports["CHANMODES"][0])) + if channel not in user.channels: user.channels.append(channel) + if user not in channel.users: channel.users.append(user) + elif cmd=="KICK": + kicker=self.user(origin) + if kicker.nick!=origin: kicker.nick=origin + if kicker.idnt!=ident: kicker.idnt=ident + if kicker.host!=host: kicker.host=host + + kicked=self.user(params) + if kicked.nick!=params: kicked.nick=params + + channel=self.channel(target) + self.event("onRecv", channel.modules, line=line, data=parsed) + if channel.name!=target: channel.name=target ### Server seems to have changed the idea of the case of the channel name + if channel in kicked.channels: kicked.channels.remove(channel) + if kicked in channel.users: channel.users.remove(kicked) + if self.supports.has_key("PREFIX"): + for mode in self.supports["PREFIX"][0]: + if channel.modes.has_key(mode) and kicked in channel.modes[mode]: channel.modes[mode].remove(kicked) + #if not len(chanobj.users): #Let's remove this channel + # del self.channels[target.lower()] + if all([kicked not in c.users for c in self.identity.channels]): + with self.loglock: + print >>self.log, "%s *** User %s!%s@%s was orphaned when being kicked from %s."%(timestamp, kicked.nick, kicked.idnt, kicked.host, channel.name) + self.log.flush() + elif cmd=="PART": + user=self.user(origin) + if user.nick!=origin: user.nick=origin + if user.idnt!=ident: user.idnt=ident + if user.host!=host: user.host=host + + channel=self.channel(target) + self.event("onRecv", channel.modules, line=line, data=parsed) + if channel.name!=target: channel.name=target ### Server seems to have changed the idea of the case of the channel name + + if channel in user.channels: user.channels.remove(channel) + if user in channel.users: channel.users.remove(user) + if self.supports.has_key("PREFIX"): + for mode in self.supports["PREFIX"][0]: + if channel.modes.has_key(mode) and user in channel.modes[mode]: channel.modes[mode].remove(user) + if all([user not in c.users for c in self.identity.channels]): + with self.loglock: + print >>self.log, "%s *** User %s!%s@%s was orphaned when parting %s."%(timestamp, user.nick, user.idnt, user.host, channel.name) + self.log.flush() + elif cmd=="QUIT": + user=self.user(origin) + if user.nick!=origin: user.nick=origin + if user.idnt!=ident: user.idnt=ident + if user.host!=host: user.host=host + channels=list(user.channels) + for channel in user.channels: + for module in channel.modules: + if module not in modules: modules.append(module) + self.event(modules, line, parsed) + for channel in channels: + channel.lock.acquire(True) + for channel in user.channels: + if user in channel.users: channel.users.remove(user) + if self.supports.has_key("PREFIX"): + for mode in self.supports["PREFIX"][0]: + if channel.modes.has_key(mode) and user in channel.modes[mode]: channel.modes[mode].remove(user) + #if not len(chanobj.users): #Let's remove this channel + # del self.channels[chan] + # On second thought, no, since we may want to save certain info + user.channels=[] + for channel in channels: + channel.lock.release() + print >>self.log, "%s *** User %s!%s@%s was orphaned when quitting IRC."%(timestamp, user.nick, user.idnt, user.host) + elif cmd=="MODE": + if target[0] in self.supports["CHANTYPES"]: + user=self.user(origin) + if user.nick!=origin: user.nick=origin + if user.idnt!=ident: user.idnt=ident + if user.host!=host: user.host=host + + channel=self.channel(target) + self.event("onRecv", channel.modules, line=line, data=parsed) + with channel.lock: + if channel.name!=target: channel.name=target ### Server seems to have changed the idea of the case of the channel name + + 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]: + param=modeparams.pop(0) + if modeset=="+": + if channel.modes.has_key(mode): + if param.lower() not in [mask.lower() for (mask, setby, settime) in channel.modes[mode]]: channel.modes[mode].append((param, origin, int(time.time()))) + else: channel.modes[mode]=[(param, origin, int(time.time()))] + else: + if mode in channel.modes.keys(): + 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()) + #print "Index: %d"%index + 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 self.supports["CHANMODES"][1]: + param=modeparams.pop(0) + if modeset=="+": channel.modes[mode]=param + else: channel.modes[mode]=None + elif mode in self.supports["CHANMODES"][2]: + if modeset=="+": + param=modeparams.pop(0) + channel.modes[mode]=param + else: channel.modes[mode]=None + elif mode in self.supports["CHANMODES"][3]: + if modeset=="+": channel.modes[mode]=True + else: channel.modes[mode]=False + elif self.supports.has_key("PREFIX") and mode in self.supports["PREFIX"][0]: + modenick=modeparams.pop(0) + modeuser=self.user(modenick) + if modeuser.nick!=modenick: modeuser.nick=modenick + if modeset=="+": + if channel.modes.has_key(mode) and modeuser not in channel.modes[mode]: channel.modes[mode].append(modeuser) + if not channel.modes.has_key(mode): channel.modes[mode]=[modeuser] + elif channel.modes.has_key(mode) and modeuser in channel.modes[mode]: channel.modes[mode].remove(modeuser) + else: + user=self.user(target) + 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 user.modes: user.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 user.snomask: user.snomask+=snomode + if snomodeset=="-" and snomode in user.snomask: user.snomask=user.snomask.replace(snomode,"") + if modeset=="-": + if mode in user.modes: user.modes=user.modes.replace(mode,"") + if mode=="s": user.snomask="" + elif cmd=="TOPIC": + user=self.user(origin) + if user.nick!=origin: user.nick=origin + if user.idnt!=ident: user.idnt=ident + if user.host!=host: user.host=host + + channel=self.channel(target) + self.event("onRecv", channel.modules, line=line, data=parsed) + + with channel.lock: + if channel.name!=target: channel.name=target ### Server seems to have changed the idea of the case of the channel name + channel.topic=extinfo + elif cmd=="PRIVMSG": + user=self.user(origin) + if user.nick!=origin: user.nick=origin + if user.idnt!=ident: user.idnt=ident + if user.host!=host: user.host=host + + if target[0] in self.supports["CHANTYPES"]: + channel=self.channel(target) + self.event("onRecv", channel.modules, line=line, data=parsed) + if channel.name!=target: channel.name=target ### Server seems to have changed the idea of the case of the channel name + elif target[0]=="$": + pass ### Server message -- Not implemented + else: + targetuser=self.user(target) + if targetuser.nick!=target: targetuser.nick=target ### Server seems to have changed the idea of the case of the nickname + + ### CTCP handling + ctcp=re.findall("^\x01(.*?)(?:\\s+(.*?)\\s*)?\x01$",extinfo) + if ctcp: + (ctcptype,ext)=ctcp[0] + if ctcptype.upper()=="VERSION": + user.ctcpreply("VERSION",self.ctcpversion()) + if ctcptype.upper()=="TIME": + tformat=time.ctime() + tz=time.tzname[0] + user.ctcpreply("TIME","%(tformat)s %(tz)s" % vars()) + if ctcptype.upper()=="PING": user.ctcpreply("PING","%(ext)s" % vars()) + if ctcptype.upper()=="FINGER": user.ctcpreply("FINGER","%(ext)s" % vars()) + elif cmd==910: # Channel Access List + (channame,mask,setby,settime)=params.split() + channel=self.channel(channame) + self.event("onRecv", channel.modules, line=line, data=parsed) + if channel.name!=channame: channel.name=channame ### Server seems to have changed the idea of the case of the channel name + if "w" in channel.modes.keys(): + if mask not in [m for (m, b, t) in channel.modes["w"]]: channel.modes["w"].append((mask, setby, int(settime))) + else: channel.modes["w"]=[(mask, setby, int(settime))] + elif cmd==941: # Channel spamfilter List + (channame,mask,setby,settime)=params.split() + channel=self.channel(channame) + self.event("onRecv", channel.modules, line=line, data=parsed) + if channel.name!=channame: channel.name=channame ### Server seems to have changed the idea of the case of the channel name + if "g" in channel.modes.keys(): + if mask not in [m for (m, b, t) in channel.modes["g"]]: channel.modes["g"].append((mask, setby, int(settime))) + else: channel.modes["g"]=[(mask, setby, int(settime))] + elif cmd==954: # Channel spamfilter List + (channame,mask,setby,settime)=params.split() + channel=self.channel(channame) + self.event("onRecv", channel.modules, line=line, data=parsed) + if channel.name!=channame: channel.name=channame ### Server seems to have changed the idea of the case of the channel name + if "X" in channel.modes.keys(): + if mask not in [m for (m, b, t) in channel.modes["X"]]: channel.modes["X"].append((mask, setby, int(settime))) + else: channel.modes["X"]=[(mask, setby, int(settime))] + 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.modules, line=line, data=parsed) + + else: ### Line does NOT match ":src cmd target params :extinfo" + self.event("onRecv", self.modules, line=line, data=None) + 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. + with self.loglock: + exc,excmsg,tb=sys.exc_info() + timestamp=reduce(lambda x,y: x+":"+y,[str(t).rjust(2,"0") for t in time.localtime()[0:6]]) + print >>self.log, "%(timestamp)s *** Connection to %(server)s:%(port)s failed: %(excmsg)s." % vars() + self.log.flush() + 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. + try: + 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: + modules=list(self.modules) + for channel in self.channels: + for module in channel.modules: + if module not in modules: modules.append(module) + self.event("onDisconnect", self.modules) + + ### Tell outgoing thread to quit. + self.outgoing.put("quit") + + ### Wait until the outgoing thread dies. + outgoingthread.join() + + self.connected=False + self.registered=False + + try: self.connection.close() + except: pass + + with self.loglock: + timestamp=reduce(lambda x,y: x+":"+y,[str(t).rjust(2,"0") for t in time.localtime()[0:6]]) + print >>self.log, "%(timestamp)s *** Connection Terminated." % vars() + self.log.flush() + + if self.quitexpected or not self.autoreconnect: 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 + with self.loglock: + timestamp=reduce(lambda x,y: x+":"+y,[str(t).rjust(2,"0") for t in time.localtime()[0:6]]) + print >>self.log, "%(timestamp)s !!! Fatal Exception" % vars() + for tbline in traceback.format_exc().split("\n"): + print >>self.log, "%(timestamp)s !!! %(tbline)s" % vars() + self.log.flush() + sys.exit() + + finally: + with self.loglock: + timestamp=reduce(lambda x,y: x+":"+y,[str(t).rjust(2,"0") for t in time.localtime()[0:6]]) + print >>self.log, "%(timestamp)s ### Log session ended" % vars() + self.log.flush() + + Thread.__init__(self) ### Makes thread restartable + + def __repr__(self): + server=self.server + if self.ipv6 and ":" in server: + server="[%s]"%server + port=self.port + if self.identity: + nick=self.identity.nick + ident=self.identity.idnt if self.identity.idnt else "*" + host=self.identity.host if self.identity.host else "*" + else: + nick="*" + ident="*" + host="*" + if self.ssl and self.ipv6: + protocol="ircs6" + elif self.ssl: + protocol="ircs" + elif self.ipv6: + protocol="irc6" + else: + protocol="irc" + return "" % locals() + #else: return "" % locals() + def quit(self, msg="", origin=None): + with self.lock: + self.quitexpected=True + if self.connected: + if len(msg): self.raw("QUIT :%(msg)s" % vars(), origin=origin) + else: self.raw("QUIT", origin=origin) + def ctcpversion(self): + reply=[] + ### Prepare reply for module + reply.append("%(__name__)s %(__version__)s, %(__author__)s" % vars(self)) + + ### Prepare reply for Python and OS versions + pyver=sys.version.split("\n") + pyver[0]="Python "+pyver[0] + reply.extend(pyver) + reply.extend(platform.platform().split("\n")) + ### Prepare reply for extension modules + for module in self.modules: + try: + r="%(__name__)s %(__version__)s" % vars(module) + if "__extinfo__" in vars(module): r+=", %(__extinfo__)s" % vars() + reply.append(r) + except: + pass + return reduce(lambda x, y: "%s; %s" % (x,y),reply) + + def raw(self, line, origin=None): + self.outgoing.put((line, origin)) + + + def user(self,nick): + users=[user for user in self.users if user.nick.lower()==nick.lower()] + if len(users): + return users[0] + else: + user=User(nick,self) + self.users.append(user) + timestamp=reduce(lambda x,y: x+":"+y,[str(t).rjust(2,"0") for t in time.localtime()[0:6]]) + with self.loglock: + print >>self.log, "%s *** User %s created."%(timestamp, nick) + self.log.flush() + 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] + 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) + with self.loglock: + print >>self.log, "%s *** Channel %s created."%(timestamp, name) + self.log.flush() + return chan + + + +class Channel(object): + def __init__(self,name,context): + self.name=name + self.context=context + self.modules=[] + self.topic="" + self.topicsetby="" + self.topictime=() + self.topicmod="" + self.modes={} + self.users=[] + self.created=None + self.lock=Lock() + def msg(self, msg, origin=None): + chan=self.name + self.context.raw("PRIVMSG %(chan)s :%(msg)s" % vars(), origin=origin) + def settopic(self, msg, origin=None): + chan=self.name + self.context.raw("TOPIC %(chan)s :%(msg)s" % vars(), origin=origin) + def notice(self, msg, target="", origin=None): + chan=self.name + self.context.raw("NOTICE %(target)s%(chan)s :%(msg)s" % vars(), origin=origin) + def ctcp(self, act, msg="", origin=None): + if len(msg): self.msg("\01%(act)s %(msg)s\01" % vars(), origin=origin) + else: self.msg("\01%(act)s\01" % vars()) + def ctcpreply(self, act, msg="", origin=None): + if len(msg): self.notice("\01%(act)s %(msg)s\01" % vars(), origin=origin) + else: self.notice("\01%(act)s\01" % vars(), origin=origin) + def me(self,msg="", origin=None): self.ctcp("ACTION", msg, origin=origin) + def part(self, msg="", origin=None): + chan=self.name + if msg: self.context.raw("PART %(chan)s :%(msg)s" % vars(), origin=origin) + else: self.context.raw("PART %(chan)s" % vars(), origin) + def join(self, key="", origin=None): + chan=self.name + if key: self.context.raw("JOIN :%(chan)s %(key)s" % vars(), origin=origin) + else: self.context.raw("JOIN :%(chan)s" % vars(), origin=origin) + def kick(self, nick, msg="", origin=None): + chan=self.name + if len(msg): self.context.raw("KICK %(chan)s %(nick)s :%(msg)s" % vars(), origin=origin) + else: self.context.raw("KICK %(chan)s %(nick)s" % vars(), origin=origin) + def __repr__(self): + return "" +class User(object): + def __init__(self,nick,context): + self.nick=nick + self.idnt="" + self.host="" + self.channels=[] + self.context=context + self.modes="" + self.snomask="" + self.server=None + self.hops=None + self.ircop=False + def __repr__(self): + return "" % vars(self) + def msg(self, msg, origin=None): + nick=self.nick + self.context.raw("PRIVMSG %(nick)s :%(msg)s" % vars(), origin=origin) + def notice(self, msg, origin=None): + nick=self.nick + self.context.raw("NOTICE %(nick)s :%(msg)s" % vars(), origin=origin) + def ctcp(self, act, msg="", origin=None): + if len(msg): self.msg("\01%(act)s %(msg)s\01" % vars(), origin=origin) + else: self.msg("\01%(act)s\01" % vars(), origin=origin) + def ctcpreply(self, act, msg="", origin=None): + if len(msg): self.notice("\01%(act)s %(msg)s\01" % vars(), origin=origin) + else: self.notice("\01%(act)s\01" % vars(), origin=origin) + def me(self, msg, origin=None): self.ctcp("ACTION", msg, origin=origin) +class SendLines(Thread): + def __init__(self, connection, lines, origin=None): + self.connection=connection + self.lines=lines + self.origin=origin + Thread.__init__(self) + def run(self): + for line in self.lines: + self.connection.raw(reply, origin=self.origin) + self.connection.log.flush() + time.sleep(2) + +class Outgoing(Thread): + def __init__(self, IRC, throttle=0.25, lines=40, t=5): + self.IRC=IRC + self.throttle=throttle + self.lines=lines + self.time=t + #self.queue=Queue() + Thread.__init__(self) + def run(self): + throttled=False + timestamps=[] + while True: + q=self.IRC.outgoing.get() + 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] + if cmd.upper()=="QUIT": self.IRC.quitexpected=True + 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 + + ### Modify line if it contains a password so that the password is not logged or sent to any potentially untrustworthy modules + #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) + #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.modules, line=line, data=(cmd, target, params, extinfo), origin=origin) + with self.IRC.loglock: + print >>self.IRC.log, "%(timestamp)s >>> %(line)s" % vars() + self.IRC.log.flush() + timestamps.append(time.time()) + while timestamps[0]=self.lines: throttled=True + if throttled: time.sleep(max(timestamps[-1]+self.throttle-time.time(),0)) + + +class Pinger(Thread): + def __init__(self, connection, lock=None): + self.connection=connection + self.lock=lock + self.daemon=True + Thread.__init__(self) + + def run(self): + pass \ No newline at end of file diff --git a/logger.py b/logger.py deleted file mode 120000 index e1dc8ce..0000000 --- a/logger.py +++ /dev/null @@ -1 +0,0 @@ -../logger.py \ No newline at end of file diff --git a/logger.py b/logger.py new file mode 100644 index 0000000..043b0f8 --- /dev/null +++ b/logger.py @@ -0,0 +1,310 @@ +#!/usr/bin/python + +from threading import Thread, Lock +import re, time, sys, string, socket, os, platform, traceback, Queue, ssl, urllib2 + +class LogRotate(Thread): + def __init__(self, logger): + self.logger=logger + Thread.__init__(self) + self.daemon=True + self.start() + def run(self): + 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))) + #print time.time()-nextrotate + #print time.localtime(), time.localtime(nextrotate) + 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)))) + with self.logger.rotatelock: + if all([not log or log.closed for log in self.logger.consolelogs.values()+self.logger.channellogs.values()]): + ### If there are no logs to rotate, we are going to terminate this thread. Logger will spawn another LogRotate thread when needed. + self.logger.logrotate=None + break + now=time.localtime() + timestamp=reduce(lambda x,y: x+":"+y,[str(t).rjust(2,"0") for t in now[0:6]]) + for IRC in self.logger.labels.keys(): + if IRC.connected: + with IRC.lock: + try: + self.logger.rotateConsoleLog(IRC) + except: + with IRC.loglock: + exc,excmsg,tb=sys.exc_info() + print >>IRC.log, "%(timestamp)s !!! [LogRotate] Exception in module %(module)s" % vars() + for tbline in traceback.format_exc().split("\n"): + print >>IRC.log, "%(timestamp)s !!! [LogRotate] %(tbline)s" % vars() + IRC.log.flush() + if IRC.identity: + for channel in IRC.identity.channels: + try: + self.logger.rotateChannelLog(channel) + except: + with IRC.loglock: + exc,excmsg,tb=sys.exc_info() + print >>IRC.log, "%(timestamp)s !!! [LogRotate] Exception in module %(module)s" % vars() + for tbline in traceback.format_exc().split("\n"): + print >>IRC.log, "%(timestamp)s !!! [LogRotate] %(tbline)s" % vars() + IRC.log.flush() + nextrotate+=3600*24 + +class Logger(object): + def __init__(self, logroot): + self.logroot=logroot + path=[logroot] + #print path + while not os.path.isdir(path[0]): + split=os.path.split(path[0]) + path.insert(1,split[1]) + path[0]=split[0] + #print path + while len(path)>1: + path[0]=os.path.join(*path[:2]) + del path[1] + #print path + os.mkdir(path[0]) + #return + self.consolelogs={} + self.channellogs={} + self.labels={} + self.rotatelock=Lock() + self.logrotate=None + def onModuleAdd(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.openConsoleLog(IRC) + if IRC.identity: + for channel in IRC.identity.channels: + self.openChannelLog(channel) + + def onModuleRem(self, IRC): + if IRC.connected: + for channel in self.channellogs.keys(): + if channel in IRC.channels: + if not self.channellogs[channel].closed: self.closeChannelLog(channel) + del self.channellogs[channel] + if not self.consolelogs[IRC].closed: self.closeConsoleLog(IRC) + del self.labels[IRC] + del self.consolelogs[IRC] + + def openConsoleLog(self, IRC): + with self.rotatelock: + if not self.logrotate or not self.logrotate.isAlive(): + self.logrotate=LogRotate(self) + now=time.localtime() + timestamp=reduce(lambda x,y: x+":"+y,[str(t).rjust(2,"0") for t in now[0:6]]) + self.consolelogs[IRC]=open(os.path.join(self.logroot, self.labels[IRC], "console-%04d.%02d.%02d.log"%now[:3]), "a") + print >>self.consolelogs[IRC], "%s %s ### Log session started" % (timestamp, time.tzname[now[-1]]) + self.consolelogs[IRC].flush() + def closeConsoleLog(self, IRC): + if IRC in self.consolelogs.keys() and type(self.consolelogs[IRC])==file and not self.consolelogs[IRC].closed: + now=time.localtime() + timestamp=reduce(lambda x,y: x+":"+y,[str(t).rjust(2,"0") for t in now[0:6]]) + print >>self.consolelogs[IRC], "%s %s ### Log session ended" % (timestamp, time.tzname[now[-1]]) + self.consolelogs[IRC].close() + def rotateConsoleLog(self, IRC): + self.closeConsoleLog(IRC) + self.openConsoleLog(IRC) + + def openChannelLog(self, channel): + with self.rotatelock: + if not self.logrotate or not self.logrotate.isAlive(): + self.logrotate=LogRotate(self) + self.logrotate.daemon=True + self.logrotate.start() + now=time.localtime() + timestamp=reduce(lambda x,y: x+":"+y,[str(t).rjust(2,"0") for t in now[0:6]]) + label=self.labels[channel.context] + self.channellogs[channel]=open(os.path.join(self.logroot, label, "channel-%s-%04d.%02d.%02d.log"%((urllib2.quote(channel.name.lower()).replace("/","%2f"),)+now[:3])), "a") + print >>self.channellogs[channel], "%s %s ### Log session started" % (timestamp, time.tzname[now[-1]]) + self.channellogs[channel].flush() + if channel.context.identity in channel.users: + if channel.topic: print >>self.channellogs[channel], "%s %s <<< :%s 332 %s %s :%s" % (timestamp, time.tzname[now[-1]], channel.context.serv, channel.context.identity.nick, channel.name, channel.topic) + if channel.topicsetby and channel.topictime: print >>self.channellogs[channel], "%s %s <<< :%s 333 %s %s %s %s" % (timestamp, time.tzname[now[-1]], channel.context.serv, channel.context.identity.nick, channel.name, channel.topicsetby, channel.topictime) + if channel.users: + secret="s" in channel.modes.keys() and channel.modes["s"] + private="p" in channel.modes.keys() and channel.modes["p"] + namesusers=[] + modes, symbols=channel.context.supports["PREFIX"] + print >>self.channellogs[channel], "%s %s <<< :%s 353 %s %s %s :%s" % (timestamp, time.tzname[now[-1]], + channel.context.serv, + channel.context.identity.nick, + "@" if secret else ("*" if private else "="), + channel.name, + " ".join(["".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])) + if channel.modes: + modes=channel.modes.keys() + modestr="".join([mode for mode in modes if mode not in channel.context.supports["CHANMODES"][0]+channel.context.supports["PREFIX"][0] and channel.modes[mode]]) + params=" ".join([channel.modes[mode] for mode in modes if mode in channel.context.supports["CHANMODES"][1]+channel.context.supports["CHANMODES"][2] and channel.modes[mode]]) + print >>self.channellogs[channel], "%s %s <<< :%s 324 %s %s +%s %s" % (timestamp, time.tzname[now[-1]], channel.context.server, channel.context.identity.nick, channel.name, modestr, params) + if channel.created: print >>self.channellogs[channel], "%s %s <<< :%s 329 %s %s %s" % (timestamp, time.tzname[now[-1]], channel.context.serv, channel.context.identity.nick, channel.name, channel.created) + self.channellogs[channel].flush() + def closeChannelLog(self, channel): + if channel in self.channellogs.keys() and type(self.channellogs[channel])==file and not self.channellogs[channel].closed: + now=time.localtime() + timestamp=reduce(lambda x,y: x+":"+y,[str(t).rjust(2,"0") for t in now[0:6]]) + print >>self.channellogs[channel], "%s %s ### Log session ended" % (timestamp, time.tzname[now[-1]]) + self.channellogs[channel].close() + def rotateChannelLog(self, channel): + self.closeChannelLog(channel) + self.openChannelLog(channel) + def onRecv(self, IRC, line, data): + modemapping=dict(Y="ircop", q="owner", a="admin", o="op", h="halfop", v="voice") + now=time.localtime() + timestamp=reduce(lambda x,y: x+":"+y,[str(t).rjust(2,"0") for t in now[0:6]]) + if data==None: + print >>self.consolelogs[IRC], "%s %s <<< %s" % (timestamp, time.tzname[now[-1]], line) + self.consolelogs[IRC].flush() + return + (origin, ident, host, cmd, target, params, extinfo)=data + if re.match("^\\d+$",cmd): cmd=int(cmd) + if cmd in (324, 329): + modeparams=params.split() + channame=modeparams[0] + channel=IRC.channel(channame) + if channel in self.channellogs.keys() and not self.channellogs[channel].closed: log=self.channellogs[channel] + else: log=self.consolelogs[IRC] + print >>log, "%s %s <<< %s" % (timestamp, time.tzname[now[-1]], line) + log.flush() + elif cmd == 332: + channel=IRC.channel(params) + if channel in self.channellogs.keys() and not self.channellogs[channel].closed: log=self.channellogs[channel] + else: log=self.consolelogs[IRC] + print >>log, "%s %s <<< %s" % (timestamp, time.tzname[now[-1]], line) + log.flush() + elif cmd == 333: + (channame,nick,dt)=params.split() + channel=IRC.channel(channame) + if not self.channellogs[channel].closed: + print >>self.channellogs[channel], "%s %s <<< %s" % (timestamp, time.tzname[now[-1]], line) + self.channellogs[channel].flush() + elif cmd == 353: + (flag, channame)=params.split() + channel=IRC.channel(channame) + if not self.channellogs[channel].closed: + print >>self.channellogs[channel], "%s %s <<< %s" % (timestamp, time.tzname[now[-1]], line) + self.channellogs[channel].flush() + elif cmd=="JOIN": + user=IRC.user(origin) + channel=IRC.channel(target if len(target) else extinfo) + if user==IRC.identity: + self.openChannelLog(channel) + print >>self.channellogs[channel], "%s %s <<< %s" % (timestamp, time.tzname[now[-1]], line) + self.channellogs[channel].flush() + elif cmd == "PRIVMSG": + if target and target[0] in IRC.supports["CHANTYPES"]: + channel=IRC.channel(target) + if ident and host: + user=IRC.user(origin) + classes=" ".join([modemapping[mode] for mode in IRC.supports["PREFIX"][0] if mode in channel.modes.keys() and user in channel.modes[mode]]) + else: + classes="server" + if classes: print >>self.channellogs[channel], "%s %s %s <<< %s" % (timestamp, time.tzname[now[-1]], classes, line) + else: print >>self.channellogs[channel], "%s %s <<< %s" % (timestamp, time.tzname[now[-1]], line) + self.channellogs[channel].flush() + elif cmd == "NOTICE": + if target and (target[0] in IRC.supports["CHANTYPES"] or (len(target)>1 and target[0] in IRC.supports["PREFIX"][1] and target[1] in IRC.supports["CHANTYPES"])): + if target[0] in IRC.supports["PREFIX"][1]: + channel=IRC.channel(target[1:]) + else: + channel=IRC.channel(target) + if ident and host: + user=IRC.user(origin) + classes=" ".join([modemapping[mode] for mode in IRC.supports["PREFIX"][0] if mode in channel.modes.keys() and user in channel.modes[mode]]) + else: + classes="server" + if classes: print >>self.channellogs[channel], "%s %s %s <<< %s" % (timestamp, time.tzname[now[-1]], classes, line) + else: print >>self.channellogs[channel], "%s %s <<< %s" % (timestamp, time.tzname[now[-1]], line) + self.channellogs[channel].flush() + elif target.lower()==IRC.identity.nick.lower() and not ident and not host: + print >>self.consolelogs[IRC], "%s %s <<< %s" % (timestamp, time.tzname[now[-1]], line) + self.consolelogs[IRC].flush() + elif cmd == "TOPIC": + channel=IRC.channel(target) + print >>self.channellogs[channel], "%s %s <<< %s" % (timestamp, time.tzname[now[-1]], line) + self.channellogs[channel].flush() + elif cmd == "PART": + user=IRC.user(origin) + channel=IRC.channel(target) + print >>self.channellogs[channel], "%s %s <<< %s" % (timestamp, time.tzname[now[-1]], line) + self.channellogs[channel].flush() + if user==IRC.identity: + self.closeChannelLog(channel) + elif cmd == "KICK": + kicked=IRC.user(params) + channel=IRC.channel(target) + print >>self.channellogs[channel], "%s %s <<< %s" % (timestamp, time.tzname[now[-1]], line) + self.channellogs[channel].flush() + if kicked==IRC.identity: + self.closeChannelLog(channel) + elif cmd == "MODE": + if target and target[0] in IRC.supports["CHANTYPES"]: + channel=IRC.channel(target) + print >>self.channellogs[channel], "%s %s <<< %s" % (timestamp, time.tzname[now[-1]], line) + self.channellogs[channel].flush() + else: + print >>self.consolelogs[IRC], "%s %s <<< %s" % (timestamp, time.tzname[now[-1]], line) + self.consolelogs[IRC].flush() + elif cmd in ("NICK", "QUIT"): + user=IRC.user(origin) + for channel in user.channels: + print >>self.channellogs[channel], "%s %s <<< %s" % (timestamp, time.tzname[now[-1]], line) + self.channellogs[channel].flush() + else: + print >>self.consolelogs[IRC], "%s %s <<< %s" % (timestamp, time.tzname[now[-1]], line) + self.consolelogs[IRC].flush() + def onConnectAttempt(self, IRC): + if IRC not in self.consolelogs.keys() or (not self.consolelogs[IRC]) or self.consolelogs[IRC].closed: + self.openConsoleLog(IRC) + now=time.localtime() + timestamp=reduce(lambda x,y: x+":"+y,[str(t).rjust(2,"0") for t in now[0:6]]) + print >>self.consolelogs[IRC], "%s %s *** Attempting connection to %s:%s." % (timestamp, time.tzname[now[-1]], IRC.server, IRC.port) + def onConnect(self, IRC): + if IRC not in self.consolelogs.keys() or (not self.consolelogs[IRC]) or self.consolelogs[IRC].closed: + self.openConsoleLog(IRC) + #self.openConsoleLog(IRC) + now=time.localtime() + timestamp=reduce(lambda x,y: x+":"+y,[str(t).rjust(2,"0") for t in now[0:6]]) + print >>self.consolelogs[IRC], "%s %s *** Connection to %s:%s established." % (timestamp, time.tzname[now[-1]], IRC.server, IRC.port) + def onDisconnect(self, IRC): + now=time.localtime() + timestamp=reduce(lambda x,y: x+":"+y,[str(t).rjust(2,"0") for t in now[0:6]]) + for channel in IRC.identity.channels: + print >>self.channellogs[channel], "%s %s *** Connection to %s:%s terminated." % (timestamp, time.tzname[now[-1]], IRC.server, IRC.port) + self.channellogs[channel].flush() + self.closeChannelLog(channel) + print >>self.consolelogs[IRC], "%s %s *** Connection %s:%s terminated." % (timestamp, time.tzname[now[-1]], IRC.server, IRC.port) + self.consolelogs[IRC].flush() + self.closeConsoleLog(IRC) + def onSend(self, IRC, line, data, origin): + modemapping=dict(Y="ircop", q="owner", a="admin", o="op", h="halfop", v="voice") + now=time.localtime() + timestamp=reduce(lambda x,y: x+":"+y,[str(t).rjust(2,"0") for t in now[0:6]]) + (cmd, target, params, extinfo)=data + if IRC.registered and cmd=="PRIVMSG" and "CHANTYPES" in IRC.supports.keys() and len(target) and target[0] in IRC.supports["CHANTYPES"]: + channel=IRC.channel(target) + if channel in IRC.identity.channels: + classes=" ".join([modemapping[mode] for mode in IRC.supports["PREFIX"][0] if mode in channel.modes.keys() and IRC.identity in channel.modes[mode]]) + if classes: print >>self.channellogs[channel], "%s %s %s >>> :%s!%s@%s %s" % (timestamp, time.tzname[now[-1]], classes, IRC.identity.nick, IRC.identity.idnt, IRC.identity.host, line) + else: print >>self.channellogs[channel], "%s %s >>> :%s!%s@%s %s" % (timestamp, time.tzname[now[-1]], IRC.identity.nick, IRC.identity.idnt, IRC.identity.host, line) + self.channellogs[channel].flush() + else: + print >>self.consolelogs[IRC], "%s %s >>> :%s!%s@%s %s" % (timestamp, time.tzname[now[-1]], IRC.identity.nick, IRC.identity.idnt, IRC.identity.host, line) + self.consolelogs[IRC].flush() + if IRC.registered and len(target) and (target[0] in IRC.supports["CHANTYPES"] or (len(target)>1 and target[0] in IRC.supports["PREFIX"][1] and target[1] in IRC.supports["CHANTYPES"])) and cmd=="NOTICE": + channel=IRC.channel(target[1:] if target[0] in IRC.supports["PREFIX"][1] else target) + if channel in IRC.identity.channels: + classes=" ".join([modemapping[mode] for mode in IRC.supports["PREFIX"][0] if mode in channel.modes.keys() and IRC.identity in channel.modes[mode]]) + if classes: print >>self.channellogs[channel], "%s %s %s >>> :%s!%s@%s %s" % (timestamp, time.tzname[now[-1]], classes, IRC.identity.nick, IRC.identity.idnt, IRC.identity.host, line) + else: print >>self.channellogs[channel], "%s %s >>> :%s!%s@%s %s" % (timestamp, time.tzname[now[-1]], IRC.identity.nick, IRC.identity.idnt, IRC.identity.host, line) + self.channellogs[channel].flush() + else: + print >>self.consolelogs[IRC], "%s %s >>> :%s!%s@%s %s" % (timestamp, time.tzname[now[-1]], IRC.identity.nick, IRC.identity.idnt, IRC.identity.host, line) + self.consolelogs[IRC].flush() diff --git a/sedbot.py b/sedbot.py index fe34da4..9a23006 100644 --- a/sedbot.py +++ b/sedbot.py @@ -2,11 +2,12 @@ import os, re, time class SED(object): - def __init__(self): + def __init__(self, expiry=1800): self.__name__="SED Bot" self.__version__="0.0.1" + self.expiry=expiry self.history=[] - self.pattern=r"^!s([,/#])((?:.|\\\1)*)\1((?:.|\\\1)*)\1([ig]*)$" + self.pattern=r"^!?s([,/#])((?:.|\\\1)*)\1((?:.|\\\1)*)\1([ig]*)$" def onRecv(self, IRC, line, data): if data==None: return self.replace(IRC, *data) @@ -14,8 +15,11 @@ class SED(object): if origin==self: return #print data (cmd, target, params, extinfo)=data - self.replace(IRC, IRC.identity.nick, IRC.identity.idnt, IRC.identity.host, *data) + if IRC.identity: self.replace(IRC, IRC.identity.nick, IRC.identity.idnt, IRC.identity.host, *data) def replace(self, IRC, origin, ident, host, cmd, target, params, extinfo): + ### Clear out old data that has expired. + while len(self.history) and self.history[0][0]