X-Git-Url: https://jfr.im/git/erebus.git/blobdiff_plain/134c1193cd77499e34b68ba721173e34480a7f05..bac69af4f305607b00110e93f737a248767aa677:/erebus.py?ds=sidebyside diff --git a/erebus.py b/erebus.py index 3f26ea2..cd85e3e 100644 --- a/erebus.py +++ b/erebus.py @@ -1,9 +1,12 @@ #!/usr/bin/python +# vim: fileencoding=utf-8 # Erebus IRC bot - Author: John Runyon # main startup code -import os, sys, select, MySQLdb, MySQLdb.cursors, time, random, gc +from __future__ import print_function + +import os, sys, select, MySQLdb, MySQLdb.cursors, time, traceback, random, gc import bot, config, ctlmod class Erebus(object): #singleton to pass around @@ -15,17 +18,24 @@ class Erebus(object): #singleton to pass around numhandlers = {} msghandlers = {} chanhandlers = {} + exceptionhandlers = [] # list of (Exception_class, handler_function) tuples users = {} chans = {} class User(object): def __init__(self, nick, auth=None): self.nick = nick - self.auth = auth + if auth is None: + self.auth = None + else: + self.auth = auth.lower() self.checklevel() self.chans = [] + def bind_bot(self, bot): + return main._BoundUser(self, bot) + def msg(self, *args, **kwargs): main.randbot().msg(self, *args, **kwargs) def slowmsg(self, *args, **kwargs): @@ -56,6 +66,23 @@ class Erebus(object): #singleton to pass around self.glevel = 0 return self.glevel + def setlevel(self, level, savetodb=True): + if savetodb: + if level != 0: + c = main.query("REPLACE INTO users (auth, level) VALUES (%s, %s)", (self.auth, level)) + else: + c = main.query("DELETE FROM users WHERE auth = %s", (self.auth,)) + if c == 0: # no rows affected + c = True # is fine + if c: + self.glevel = level + return True + else: + return False + else: + self.glevel = level + return True + def join(self, chan): if chan not in self.chans: self.chans.append(chan) def part(self, chan): @@ -69,7 +96,23 @@ class Erebus(object): #singleton to pass around self.nick = newnick def __str__(self): return self.nick - def __repr__(self): return "" % (self.nick,self.glevel) + def __repr__(self): return "" % (self.nick, self.glevel) + + class _BoundUser(object): + def __init__(self, user, bot): + self.__dict__['_bound_user'] = user + self.__dict__['_bound_bot'] = bot + def __getattr__(self, name): + return getattr(self._bound_user, name) + def __setattr__(self, name, value): + setattr(self._bound_user, name, value) + def msg(self, *args, **kwargs): + self._bound_bot.msg(self._bound_user, *args, **kwargs) + def slowmsg(self, *args, **kwargs): + self._bound_bot.slowmsg(self._bound_user, *args, **kwargs) + def fastmsg(self, *args, **kwargs): + self._bound_bot.fastmsg(self._bound_user, *args, **kwargs) + def __repr__(self): return "<_BoundUser %r %r>" % (self._bound_user, self._bound_bot) class Channel(object): def __init__(self, name, bot): @@ -114,6 +157,9 @@ class Erebus(object): #singleton to pass around return True else: return False + else: + self.levels[auth] = level + return True def userjoin(self, user, level=None): if user not in self.users: self.users.append(user) @@ -137,6 +183,8 @@ class Erebus(object): #singleton to pass around def __repr__(self): return "" % (self.name) def __init__(self, cfg): + self.mustquit = None + self.starttime = time.time() self.cfg = cfg self.trigger = cfg.trigger if os.name == "posix": @@ -147,11 +195,11 @@ class Erebus(object): #singleton to pass around self.fdlist = [] def query(self, *args, **kwargs): - if 'norecurse' in kwargs: - norecurse = kwargs['norecurse'] - del kwargs['norecurse'] + if 'noretry' in kwargs: + noretry = kwargs['noretry'] + del kwargs['noretry'] else: - norecurse = False + noretry = False self.log("[SQL]", "?", "query(%s, %s)" % (', '.join([repr(i) for i in args]), ', '.join([str(key)+"="+repr(kwargs[key]) for key in kwargs]))) try: @@ -161,14 +209,22 @@ class Erebus(object): #singleton to pass around return curs else: return res + except MySQLdb.DataError as e: + self.log("[SQL]", ".", "MySQL DataError: %r" % (e)) + return False except MySQLdb.MySQLError as e: self.log("[SQL]", "!", "MySQL error! %r" % (e)) - if not norecurse: + if not noretry: dbsetup() - return self.query(*args, norecurse=True, **kwargs) + return self.query(*args, noretry=True, **kwargs) else: raise e + def querycb(self, cb, *args, **kwargs): + def run_query(): + cb(self.query(*args, **kwargs)) + threading.Thread(target=run_query).start() + def newbot(self, nick, user, bind, authname, authpass, server, port, realname): if bind is None: bind = '' obj = bot.Bot(self, nick, user, bind, authname, authpass, server, port, realname) @@ -180,25 +236,31 @@ class Erebus(object): #singleton to pass around self.po.register(fileno, select.POLLIN) elif self.potype == "select": self.fdlist.append(fileno) + def delfd(self, fileno): + del self.fds[fileno] + if self.potype == "poll": + self.po.unregister(fileno) + elif self.potype == "select": + self.fdlist.remove(fileno) def bot(self, name): #get Bot() by name (nick) return self.bots[name.lower()] def fd(self, fileno): #get Bot() by fd/fileno return self.fds[fileno] def randbot(self): #get Bot() randomly - return self.bots[random.choice(self.bots.keys())] + return self.bots[random.choice(list(self.bots.keys()))] - def user(self, _nick, justjoined=False, create=True): + def user(self, _nick, send_who=False, create=True): nick = _nick.lower() + + if send_who and (nick not in self.users or not self.users[nick].isauthed()): + self.randbot().conn.send("WHO %s n%%ant,1" % (nick)) + if nick in self.users: return self.users[nick] elif create: user = self.User(_nick) self.users[nick] = user - - if justjoined: - self.randbot().conn.send("WHO %s n%%ant,1" % (nick)) - return user else: return None @@ -214,13 +276,15 @@ class Erebus(object): #singleton to pass around return chan def poll(self): + timeout_seconds = 30 if self.potype == "poll": - return [fd for (fd, ev) in self.po.poll()] + pollres = self.po.poll(timeout_seconds * 1000) + return [fd for (fd, ev) in pollres] elif self.potype == "select": - return select.select(self.fdlist, [], [])[0] + return select.select(self.fdlist, [], [], timeout_seconds)[0] def connectall(self): - for bot in self.bots.itervalues(): + for bot in self.bots.values(): if bot.conn.state == 0: bot.connect() @@ -228,10 +292,17 @@ class Erebus(object): #singleton to pass around return ctlmod.modules[name] def log(self, source, level, message): - print "%09.3f %s [%s] %s" % (time.time() % 100000, source, level, message) + print("%09.3f %s [%s] %s" % (time.time() % 100000, source, level, message)) def getuserbyauth(self, auth): - return [u for u in self.users.itervalues() if u.auth == auth.lower()] + return [u for u in self.users.values() if u.auth == auth.lower()] + + def getdb(self): + """Get a DB object. The object must be returned to the pool after us, using returndb().""" + return self.dbs.pop() + + def returndb(self, db): + self.dbs.append(db) #bind functions def hook(self, word, handler): @@ -273,15 +344,27 @@ class Erebus(object): #singleton to pass around def getchanhook(self, chan): return self.chanhandlers[chan] + def hookexception(self, exc, handler): + self.exceptionhandlers.append((exc, handler)) + def unhookexception(self, exc, handler): + self.exceptionhandlers.remove((exc, handler)) + def hasexceptionhook(self, exc): + return any((True for x,h in self.exceptionhandlers if isinstance(exc, x))) + def getexceptionhook(self, exc): + return (h for x,h in self.exceptionhandlers if isinstance(exc, x)) + def dbsetup(): main.db = None + main.dbs = [] + for i in range(cfg.get('erebus', 'num_db_connections', 2)-1): + main.dbs.append(MySQLdb.connect(host=cfg.dbhost, user=cfg.dbuser, passwd=cfg.dbpass, db=cfg.dbname, cursorclass=MySQLdb.cursors.DictCursor)) main.db = MySQLdb.connect(host=cfg.dbhost, user=cfg.dbuser, passwd=cfg.dbpass, db=cfg.dbname, cursorclass=MySQLdb.cursors.DictCursor) def setup(): global cfg, main - cfg = config.setup('bot.config') + cfg = config.Config('bot.config') if cfg.getboolean('debug', 'gc'): gc.set_debug(gc.DEBUG_LEAK) @@ -291,12 +374,12 @@ def setup(): pidfile.close() main = Erebus(cfg) + dbsetup() autoloads = [mod for mod, yes in cfg.items('autoloads') if int(yes) == 1] for mod in autoloads: ctlmod.load(main, mod) - dbsetup() c = main.query("SELECT nick, user, bind, authname, authpass FROM bots WHERE active = 1") if c: rows = c.fetchall() @@ -308,8 +391,26 @@ def setup(): def loop(): poready = main.poll() for fileno in poready: - for line in main.fd(fileno).getdata(): - main.fd(fileno).parse(line) + try: + data = main.fd(fileno).getdata() + except: + main.log('*', '!', 'Super-mega-emergency: getdata raised exception for socket %d' % (fileno)) + traceback.print_exc() + data = None + if data is None: + main.fd(fileno).close() + else: + for line in data: + if cfg.getboolean('debug', 'io'): + main.log(str(main.fd(fileno)), 'I', line) + try: + main.fd(fileno).parse(line) + except: + main.log('*', '!', 'Super-mega-emergency: parse raised exception for socket %d data %r' % (fileno, line)) + traceback.print_exc() + if main.mustquit is not None: + main.log('*', '!', 'Core exiting due to: %s' % (main.mustquit)) + raise main.mustquit if __name__ == '__main__': try: os.rename('logfile', 'oldlogs/%s' % (time.time()))