X-Git-Url: https://jfr.im/git/erebus.git/blobdiff_plain/5f5d669f59db6e2156db1e529cada7a353430087..c02e2c1df2674e795b2b25c0e226bd6096d7a838:/erebus.py diff --git a/erebus.py b/erebus.py index 61dd1c9..fb6fcc8 100644 --- a/erebus.py +++ b/erebus.py @@ -6,8 +6,8 @@ from __future__ import print_function -import os, sys, select, MySQLdb, MySQLdb.cursors, time, traceback, random, gc -import bot, config, ctlmod +import os, sys, select, time, traceback, random, gc +import bot, config, ctlmod, modlib class Erebus(object): #singleton to pass around APIVERSION = 0 @@ -124,6 +124,8 @@ class Erebus(object): #singleton to pass around self.voices = [] self.ops = [] + self.deleting = False # if true, the bot will remove cached records of this channel when the bot sees that it has left the channel + c = main.query("SELECT user, level FROM chusers WHERE chan = %s", (self.name,)) if c: row = c.fetchone() @@ -194,30 +196,41 @@ class Erebus(object): #singleton to pass around self.potype = "select" self.fdlist = [] - def query(self, *args, **kwargs): - if 'noretry' in kwargs: - noretry = kwargs['noretry'] - del kwargs['noretry'] - else: - noretry = False + def query(self, sql, parameters=[], noretry=False): + # Callers use %s-style (paramstyle='format') placeholders in queries. + # There's no provision for a literal '%s' present inside the query; stuff it in a parameter instead. + if db_api.paramstyle == 'format' or db_api.paramstyle == 'pyformat': # mysql, postgresql + # psycopg actually asks for a mapping with %(name)s style (pyformat) but it will accept %s style. + pass + elif db_api.paramstyle == 'qmark': # sqlite doesn't like %s style. + parameters = [str(p) for p in parameters] + sql = sql.replace('%s', '?') # hope that wasn't literal, oopsie + + log_noretry = '' + if noretry: + log_noretry = ', noretry=True' + self.log("[SQL]", "?", "query(%r, %r%s)" % (sql, parameters, log_noretry)) - self.log("[SQL]", "?", "query(%s, %s)" % (', '.join([repr(i) for i in args]), ', '.join([str(key)+"="+repr(kwargs[key]) for key in kwargs]))) try: curs = self.db.cursor() - res = curs.execute(*args, **kwargs) + res = curs.execute(sql, parameters) if res: return curs else: return res - except MySQLdb.MySQLError as e: - self.log("[SQL]", "!", "MySQL error! %r" % (e)) + except db_api.DataError as e: + self.log("[SQL]", ".", "DB DataError: %r" % (e)) + return False + except db_api.Error as e: + self.log("[SQL]", "!", "DB error! %r" % (e)) if not noretry: dbsetup() - return self.query(*args, noretry=True, **kwargs) + return self.query(sql, parameters, noretry=True) else: raise e def querycb(self, cb, *args, **kwargs): + # TODO this should either get thrown out with getdb()/returndb(), or else be adjusted to make use of it. def run_query(): cb(self.query(*args, **kwargs)) threading.Thread(target=run_query).start() @@ -228,6 +241,8 @@ class Erebus(object): #singleton to pass around self.bots[nick.lower()] = obj def newfd(self, obj, fileno): + if not isinstance(obj, modlib.Socketlike): + raise Exception('Attempted to hook a socket without a class to process data') self.fds[fileno] = obj if self.potype == "poll": self.po.register(fileno, select.POLLIN) @@ -295,7 +310,9 @@ class Erebus(object): #singleton to pass around 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().""" + """Get a DB object. The object must be returned to the pool after us, using returndb(). This is intended for use from child threads. + It should probably be treated as deprecated though. Where possible new modules should avoid using threads. + In the future, timers will be provided (manipulating the timeout_seconds of the poll() method), and that should mostly be used in place of threading.""" return self.dbs.pop() def returndb(self, db): @@ -354,9 +371,29 @@ class Erebus(object): #singleton to pass around def dbsetup(): main.db = None main.dbs = [] + dbtype = cfg.get('erebus', 'dbtype', 'mysql') + if dbtype == 'mysql': + _dbsetup_mysql() + elif dbtype == 'sqlite': + _dbsetup_sqlite() + else: + main.log('*', '!', 'Unknown dbtype in config: %s' % (dbtype)) + +def _dbsetup_mysql(): + global db_api + import MySQLdb as db_api, MySQLdb.cursors 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) + main.dbs.append(db_api.connect(host=cfg.dbhost, user=cfg.dbuser, passwd=cfg.dbpass, db=cfg.dbname, cursorclass=MySQLdb.cursors.DictCursor)) + main.db = db_api.connect(host=cfg.dbhost, user=cfg.dbuser, passwd=cfg.dbpass, db=cfg.dbname, cursorclass=MySQLdb.cursors.DictCursor) + +def _dbsetup_sqlite(): + global db_api + import sqlite3 as db_api + for i in range(cfg.get('erebus', 'num_db_connections', 2)): + main.db = db_api.connect(cfg.dbhost) + main.db.row_factory = db_api.Row + main.db.isolation_level = None + main.dbs.append(main.db) def setup(): global cfg, main @@ -391,7 +428,7 @@ def loop(): try: data = main.fd(fileno).getdata() except: - main.log('*', '!', 'Super-mega-emergency: getdata raised exception for socket %d' % (fileno)) + main.log('*', '!', 'Error receiving data: getdata raised exception for socket %d, closing' % (fileno)) traceback.print_exc() data = None if data is None: @@ -403,7 +440,7 @@ def loop(): try: main.fd(fileno).parse(line) except: - main.log('*', '!', 'Super-mega-emergency: parse raised exception for socket %d data %r' % (fileno, line)) + main.log('*', '!', 'Error receiving data: parse raised exception for socket %d data %r, ignoring' % (fileno, line)) traceback.print_exc() if main.mustquit is not None: main.log('*', '!', 'Core exiting due to: %s' % (main.mustquit))