Revert back to non-wrap/abstracted form since it'll be replaced by a better

Factoids plugin.
This commit is contained in:
James Vega 2004-12-05 19:31:42 +00:00
parent 08f4e8c59f
commit d9e766b278

View File

@ -34,23 +34,31 @@ available on demand via several commands.
__revision__ = "$Id$" __revision__ = "$Id$"
import supybot.plugins as plugins
import time import time
import getopt import getopt
import string import string
import os.path import os.path
from itertools import imap from itertools import imap
import supybot.dbi as dbi
import supybot.conf as conf import supybot.conf as conf
import supybot.utils as utils import supybot.utils as utils
import supybot.ircdb as ircdb import supybot.ircdb as ircdb
from supybot.commands import *
import supybot.plugins as plugins
import supybot.ircutils as ircutils import supybot.ircutils as ircutils
import supybot.privmsgs as privmsgs import supybot.privmsgs as privmsgs
import supybot.registry as registry import supybot.registry as registry
import supybot.callbacks as callbacks import supybot.callbacks as callbacks
import supybot.Owner as Owner
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/>'
conf.registerPlugin('Factoids') conf.registerPlugin('Factoids')
conf.registerChannelValue(conf.supybot.plugins.Factoids, 'learnSeparator', conf.registerChannelValue(conf.supybot.plugins.Factoids, 'learnSeparator',
@ -71,36 +79,19 @@ conf.registerChannelValue(conf.supybot.plugins.Factoids,
'factoidPrefix', registry.StringWithSpaceOnRight('could be ', """Determines 'factoidPrefix', registry.StringWithSpaceOnRight('could be ', """Determines
the string that factoids will be introduced by.""")) the string that factoids will be introduced by."""))
class MultiKeyError(KeyError): class Factoids(plugins.ChannelDBHandler, callbacks.Privmsg):
pass def __init__(self):
callbacks.Privmsg.__init__(self)
plugins.ChannelDBHandler.__init__(self)
class LockError(Exception): def die(self):
pass callbacks.Privmsg.die(self)
plugins.ChannelDBHandler.die(self)
class SqliteFactoidsDB(object): def makeDb(self, filename):
def __init__(self, filename):
self.dbs = ircutils.IrcDict()
self.filename = filename
def close(self):
for db in self.dbs.itervalues():
db.close()
def _getDb(self, channel):
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/>'
filename = plugins.makeChannelFilename(self.filename, channel)
if filename in self.dbs:
return self.dbs[filename]
if os.path.exists(filename): if os.path.exists(filename):
self.dbs[filename] = sqlite.connect(filename) return sqlite.connect(filename)
return self.dbs[filename]
db = sqlite.connect(filename) db = sqlite.connect(filename)
self.dbs[filename] = db
cursor = db.cursor() cursor = db.cursor()
cursor.execute("""CREATE TABLE keys ( cursor.execute("""CREATE TABLE keys (
id INTEGER PRIMARY KEY, id INTEGER PRIMARY KEY,
@ -123,148 +114,7 @@ class SqliteFactoidsDB(object):
db.commit() db.commit()
return db return db
def add(self, channel, key, factoid, name): def learn(self, irc, msg, args):
db = self._getDb(channel)
cursor = db.cursor()
cursor.execute("SELECT id, locked FROM keys WHERE key LIKE %s", key)
if cursor.rowcount == 0:
cursor.execute("""INSERT INTO keys VALUES (NULL, %s, 0)""", key)
db.commit()
cursor.execute("SELECT id, locked FROM keys WHERE key LIKE %s",key)
(id, locked) = imap(int, cursor.fetchone())
if not locked:
cursor.execute("""INSERT INTO factoids VALUES
(NULL, %s, %s, %s, %s)""",
id, name, int(time.time()), factoid)
db.commit()
else:
raise LockError
def get(self, channel, key):
db = self._getDb(channel)
cursor = db.cursor()
cursor.execute("""SELECT factoids.fact FROM factoids, keys
WHERE keys.key LIKE %s AND factoids.key_id=keys.id
ORDER BY factoids.id
LIMIT 20""", key)
return [t[0] for t in cursor.fetchall()]
def lock(self, channel, key):
db = self._getDb(channel)
cursor = db.cursor()
cursor.execute("UPDATE keys SET locked=1 WHERE key LIKE %s", key)
db.commit()
def unlock(self, channel, key):
db = self._getDb(channel)
cursor = db.cursor()
cursor.execute("UPDATE keys SET locked=0 WHERE key LIKE %s", key)
db.commit()
def remove(self, channel, key, number):
db = self._getDb(channel)
cursor = db.cursor()
cursor.execute("""SELECT keys.id, factoids.id
FROM keys, factoids
WHERE key LIKE %s AND
factoids.key_id=keys.id""", key)
if cursor.rowcount == 0:
raise dbi.NoRecordError
elif cursor.rowcount == 1 or number is True:
(id, _) = cursor.fetchone()
cursor.execute("""DELETE FROM factoids WHERE key_id=%s""", id)
cursor.execute("""DELETE FROM keys WHERE key LIKE %s""", key)
db.commit()
else:
if number is not None:
results = cursor.fetchall()
try:
(_, id) = results[number]
except IndexError:
raise dbi.NoRecordError
cursor.execute("DELETE FROM factoids WHERE id=%s", id)
db.commit()
else:
raise MultiKeyError, cursor.rowcount
def random(self, channel):
db = self._getDb(channel)
cursor = db.cursor()
cursor.execute("""SELECT fact, key_id FROM factoids
ORDER BY random()
LIMIT 3""")
if cursor.rowcount == 0:
raise dbi.NoRecordError
L = []
for (factoid, id) in cursor.fetchall():
cursor.execute("""SELECT key FROM keys WHERE id=%s""", id)
(key,) = cursor.fetchone()
L.append((key, factoid))
return L
def info(self, channel, key):
db = self._getDb(channel)
cursor = db.cursor()
cursor.execute("SELECT id, locked FROM keys WHERE key LIKE %s", key)
if cursor.rowcount == 0:
raise dbi.NoRecordError
(id, locked) = imap(int, cursor.fetchone())
cursor.execute("""SELECT added_by, added_at FROM factoids
WHERE key_id=%s
ORDER BY id""", id)
return (locked, cursor.fetchall())
_sqlTrans = string.maketrans('*?', '%_')
def select(self, channel, values, predicates, globs):
db = self._getDb(channel)
cursor = db.cursor()
tables = ['keys']
criteria = []
predicateName = 'p'
formats = []
if not values:
target = 'keys.key'
else:
target = 'factoids.fact'
if 'factoids' not in tables:
tables.append('factoids')
criteria.append('factoids.key_id=keys.id')
for glob in globs:
criteria.append('TARGET LIKE %s')
formats.append(glob.translate(self._sqlTrans))
for r in predicates:
criteria.append('%s(TARGET)' % predicateName)
def p(s, r=r):
return int(bool(r(s)))
db.create_function(predicateName, 1, p)
predicateName += 'p'
sql = """SELECT keys.key FROM %s WHERE %s""" % \
(', '.join(tables), ' AND '.join(criteria))
sql = sql.replace('TARGET', target)
cursor.execute(sql, formats)
if cursor.rowcount == 0:
raise dbi.NoRecordError
elif cursor.rowcount == 1 and \
conf.supybot.plugins.Factoids.showFactoidIfOnlyOneMatch.get(channel)():
return cursor.fetchone()[0]
elif cursor.rowcount > 100:
return None
else:
return cursor.fetchall()
FactoidsDB = plugins.DB('Factoids', {'sqlite': SqliteFactoidsDB})
class Factoids(callbacks.Privmsg):
def __init__(self):
self.__parent = super(Factoids, self)
self.__parent.__init__()
self.db = FactoidsDB()
def die(self):
self.__parent.die()
self.db.close()
def learn(self, irc, msg, args, channel, text):
"""[<channel>] <key> as <value> """[<channel>] <key> as <value>
Associates <key> with <value>. <channel> is only necessary if the Associates <key> with <value>. <channel> is only necessary if the
@ -272,36 +122,58 @@ class Factoids(callbacks.Privmsg):
to separate the key from the value. It can be changed to another to separate the key from the value. It can be changed to another
word via the learnSeparator registry value. word via the learnSeparator registry value.
""" """
channel = privmsgs.getChannel(msg, args)
try: try:
separator = conf.supybot.plugins.Factoids. \ separator = conf.supybot.plugins.Factoids. \
learnSeparator.get(channel)() learnSeparator.get(channel)()
i = text.index(separator) i = args.index(separator)
except ValueError: except ValueError:
raise callbacks.ArgumentError raise callbacks.ArgumentError
text.pop(i) args.pop(i)
key = ' '.join(text[:i]) key = ' '.join(args[:i])
factoid = ' '.join(text[i:]) factoid = ' '.join(args[i:])
try: db = self.getDb(channel)
cursor = db.cursor()
cursor.execute("SELECT id, locked FROM keys WHERE key LIKE %s", key)
if cursor.rowcount == 0:
cursor.execute("""INSERT INTO keys VALUES (NULL, %s, 0)""", key)
db.commit()
cursor.execute("SELECT id, locked FROM keys WHERE key LIKE %s",key)
(id, locked) = imap(int, cursor.fetchone())
capability = ircdb.makeChannelCapability(channel, 'factoids')
if not locked:
if ircdb.users.hasUser(msg.prefix):
name = ircdb.users.getUser(msg.prefix).name name = ircdb.users.getUser(msg.prefix).name
except KeyError: else:
name = msg.nick name = msg.nick
try: cursor.execute("""INSERT INTO factoids VALUES
self.db.add(channel, key, factoid, name) (NULL, %s, %s, %s, %s)""",
id, name, int(time.time()), factoid)
db.commit()
irc.replySuccess() irc.replySuccess()
except LockError: else:
irc.error('That factoid is locked.') irc.error('That factoid is locked.')
learn = wrap(learn, ['channeldb', many('something')])
def _lookupFactoid(self, channel, key):
db = self.getDb(channel)
cursor = db.cursor()
cursor.execute("""SELECT factoids.fact FROM factoids, keys
WHERE keys.key LIKE %s AND factoids.key_id=keys.id
ORDER BY factoids.id
LIMIT 20""", key)
return [t[0] for t in cursor.fetchall()]
def _replyFactoids(self, irc, channel, key, factoids, number=0, error=True): def _replyFactoids(self, irc, channel, key, factoids, number=0, error=True):
if factoids: if factoids:
if number: if number:
try: try:
irc.reply(factoids[number]) irc.reply(factoids[number-1])
except IndexError: except IndexError:
irc.errorInvalid('number for that key') irc.error('That\'s not a valid number for that key.')
return
else: else:
intro = self.registryValue('factoidPrefix', channel) intro = self.registryValue('factoidPrefix', channel)
prefix = '%s %s' % (utils.quoted(key), intro) prefix = '%r %s' % (key, intro)
if len(factoids) == 1: if len(factoids) == 1:
irc.reply(prefix + factoids[0]) irc.reply(prefix + factoids[0])
else: else:
@ -320,46 +192,64 @@ class Factoids(callbacks.Privmsg):
channel = msg.args[0] channel = msg.args[0]
if self.registryValue('replyWhenInvalidCommand', channel): if self.registryValue('replyWhenInvalidCommand', channel):
key = ' '.join(tokens) key = ' '.join(tokens)
factoids = self.db.get(channel, key) factoids = self._lookupFactoid(channel, key)
self._replyFactoids(irc, channel, key, factoids, error=False) self._replyFactoids(irc, channel, key, factoids, error=False)
def whatis(self, irc, msg, args, channel, number, key): def whatis(self, irc, msg, args):
"""[<channel>] <key> [<number>] """[<channel>] <key> [<number>]
Looks up the value of <key> in the factoid database. If given a Looks up the value of <key> in the factoid database. If given a
number, will return only that exact factoid. <channel> is only number, will return only that exact factoid. <channel> is only
necessary if the message isn't sent in the channel itself. necessary if the message isn't sent in the channel itself.
""" """
factoids = self.db.get(channel, key) channel = privmsgs.getChannel(msg, args)
if len(args) > 1 and args[-1].isdigit():
number = args.pop()
else:
number = ''
key = privmsgs.getArgs(args)
if number:
try:
number = int(number)
except ValueError:
irc.error('%s is not a valid number.' % number)
return
else:
number = 0
factoids = self._lookupFactoid(channel, key)
self._replyFactoids(irc, channel, key, factoids, number) self._replyFactoids(irc, channel, key, factoids, number)
whatis = wrap(whatis, ['channeldb', reverse(optional('positiveInt', 0)),
'something'])
def lock(self, irc, msg, args, channel, key): def lock(self, irc, msg, args):
"""[<channel>] <key> """[<channel>] <key>
Locks the factoid(s) associated with <key> so that they cannot be Locks the factoid(s) associated with <key> so that they cannot be
removed or added to. <channel> is only necessary if the message isn't removed or added to. <channel> is only necessary if the message isn't
sent in the channel itself. sent in the channel itself.
""" """
self.db.lock(channel, key) channel = privmsgs.getChannel(msg, args)
key = privmsgs.getArgs(args)
db = self.getDb(channel)
cursor = db.cursor()
cursor.execute("UPDATE keys SET locked=1 WHERE key LIKE %s", key)
db.commit()
irc.replySuccess() irc.replySuccess()
lock = wrap(lock, ['channeldb', ('checkChannelCapability', 'op'),
'something'])
def unlock(self, irc, msg, args, channel, key): def unlock(self, irc, msg, args):
"""[<channel>] <key> """[<channel>] <key>
Unlocks the factoid(s) associated with <key> so that they can be Unlocks the factoid(s) associated with <key> so that they can be
removed or added to. <channel> is only necessary if the message isn't removed or added to. <channel> is only necessary if the message isn't
sent in the channel itself. sent in the channel itself.
""" """
self.db.unlock(channel, key) channel = privmsgs.getChannel(msg, args)
key = privmsgs.getArgs(args)
db = self.getDb(channel)
cursor = db.cursor()
cursor.execute("UPDATE keys SET locked=0 WHERE key LIKE %s", key)
db.commit()
irc.replySuccess() irc.replySuccess()
unlock = wrap(unlock, ['channeldb', ('checkChannelCapability', 'op'),
'something'])
def forget(self, irc, msg, args, channel, number, key): def forget(self, irc, msg, args):
"""[<channel>] <key> [<number>|*] """[<channel>] <key> [<number>|*]
Removes the factoid <key> from the factoids database. If there are Removes the factoid <key> from the factoids database. If there are
@ -368,49 +258,92 @@ class Factoids(callbacks.Privmsg):
factoids associated with a key. <channel> is only necessary if factoids associated with a key. <channel> is only necessary if
the message isn't sent in the channel itself. the message isn't sent in the channel itself.
""" """
if number == '*': channel = privmsgs.getChannel(msg, args)
if args[-1].isdigit():
number = int(args.pop())
number -= 1
if number < 0:
irc.error('Negative numbers aren\'t valid.')
return
elif args[-1] == '*':
del args[-1]
number = True number = True
else: else:
number -= 1 number = None
key = ' '.join(key) key = privmsgs.getArgs(args)
try: db = self.getDb(channel)
self.db.remove(channel, key, number) cursor = db.cursor()
irc.replySuccess() cursor.execute("""SELECT keys.id, factoids.id
except dbi.NoRecordError: FROM keys, factoids
WHERE key LIKE %s AND
factoids.key_id=keys.id""", key)
if cursor.rowcount == 0:
irc.error('There is no such factoid.') irc.error('There is no such factoid.')
except MultiKeyError, e: elif cursor.rowcount == 1 or number is True:
(id, _) = cursor.fetchone()
cursor.execute("""DELETE FROM factoids WHERE key_id=%s""", id)
cursor.execute("""DELETE FROM keys WHERE key LIKE %s""", key)
db.commit()
irc.replySuccess()
else:
if number is not None:
results = cursor.fetchall()
try:
(_, id) = results[number]
except IndexError:
irc.error('Invalid factoid number.')
return
cursor.execute("DELETE FROM factoids WHERE id=%s", id)
db.commit()
irc.replySuccess()
else:
irc.error('%s factoids have that key. ' irc.error('%s factoids have that key. '
'Please specify which one to remove, ' 'Please specify which one to remove, '
'or use * to designate all of them.' % str(e)) 'or use * to designate all of them.' %
forget = wrap(forget, ['channeldb', cursor.rowcount)
reverse(first('positiveInt', ('literal', '*'))),
'something'])
def random(self, irc, msg, args, channel): def random(self, irc, msg, args):
"""[<channel>] """[<channel>]
Returns a random factoid from the database for <channel>. <channel> Returns a random factoid from the database for <channel>. <channel>
is only necessary if the message isn't sent in the channel itself. is only necessary if the message isn't sent in the channel itself.
""" """
try: channel = privmsgs.getChannel(msg, args)
L = ['"%s": %s' % (ircutils.bold(k), v) db = self.getDb(channel)
for (k, v) in self.db.random(channel)] cursor = db.cursor()
cursor.execute("""SELECT fact, key_id FROM factoids
ORDER BY random()
LIMIT 3""")
if cursor.rowcount != 0:
L = []
for (factoid, id) in cursor.fetchall():
cursor.execute("""SELECT key FROM keys WHERE id=%s""", id)
(key,) = cursor.fetchone()
L.append('"%s": %s' % (ircutils.bold(key), factoid))
irc.reply('; '.join(L)) irc.reply('; '.join(L))
except dbi.NoRecordError: else:
irc.error('I couldn\'t find a factoid.') irc.error('I couldn\'t find a factoid.')
random = wrap(random, ['channeldb'])
def info(self, irc, msg, args, channel, key): def info(self, irc, msg, args):
"""[<channel>] <key> """[<channel>] <key>
Gives information about the factoid(s) associated with <key>. Gives information about the factoid(s) associated with <key>.
<channel> is only necessary if the message isn't sent in the channel <channel> is only necessary if the message isn't sent in the channel
itself. itself.
""" """
try: channel = privmsgs.getChannel(msg, args)
(locked, factoids) = self.db.info(channel, key) key = privmsgs.getArgs(args)
except dbi.NoRecordError: db = self.getDb(channel)
irc.error('No factoid matches that key.', Raise=True) cursor = db.cursor()
cursor.execute("SELECT id, locked FROM keys WHERE key LIKE %s", key)
if cursor.rowcount == 0:
irc.error('No factoid matches that key.')
return
(id, locked) = imap(int, cursor.fetchone())
cursor.execute("""SELECT added_by, added_at FROM factoids
WHERE key_id=%s
ORDER BY id""", id)
factoids = cursor.fetchall()
L = [] L = []
counter = 0 counter = 0
for (added_by, added_at) in factoids: for (added_by, added_at) in factoids:
@ -419,11 +352,10 @@ class Factoids(callbacks.Privmsg):
time.localtime(int(added_at))) time.localtime(int(added_at)))
L.append('#%s was added by %s at %s' % (counter,added_by,added_at)) L.append('#%s was added by %s at %s' % (counter,added_by,added_at))
factoids = '; '.join(L) factoids = '; '.join(L)
s = 'Key %s is %s and has %s associated with it: %s' % \ s = 'Key %r is %s and has %s associated with it: %s' % \
(utils.quoted(key), locked and 'locked' or 'not locked', (key, locked and 'locked' or 'not locked',
utils.nItems('factoid', counter), factoids) utils.nItems('factoid', counter), factoids)
irc.reply(s) irc.reply(s)
info = wrap(info, ['channeldb', 'something'])
def change(self, irc, msg, args): def change(self, irc, msg, args):
"""[<channel>] <key> <number> <regexp> """[<channel>] <key> <number> <regexp>
@ -452,7 +384,7 @@ class Factoids(callbacks.Privmsg):
WHERE keys.key LIKE %s AND WHERE keys.key LIKE %s AND
keys.id=factoids.key_id""", key) keys.id=factoids.key_id""", key)
if cursor.rowcount == 0: if cursor.rowcount == 0:
irc.error('I couldn\'t find any key %s' % utils.quoted(key)) irc.error('I couldn\'t find any key %r' % key)
return return
elif cursor.rowcount < number: elif cursor.rowcount < number:
irc.error('That\'s not a valid key id.') irc.error('That\'s not a valid key id.')
@ -463,43 +395,63 @@ class Factoids(callbacks.Privmsg):
db.commit() db.commit()
irc.replySuccess() irc.replySuccess()
def search(self, irc, msg, args, channel, optlist, globs): _sqlTrans = string.maketrans('*?', '%_')
def search(self, irc, msg, args):
"""[<channel>] [--values] [--{regexp}=<value>] [<glob>] """[<channel>] [--values] [--{regexp}=<value>] [<glob>]
Searches the keyspace for keys matching <glob>. If --regexp is given, Searches the keyspace for keys matching <glob>. If --regexp is given,
it associated value is taken as a regexp and matched against the keys. it associated value is taken as a regexp and matched against the keys.
If --values is given, search the value space instead of the keyspace. If --values is given, search the value space instead of the keyspace.
""" """
if not optlist and not globs: channel = privmsgs.getChannel(msg, args)
(optlist, rest) = getopt.getopt(args, '', ['values', 'regexp='])
if not optlist and not rest:
raise callbacks.ArgumentError raise callbacks.ArgumentError
values = False tables = ['keys']
predicates = [] formats = []
criteria = []
target = 'keys.key'
predicateName = 'p'
db = self.getDb(channel)
for (option, arg) in optlist: for (option, arg) in optlist:
if option == 'values': if option == '--values':
values = True target = 'factoids.fact'
elif option == 'regexp': if 'factoids' not in tables:
predicates.append(arg.search) tables.append('factoids')
L = [] criteria.append('factoids.key_id=keys.id')
for glob in globs: elif option == '--regexp':
criteria.append('%s(TARGET)' % predicateName)
try:
r = utils.perlReToPythonRe(arg)
except ValueError, e:
irc.error('Invalid regexp: %s' % e)
return
def p(s, r=r):
return int(bool(r.search(s)))
db.create_function(predicateName, 1, p)
predicateName += 'p'
for glob in rest:
if '*' not in glob and '?' not in glob: if '*' not in glob and '?' not in glob:
glob = '*%s*' % glob glob = '*%s*' % glob
L.append(glob) criteria.append('TARGET LIKE %s')
try: formats.append(glob.translate(self._sqlTrans))
factoids = self.db.select(channel, values, predicates, L) cursor = db.cursor()
if isinstance(factoids, basestring): sql = """SELECT keys.key FROM %s WHERE %s""" % \
self.whatis(irc, msg, [factoids]) (', '.join(tables), ' AND '.join(criteria))
elif factoids is None: sql = sql.replace('TARGET', target)
cursor.execute(sql, formats)
if cursor.rowcount == 0:
irc.reply('No keys matched that query.')
elif cursor.rowcount == 1 and \
conf.supybot.plugins.Factoids.showFactoidIfOnlyOneMatch.get(channel)():
self.whatis(irc, msg, [cursor.fetchone()[0]])
elif cursor.rowcount > 100:
irc.reply('More than 100 keys matched that query; ' irc.reply('More than 100 keys matched that query; '
'please narrow your query.') 'please narrow your query.')
else: else:
keys = [repr(t[0]) for t in factoids] keys = [repr(t[0]) for t in cursor.fetchall()]
s = utils.commaAndify(keys) s = utils.commaAndify(keys)
irc.reply(s) irc.reply(s)
except dbi.NoRecordError:
irc.reply('No keys matched that query.')
search = wrap(search, ['channeldb',
getopts({'values':'', 'regexp':'regexpMatcher'}),
any('glob')])
Class = Factoids Class = Factoids