New Dunno implementation.

This commit is contained in:
Jeremy Fincher 2004-08-05 03:59:40 +00:00
parent a64d6b3881
commit 76a79b0d76
3 changed files with 211 additions and 106 deletions

View File

@ -40,6 +40,8 @@ __author__ = "Daniel DiPaolo (Strike) <ddipaolo@users.sf.net>"
import os import os
import time import time
import random
import itertools
import supybot.conf as conf import supybot.conf as conf
import supybot.utils as utils import supybot.utils as utils
@ -47,22 +49,117 @@ import supybot.ircdb as ircdb
import supybot.plugins as plugins import supybot.plugins as plugins
import supybot.registry as registry import supybot.registry as registry
import supybot.privmsgs as privmsgs import supybot.privmsgs as privmsgs
import supybot.ircutils as ircutils
import supybot.callbacks as callbacks 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 <http://pysqlite.sf.net/>'
dbfilename = os.path.join(conf.supybot.directories.data(), 'Dunno.db')
conf.registerPlugin('Dunno') conf.registerPlugin('Dunno')
conf.registerChannelValue(conf.supybot.plugins.Dunno, 'prefixNick', conf.registerChannelValue(conf.supybot.plugins.Dunno, 'prefixNick',
registry.Boolean(True, """Determines whether the bot will prefix the nick registry.Boolean(True, """Determines whether the bot will prefix the nick
of the user giving an invalid command to the "dunno" response.""")) 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): class Dunno(callbacks.Privmsg):
"""This plugin was written initially to work with MoobotFactoids, the two """This plugin was written initially to work with MoobotFactoids, the two
of them to provide a similar-to-moobot-and-blootbot interface for factoids. of them to provide a similar-to-moobot-and-blootbot interface for factoids.
@ -72,129 +169,113 @@ class Dunno(callbacks.Privmsg):
priority = 100 priority = 100
def __init__(self): def __init__(self):
callbacks.Privmsg.__init__(self) callbacks.Privmsg.__init__(self)
self.makeDb(dbfilename) self.db = DunnoDB()
def makeDb(self, filename): def die(self):
"""create Dunno database and tables""" self.db.close()
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 invalidCommand(self, irc, msg, tokens): def invalidCommand(self, irc, msg, tokens):
cursor = self.db.cursor() channel = msg.args[0]
cursor.execute("""SELECT dunno if ircutils.isChannel(channel):
FROM dunnos dunno = self.db.random(channel)[1]
ORDER BY random() if dunno is not None:
LIMIT 1""") prefixName = self.registryValue('prefixNick', channel)
if cursor.rowcount != 0: dunno = plugins.standardSubstitute(irc, msg, dunno)
prefixName = self.registryValue('prefixNick', msg.args[0]) irc.reply(dunno, prefixName=prefixName)
dunno = cursor.fetchone()[0]
dunno = plugins.standardSubstitute(irc, msg, dunno)
irc.reply(dunno, prefixName=prefixName)
def add(self, irc, msg, args): def add(self, irc, msg, args):
"""<text> """[<channel>] <text>
Adds <text> as a "dunno" to be used as a random response when no Adds <text> as a "dunno" to be used as a random response when no
command or factoid key matches. Can optionally contain '$who', which command or factoid key matches. Can optionally contain '$who', which
will be replaced by the user's name when the dunno is displayed. will be replaced by the user's name when the dunno is displayed.
<channel> 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: try:
user_id = ircdb.users.getUserId(msg.prefix) by = ircdb.users.getUserId(msg.prefix)
except KeyError: except KeyError:
irc.errorNotRegistered() irc.errorNotRegistered()
return return
text = privmsgs.getArgs(args) dunno = privmsgs.getArgs(args)
cursor = self.db.cursor()
at = int(time.time()) at = int(time.time())
cursor.execute("""INSERT INTO dunnos VALUES(NULL, %s, %s, %s)""", id = self.db.add(channel, dunno, by, at)
user_id, at, text)
self.db.commit()
cursor.execute("""SELECT id FROM dunnos WHERE added_at=%s""", at)
id = cursor.fetchone()[0]
irc.replySuccess('Dunno #%s added.' % id) irc.replySuccess('Dunno #%s added.' % id)
def remove(self, irc, msg, args): def remove(self, irc, msg, args):
"""<id> """[<channel>] <id>
Removes dunno with the given <id>. Removes dunno with the given <id>. <channel> is only necessary if the
message isn't sent in the channel itself.
""" """
# Must be registered to use this # Must be registered to use this
channel = privmsgs.getChannel(msg, args)
try: try:
user_id = ircdb.users.getUserId(msg.prefix) by = ircdb.users.getUserId(msg.prefix)
except KeyError: except KeyError:
irc.errorNotRegistered() irc.errorNotRegistered()
return return
dunno_id = privmsgs.getArgs(args, required=1) id = privmsgs.getArgs(args)
cursor = self.db.cursor() (dunno, dunnoBy, at) = self.db.get(channel, id)
cursor.execute("""SELECT added_by, dunno FROM dunnos if by != dunnoBy:
WHERE id=%s""", dunno_id) cap = ircdb.makeChannelCapability(channel, 'op')
if cursor.rowcount == 0: if not ircdb.users.checkCapability(cap):
irc.error('No dunno with id #%s.' % dunno_id) irc.errorNoCapability(cap)
return return
(added_by, dunno) = cursor.fetchone() try:
if not (ircdb.checkCapability(user_id, 'admin') or \ self.db.remove(channel, id)
added_by == user_id): irc.replySuccess()
irc.error('Only admins and the dunno creator may delete a dunno.') except KeyError:
return irc.error('No dunno has id #%s.' % id)
cursor.execute("""DELETE FROM dunnos WHERE id=%s""", dunno_id)
self.db.commit()
irc.replySuccess()
def search(self, irc, msg, args): def search(self, irc, msg, args):
"""<text> """[<channel>] <text>
Search for dunno containing the given text. Returns the ids of the Search for dunno containing the given text. Returns the ids of the
dunnos with the text in them. dunnos with the text in them. <channel> is only necessary if the
message isn't sent in the channel itself.
""" """
text = privmsgs.getArgs(args, required=1) channel = privmsgs.getChannel(msg, args)
glob = "%" + text + "%" text = privmsgs.getArgs(args)
cursor = self.db.cursor() def p(s):
cursor.execute("""SELECT id FROM dunnos WHERE dunno LIKE %s""", glob) return text.lower() in s.lower()
if cursor.rowcount == 0: ids = [str(id) for (id, _) in self.db.search(channel, p)]
irc.error('No dunnos with %r found.' % text) if ids:
return s = 'Dunno search for %r (%s found): %s.' % \
ids = [str(t[0]) for t in cursor.fetchall()] (text, len(ids), utils.commaAndify(ids))
s = 'Dunno search for %r (%s found): %s.' % \ irc.reply(s)
(text, len(ids), utils.commaAndify(ids)) else:
irc.reply(s) irc.reply('No dunnos found matching that search criteria.')
def get(self, irc, msg, args): def get(self, irc, msg, args):
"""<id> """[<channel>] <id>
Display the text of the dunno with the given id. Display the text of the dunno with the given id. <channel> 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: try:
id = int(id) id = int(id)
except ValueError: except ValueError:
irc.error('%r is not a valid dunno id.' % id) irc.error('%r is not a valid dunno id.' % id)
return return
cursor = self.db.cursor() try:
cursor.execute("""SELECT dunno FROM dunnos WHERE id=%s""", id) (dunno, by, at) = self.db.get(channel, id)
if cursor.rowcount == 0: irc.reply("Dunno #%s: %r" % (id, dunno))
irc.error('No dunno found with id #%s.' % id) except KeyError:
return irc.error('No dunno found with that id.')
dunno = cursor.fetchone()[0]
irc.reply("Dunno #%s: %r." % (id, dunno))
def change(self, irc, msg, args): def change(self, irc, msg, args):
"""<id> <regexp> """[<channel>] <id> <regexp>
Alters the dunno with the given id according to the provided regexp. Alters the dunno with the given id according to the provided regexp.
<channel> is only necessary if the message isn't sent in the channel
itself.
""" """
id, regexp = privmsgs.getArgs(args, required=2) channel = privmsgs.getChannel(msg, args)
# Must be registered to use this (id, regexp) = privmsgs.getArgs(args, required=2)
try: try:
user_id = ircdb.users.getUserId(msg.prefix) user_id = ircdb.users.getUserId(msg.prefix)
except KeyError: except KeyError:
@ -206,9 +287,9 @@ class Dunno(callbacks.Privmsg):
except ValueError: except ValueError:
irc.error('%r is not a valid dunno id.' % id) irc.error('%r is not a valid dunno id.' % id)
return return
cursor = self.db.cursor() try:
cursor.execute("""SELECT dunno FROM dunnos WHERE id=%s""", id) _ = self.db.get(channel, id)
if cursor.rowcount == 0: except KeyError:
irc.error('There is no dunno #%s.' % id) irc.error('There is no dunno #%s.' % id)
return return
try: try:
@ -216,18 +297,17 @@ class Dunno(callbacks.Privmsg):
except: except:
irc.error('%r is not a valid regular expression.' % regexp) irc.error('%r is not a valid regular expression.' % regexp)
return return
dunno = cursor.fetchone()[0] self.db.change(channel, id, replacer)
new_dunno = replacer(dunno)
cursor.execute("""UPDATE dunnos SET dunno=%s WHERE id=%s""",
new_dunno, id)
self.db.commit()
irc.replySuccess() irc.replySuccess()
def stats(self, irc, msg, args): def stats(self, irc, msg, args):
"""Returns the number of dunnos in the dunno database.""" """[<channel>]
cursor = self.db.cursor()
cursor.execute("""SELECT COUNT(*) FROM dunnos""") Returns the number of dunnos in the dunno database. <channel> is only
num = int(cursor.fetchone()[0]) 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.' % irc.reply('There %s %s in my database.' %
(utils.be(num), utils.nItems('dunno', num))) (utils.be(num), utils.nItems('dunno', num)))

View File

@ -42,6 +42,7 @@ import math
import sets import sets
import time import time
import random import random
import os.path
import urllib2 import urllib2
import UserDict import UserDict
import threading import threading
@ -109,6 +110,12 @@ class DBHandler(object):
self.cachedDb.die() self.cachedDb.die()
del self.cachedDb 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 # 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 # 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): def makeFilename(self, channel):
"""Override this to specialize the filenames of your databases.""" """Override this to specialize the filenames of your databases."""
channel = ircutils.toLower(channel) 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) return os.path.join(conf.supybot.directories.data(), prefix)
def makeDb(self, filename): def makeDb(self, filename):
@ -222,7 +230,7 @@ class ChannelUserDB(ChannelUserDictionary):
log.debug('Exception: %s', utils.exnToString(e)) log.debug('Exception: %s', utils.exnToString(e))
def flush(self): def flush(self):
fd = utils.AtomicFile(self.filename) fd = utils.transactionalFile(self.filename)
writer = csv.writer(fd) writer = csv.writer(fd)
items = self.items() items = self.items()
if not items: if not items:
@ -375,7 +383,7 @@ class FlatfileDB(object):
def serialize(self, record): def serialize(self, record):
raise NotImplementedError raise NotImplementedError
def unserialize(self, s): def deserialize(self, s):
raise NotImplementedError raise NotImplementedError
def _canonicalId(self, id): def _canonicalId(self, id):
@ -409,8 +417,9 @@ class FlatfileDB(object):
fd = file(self.filename, 'r+') fd = file(self.filename, 'r+')
fd.seek(0, 2) # End. fd.seek(0, 2) # End.
fd.write(line) fd.write(line)
self._incrementCurrentId(fd) return self.currentId
finally: finally:
self._incrementCurrentId(fd)
fd.close() fd.close()
def getRecord(self, id): def getRecord(self, id):
@ -421,7 +430,7 @@ class FlatfileDB(object):
for line in fd: for line in fd:
(lineId, strRecord) = self._splitLine(line) (lineId, strRecord) = self._splitLine(line)
if lineId == strId: if lineId == strId:
return self.unserialize(strRecord) return self.deserialize(strRecord)
raise KeyError, id raise KeyError, id
finally: finally:
fd.close() fd.close()
@ -466,7 +475,7 @@ class FlatfileDB(object):
for line in fd: for line in fd:
(strId, strRecord) = self._splitLine(line) (strId, strRecord) = self._splitLine(line)
if not strId.startswith('-'): if not strId.startswith('-'):
yield (int(strId), self.unserialize(strRecord)) yield (int(strId), self.deserialize(strRecord))
fd.close() fd.close()
def vacuum(self): def vacuum(self):

View File

@ -177,9 +177,25 @@ def attrgetter(attr):
operator.itemgetter = itemgetter operator.itemgetter = itemgetter
operator.attrgetter = attrgetter 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: for name in exported:
__builtins__[name] = globals()[name] __builtins__[name] = globals()[name]
# vim:set shiftwidth=4 tabstop=8 expandtab textwidth=78: # vim:set shiftwidth=4 tabstop=8 expandtab textwidth=78: