diff --git a/plugins/Dunno.py b/plugins/Dunno.py index d79030d67..a2315662a 100644 --- a/plugins/Dunno.py +++ b/plugins/Dunno.py @@ -40,6 +40,8 @@ __author__ = "Daniel DiPaolo (Strike) " import os import time +import random +import itertools import supybot.conf as conf import supybot.utils as utils @@ -47,22 +49,117 @@ import supybot.ircdb as ircdb import supybot.plugins as plugins import supybot.registry as registry import supybot.privmsgs as privmsgs +import supybot.ircutils as ircutils import supybot.callbacks as callbacks -try: - import sqlite -except ImportError: - raise callbacks.Error, 'You need to have PySQLite installed to use this ' \ - 'plugin. Download it at ' - -dbfilename = os.path.join(conf.supybot.directories.data(), 'Dunno.db') - - conf.registerPlugin('Dunno') conf.registerChannelValue(conf.supybot.plugins.Dunno, 'prefixNick', registry.Boolean(True, """Determines whether the bot will prefix the nick of the user giving an invalid command to the "dunno" response.""")) +class DunnoDBInterface(object): + def flush(self): + pass + + def close(self): + pass + + def add(self, channel, dunno, by, at): + """Adds a dunno and returns the id of the newly-added dunno.""" + raise NotImplementedError + + def remove(self, channel, id): + """Deletes the dunno with the given id.""" + raise NotImplementedError + + def get(self, channel, id): + """Returns the dunno with the given id.""" + raise NotImplementedError + + def change(self, channel, id, f): + """Changes the dunno with the given id using the given function f.""" + dunno = self.get(id) + newDunno = f(dunno) + self.set(id, newDunno) + + def random(self, channel): + """Returns a random (id, dunno) pair.""" + raise NotImplementedError + + def search(self, channel, p): + """Returns (id, dunno) pairs for each dunno that matches p.""" + raise NotImplementedError + + def size(self, channel): + """Returns the current number of dunnos in the database.""" + raise NotImplementedError + + +class FlatfileDunnoDB(DunnoDBInterface): + class DunnoDB(plugins.FlatfileDB): + def serialize(self, record): + return csv.join(map(str, record)) + + def deserialize(self, s): + L = csv.split(None, 2) + L[0] = float(L[0]) + L[1] = int(L[1]) + return L + + def __init__(self): + self.dbs = ircutils.IrcDict() + + def _getDb(self, channel): + if channel not in self.dbs: + filename = plugins.makeChannelFilename(channel, 'Dunno.db') + self.dbs[channel] = self.DunnoDB(filename) + return self.dbs[channel] + + def close(self): + for db in self.dbs.values(): + db.close() + + def add(self, channel, dunno, by, at): + db = self._getDb(channel) + return db.addRecord([at, by, dunno]) + + def remove(self, channel, id): + db = self._getDb(channel) + db.delRecord(id) + + def get(self, channel, id): + db = self._getDb(channel) + L = db.getRecord(id) + L.reverse() + return L # [dunno, by, at] + + def change(self, channel, id, f): + db = self._getDb(channel) + (at, by, dunno) = db.getRecord(id) + newDunno = f(dunno) + db.setRecord(id, [at, by, newDunno]) + + def random(self, channel): + db = self._getDb(channel) + (id, (at, by, dunno)) = random.choice(db.records()) + return (id, dunno) + + def search(self, channel, p): + L = [] + db = self._getDb(channel) + for (id, (at, by, dunno)) in db.records(): + if p(dunno): + L.append((id, dunno)) + return L + + def size(self, channel): + db = self._getDb(channel) + return itertools.ilen(db.records()) + + +DunnoDB = FlatfileDunnoDB + + class Dunno(callbacks.Privmsg): """This plugin was written initially to work with MoobotFactoids, the two of them to provide a similar-to-moobot-and-blootbot interface for factoids. @@ -72,129 +169,113 @@ class Dunno(callbacks.Privmsg): priority = 100 def __init__(self): callbacks.Privmsg.__init__(self) - self.makeDb(dbfilename) + self.db = DunnoDB() - def makeDb(self, filename): - """create Dunno database and tables""" - if os.path.exists(filename): - self.db = sqlite.connect(filename) - return - self.db = sqlite.connect(filename, converters={'bool': bool}) - cursor = self.db.cursor() - cursor.execute("""CREATE TABLE dunnos ( - id INTEGER PRIMARY KEY, - added_by INTEGER, - added_at TIMESTAMP, - dunno TEXT - )""") - self.db.commit() + def die(self): + self.db.close() def invalidCommand(self, irc, msg, tokens): - cursor = self.db.cursor() - cursor.execute("""SELECT dunno - FROM dunnos - ORDER BY random() - LIMIT 1""") - if cursor.rowcount != 0: - prefixName = self.registryValue('prefixNick', msg.args[0]) - dunno = cursor.fetchone()[0] - dunno = plugins.standardSubstitute(irc, msg, dunno) - irc.reply(dunno, prefixName=prefixName) + channel = msg.args[0] + if ircutils.isChannel(channel): + dunno = self.db.random(channel)[1] + if dunno is not None: + prefixName = self.registryValue('prefixNick', channel) + dunno = plugins.standardSubstitute(irc, msg, dunno) + irc.reply(dunno, prefixName=prefixName) def add(self, irc, msg, args): - """ + """[] Adds as a "dunno" to be used as a random response when no command or factoid key matches. Can optionally contain '$who', which will be replaced by the user's name when the dunno is displayed. + is only necessary if the message isn't sent in the channel + itself. """ - # Must be registered to use this + channel = privmsgs.getChannel(msg, args) try: - user_id = ircdb.users.getUserId(msg.prefix) + by = ircdb.users.getUserId(msg.prefix) except KeyError: irc.errorNotRegistered() return - text = privmsgs.getArgs(args) - cursor = self.db.cursor() + dunno = privmsgs.getArgs(args) at = int(time.time()) - cursor.execute("""INSERT INTO dunnos VALUES(NULL, %s, %s, %s)""", - user_id, at, text) - self.db.commit() - cursor.execute("""SELECT id FROM dunnos WHERE added_at=%s""", at) - id = cursor.fetchone()[0] + id = self.db.add(channel, dunno, by, at) irc.replySuccess('Dunno #%s added.' % id) def remove(self, irc, msg, args): - """ + """[] - Removes dunno with the given . + Removes dunno with the given . is only necessary if the + message isn't sent in the channel itself. """ # Must be registered to use this + channel = privmsgs.getChannel(msg, args) try: - user_id = ircdb.users.getUserId(msg.prefix) + by = ircdb.users.getUserId(msg.prefix) except KeyError: irc.errorNotRegistered() return - dunno_id = privmsgs.getArgs(args, required=1) - cursor = self.db.cursor() - cursor.execute("""SELECT added_by, dunno FROM dunnos - WHERE id=%s""", dunno_id) - if cursor.rowcount == 0: - irc.error('No dunno with id #%s.' % dunno_id) - return - (added_by, dunno) = cursor.fetchone() - if not (ircdb.checkCapability(user_id, 'admin') or \ - added_by == user_id): - irc.error('Only admins and the dunno creator may delete a dunno.') - return - cursor.execute("""DELETE FROM dunnos WHERE id=%s""", dunno_id) - self.db.commit() - irc.replySuccess() + id = privmsgs.getArgs(args) + (dunno, dunnoBy, at) = self.db.get(channel, id) + if by != dunnoBy: + cap = ircdb.makeChannelCapability(channel, 'op') + if not ircdb.users.checkCapability(cap): + irc.errorNoCapability(cap) + return + try: + self.db.remove(channel, id) + irc.replySuccess() + except KeyError: + irc.error('No dunno has id #%s.' % id) def search(self, irc, msg, args): - """ + """[] Search for dunno containing the given text. Returns the ids of the - dunnos with the text in them. + dunnos with the text in them. is only necessary if the + message isn't sent in the channel itself. """ - text = privmsgs.getArgs(args, required=1) - glob = "%" + text + "%" - cursor = self.db.cursor() - cursor.execute("""SELECT id FROM dunnos WHERE dunno LIKE %s""", glob) - if cursor.rowcount == 0: - irc.error('No dunnos with %r found.' % text) - return - ids = [str(t[0]) for t in cursor.fetchall()] - s = 'Dunno search for %r (%s found): %s.' % \ - (text, len(ids), utils.commaAndify(ids)) - irc.reply(s) + channel = privmsgs.getChannel(msg, args) + text = privmsgs.getArgs(args) + def p(s): + return text.lower() in s.lower() + ids = [str(id) for (id, _) in self.db.search(channel, p)] + if ids: + s = 'Dunno search for %r (%s found): %s.' % \ + (text, len(ids), utils.commaAndify(ids)) + irc.reply(s) + else: + irc.reply('No dunnos found matching that search criteria.') def get(self, irc, msg, args): - """ + """[] - Display the text of the dunno with the given id. + Display the text of the dunno with the given id. is only + necessary if the message isn't sent in the channel itself. """ - id = privmsgs.getArgs(args, required=1) + channel = privmsgs.getChannel(msg, args) + id = privmsgs.getArgs(args) try: id = int(id) except ValueError: irc.error('%r is not a valid dunno id.' % id) return - cursor = self.db.cursor() - cursor.execute("""SELECT dunno FROM dunnos WHERE id=%s""", id) - if cursor.rowcount == 0: - irc.error('No dunno found with id #%s.' % id) - return - dunno = cursor.fetchone()[0] - irc.reply("Dunno #%s: %r." % (id, dunno)) + try: + (dunno, by, at) = self.db.get(channel, id) + irc.reply("Dunno #%s: %r" % (id, dunno)) + except KeyError: + irc.error('No dunno found with that id.') def change(self, irc, msg, args): - """ + """[] Alters the dunno with the given id according to the provided regexp. + is only necessary if the message isn't sent in the channel + itself. """ - id, regexp = privmsgs.getArgs(args, required=2) - # Must be registered to use this + channel = privmsgs.getChannel(msg, args) + (id, regexp) = privmsgs.getArgs(args, required=2) try: user_id = ircdb.users.getUserId(msg.prefix) except KeyError: @@ -206,9 +287,9 @@ class Dunno(callbacks.Privmsg): except ValueError: irc.error('%r is not a valid dunno id.' % id) return - cursor = self.db.cursor() - cursor.execute("""SELECT dunno FROM dunnos WHERE id=%s""", id) - if cursor.rowcount == 0: + try: + _ = self.db.get(channel, id) + except KeyError: irc.error('There is no dunno #%s.' % id) return try: @@ -216,18 +297,17 @@ class Dunno(callbacks.Privmsg): except: irc.error('%r is not a valid regular expression.' % regexp) return - dunno = cursor.fetchone()[0] - new_dunno = replacer(dunno) - cursor.execute("""UPDATE dunnos SET dunno=%s WHERE id=%s""", - new_dunno, id) - self.db.commit() + self.db.change(channel, id, replacer) irc.replySuccess() def stats(self, irc, msg, args): - """Returns the number of dunnos in the dunno database.""" - cursor = self.db.cursor() - cursor.execute("""SELECT COUNT(*) FROM dunnos""") - num = int(cursor.fetchone()[0]) + """[] + + Returns the number of dunnos in the dunno database. is only + necessary if the message isn't sent in the channel itself. + """ + channel = privmsgs.getChannel(msg, args) + num = self.db.size(channel) irc.reply('There %s %s in my database.' % (utils.be(num), utils.nItems('dunno', num))) diff --git a/plugins/__init__.py b/plugins/__init__.py index 54e41981c..a42b29aa4 100644 --- a/plugins/__init__.py +++ b/plugins/__init__.py @@ -42,6 +42,7 @@ import math import sets import time import random +import os.path import urllib2 import UserDict import threading @@ -109,6 +110,12 @@ class DBHandler(object): self.cachedDb.die() del self.cachedDb +def makeChannelFilename(channel, filename): + # XXX We should put channel stuff in its own directory. + assert filename == os.path.basename(filename) + channel = ircutils.toLower(channel) + filename = '%s-%s' % (channel, filename) + return conf.supybot.directories.data.dirize(filename) # XXX: This shouldn't be a mixin. This should be contained by classes that # want such behavior. But at this point, it wouldn't gain much for us @@ -127,7 +134,8 @@ class ChannelDBHandler(object): def makeFilename(self, channel): """Override this to specialize the filenames of your databases.""" channel = ircutils.toLower(channel) - prefix = '%s-%s%s' % (channel, self.__class__.__name__, self.suffix) + prefix = makeChannelFilename(channel, + self.__class__.__name__ + self.suffix) return os.path.join(conf.supybot.directories.data(), prefix) def makeDb(self, filename): @@ -222,7 +230,7 @@ class ChannelUserDB(ChannelUserDictionary): log.debug('Exception: %s', utils.exnToString(e)) def flush(self): - fd = utils.AtomicFile(self.filename) + fd = utils.transactionalFile(self.filename) writer = csv.writer(fd) items = self.items() if not items: @@ -375,7 +383,7 @@ class FlatfileDB(object): def serialize(self, record): raise NotImplementedError - def unserialize(self, s): + def deserialize(self, s): raise NotImplementedError def _canonicalId(self, id): @@ -409,8 +417,9 @@ class FlatfileDB(object): fd = file(self.filename, 'r+') fd.seek(0, 2) # End. fd.write(line) - self._incrementCurrentId(fd) + return self.currentId finally: + self._incrementCurrentId(fd) fd.close() def getRecord(self, id): @@ -421,7 +430,7 @@ class FlatfileDB(object): for line in fd: (lineId, strRecord) = self._splitLine(line) if lineId == strId: - return self.unserialize(strRecord) + return self.deserialize(strRecord) raise KeyError, id finally: fd.close() @@ -466,7 +475,7 @@ class FlatfileDB(object): for line in fd: (strId, strRecord) = self._splitLine(line) if not strId.startswith('-'): - yield (int(strId), self.unserialize(strRecord)) + yield (int(strId), self.deserialize(strRecord)) fd.close() def vacuum(self): diff --git a/src/fix.py b/src/fix.py index 0f2f65b8f..66f1b9fb8 100644 --- a/src/fix.py +++ b/src/fix.py @@ -177,9 +177,25 @@ def attrgetter(attr): operator.itemgetter = itemgetter operator.attrgetter = attrgetter +import csv +import cStringIO as StringIO +def join(L): + fd = StringIO.StringIO() + writer = csv.writer(fd) + writer.writerow(L) + return fd.getvalue().rstrip('\r\n') + +def split(s): + fd = StringIO.StringIO(s) + reader = csv.reader(fd) + return reader.next() +csv.join = join +csv.split = split + for name in exported: __builtins__[name] = globals()[name] + # vim:set shiftwidth=4 tabstop=8 expandtab textwidth=78: