Limnoria/plugins/FunDB.py
2004-08-06 12:41:07 +00:00

467 lines
16 KiB
Python
Executable File

#!/usr/bin/env python
###
# Copyright (c) 2002, Jeremiah Fincher
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# * Redistributions of source code must retain the above copyright notice,
# this list of conditions, and the following disclaimer.
# * Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions, and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
# * Neither the name of the author of this software nor the name of
# contributors to this software may be used to endorse or promote products
# derived from this software without specific prior written consent.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
###
"""
Provides fun commands that require a database to operate.
"""
__revision__ = "$Id$"
import supybot.plugins as plugins
import re
import csv
import sets
import random
import itertools
import supybot.conf as conf
import supybot.ircdb as ircdb
import supybot.utils as utils
import supybot.world as world
import supybot.ircmsgs as ircmsgs
import supybot.ircutils as ircutils
import supybot.privmsgs as privmsgs
import supybot.registry as registry
import supybot.callbacks as callbacks
class FunDBDBInterface(object):
def close(self):
pass
def flush(self):
pass
def get(self, channel, type, id):
"""Returns just the text associated with the channel, type, and id."""
raise NotImplementedError
def info(self, channel, type, id):
"""Returns the test and the metadata associated with the
channel, type, and id."""
raise NotImplementedError
def add(self, channel, type, text, by):
raise NotImplementedError
def remove(self, channel, type, id):
raise NotImplementedError
def change(self, channel, type, id, f):
raise NotImplementedError
def random(self, channel, type):
raise NotImplementedError
def size(self, channel, type):
raise NotImplementedError
def search(self, channel, type, p):
"""Returns a list of (id, text) pairs whose text matches predicate p"""
raise NotImplementedError
class FlatfileFunDBDB(FunDBDBInterface):
class FunDBDB(plugins.FlatfileDB):
def serialize(self, v):
return csv.join(map(str, v))
def deserialize(self, s):
return csv.split(s)
def __init__(self):
self.dbs = ircutils.IrcDict()
self.filenames = sets.Set()
def close(self):
for filename in self.filenames:
try:
db = self.FunDBDB(filename)
db.close()
except EnvironmentError:
pass
def _getDb(self, channel, type):
type = type.lower()
if channel not in self.dbs:
self.dbs[channel] = {}
if type not in self.dbs[channel]:
filename = type.capitalize() + '.db'
filename = plugins.makeChannelFilename(channel, filename)
self.filenames.add(filename)
self.dbs[channel][type] = self.FunDBDB(filename)
return self.dbs[channel][type]
def get(self, channel, type, id):
return self.info(channel, type, id)[1]
def info(self, channel, type, id):
db = self._getDb(channel, type)
return db.getRecord(id)
def add(self, channel, type, text, by):
db = self._getDb(channel, type)
return db.addRecord([by, text])
def remove(self, channel, type, id):
db = self._getDb(channel, type)
db.delRecord(id)
def change(self, channel, type, id, f):
db = self._getDb(channel, type)
(by, text) = db.getRecord(id)
db.setRecord(id, [by, f(text)])
def random(self, channel, type):
db = self._getDb(channel, type)
t = random.choice(db.records())
if t is not None:
(id, (by, text)) = t
t = (id, text)
return t
def size(self, channel, type):
db = self._getDb(channel, type)
return itertools.ilen(db.records())
def search(self, channel, type, p):
db = self._getDb(channel, type)
L = []
for (id, record) in db.records():
text = record[1]
if p(text):
L.append((id, text))
return L
def FunDBDB():
return FlatfileFunDBDB()
conf.registerPlugin('FunDB')
conf.registerChannelValue(conf.supybot.plugins.FunDB, 'showIds',
registry.Boolean(True, """Determines whether the bot will show the id of an
insult/praise/lart."""))
class FunDB(callbacks.Privmsg):
"""
Contains the 'fun' commands that require a database. Currently includes
commands for larting, praising, and insulting.
"""
_types = ('insult', 'lart', 'praise')
def __init__(self):
callbacks.Privmsg.__init__(self)
self.db = FunDBDB()
def die(self):
self.db.close()
def _validType(self, irc, type, error=True):
if type not in self._types:
if error:
irc.error('%r is not a valid type. Valid types include %s.' %
(type, utils.commaAndify(self._types)))
return False
else:
return True
def _validId(self, irc, id, error=True):
try:
return int(id)
except ValueError:
if error:
irc.error('The <id> argument must be an integer.')
return None
def _isRegistered(self, irc, msg):
try:
return ircdb.users.getUserId(msg.prefix)
except KeyError:
irc.errorNotRegistered()
return None
def add(self, irc, msg, args):
"""[<channel>] <lart|insult|praise> <text>
Adds another record to the data referred to in the first argument. For
commands that will later respond with an ACTION (lart and praise), $who
should be in the message to show who should be larted or praised. I.e.
'fundb add lart slices $who in half with a free AOL cd' would make the
bot, when it used that lart against, say, jemfinch, to say '/me slices
jemfinch in half with a free AOL cd' <channel> is only necessary if
the message isn't sent in the channel itself.
"""
channel = privmsgs.getChannel(msg, args)
(type, s) = privmsgs.getArgs(args, required=2)
type = type.lower()
userId = self._isRegistered(irc, msg)
if not userId:
return
if not self._validType(irc, type):
return
if type == "lart" or type == "praise":
if '$who' not in s:
irc.error('There must be a $who in the lart/praise somewhere')
return
id = self.db.add(channel, type, s, userId)
irc.replySuccess('(%s #%s added)' % (type, id))
def remove(self, irc, msg, args):
"""[<channel>] <lart|insult|praise> <id>
Removes the data, referred to in the first argument, with the id
number <id> from the database. <channel> is only necessary if the
message isn't sent in the channel itself.
"""
channel = privmsgs.getChannel(msg, args)
(type, id) = privmsgs.getArgs(args, required=2)
if not self._isRegistered(irc, msg):
return
if not self._validType(irc, type):
return
id = self._validId(irc, id)
if id is None:
return
try:
self.db.remove(channel, type, id)
irc.replySuccess()
except KeyError:
irc.error('There is no %s with that id.' % type)
def change(self, irc, msg, args):
"""[<channel>] <lart|insult|praise> <id> <regexp>
Changes the data, referred to in the first argument, with the id
number <id> according to the regular expression <regexp>. <id> is the
zero-based index into the db; <regexp> is a regular expression of the
form s/regexp/replacement/flags. <channel> is only necessary if the
message isn't sent in the channel itself.
"""
channel = privmsgs.getChannel(msg, args)
(type, id, regexp) = privmsgs.getArgs(args, required=3)
if not self._validType(irc, type):
return
id = self._validId(irc, id)
if id is None:
return
if not self._isRegistered(irc, msg):
return
try:
replacer = utils.perlReToReplacer(regexp)
except ValueError, e:
irc.error('That regexp wasn\'t valid: %s.' % e.args[0])
return
except re.error, e:
irc.error(utils.exnToString(e))
return
try:
self.db.change(channel, type, id, replacer)
irc.replySuccess()
except KeyError:
irc.error('There is no %s with that id.' % type)
def stats(self, irc, msg, args):
"""[<channel>] <lart|insult|praise>
Returns the number of records, of the type specified, currently in
the database. <channel> is only necessary if the message isn't sent
in the channel itself.
"""
channel = privmsgs.getChannel(msg, args)
type = privmsgs.getArgs(args)
if not self._validType(irc, type):
return
total = self.db.size(channel, type)
irc.reply('There %s currently %s in my database.' %
(utils.be(total), utils.nItems(type, total)))
def get(self, irc, msg, args):
"""[<channel>] <lart|insult|praise> <id>
Gets the record with id <id> from the type specified. <channel> is
only necessary if the message isn't sent in the channel itself.
"""
channel = privmsgs.getChannel(msg, args)
(type, id) = privmsgs.getArgs(args, required=2)
if not self._validType(irc, type):
return
id = self._validId(irc, id)
if id is None:
return
try:
text = self.db.get(channel, type, id)
irc.reply(text)
except KeyError:
irc.error('There is no %s with that id.' % type)
def info(self, irc, msg, args):
"""[<channel>] <lart|insult|praise> <id>
Gets the info for the record with id <id> from the type specified.
<channel> is only necessary if the message isn't sent in the channel
itself.
"""
channel = privmsgs.getChannel(msg, args)
(type, id) = privmsgs.getArgs(args, required=2)
if not self._validType(irc, type):
return
id = self._validId(irc, id)
if id is None:
return
try:
(text, by) = self.db.info(channel, type, id)
reply = '%s #%s: %r; Created by %s.' % (type, id, text, by)
irc.reply(reply)
except KeyError:
irc.error('There is no %s with that id.' % type)
def _formatResponse(self, s, id, channel):
if self.registryValue('showIds', channel):
return '%s (#%s)' % (s, id)
else:
return s
_meRe = re.compile(r'\bme\b', re.I)
_myRe = re.compile(r'\bmy\b', re.I)
def _replaceFirstPerson(self, s, nick):
s = self._meRe.sub(nick, s)
s = self._myRe.sub('%s\'s' % nick, s)
return s
def insult(self, irc, msg, args):
"""[<channel>] <nick>
Insults <nick>. <channel> is only necessary if the message isn't
sent in the channel itself.
"""
channel = privmsgs.getChannel(msg, args)
nick = privmsgs.getArgs(args)
if not nick:
raise callbacks.ArgumentError
t = self.db.random(channel, 'insult')
if t is None:
irc.error('There are currently no available insults.')
else:
(id, insult) = t
nick = self._replaceFirstPerson(nick, msg.nick)
insult = '%s: %s' % (nick, insult.replace('$who', nick))
irc.reply(self._formatResponse(insult, id, channel),
prefixName=False)
def lart(self, irc, msg, args):
"""[<channel>] [<id>] <text> [for <reason>]
Uses a lart on <text> (giving the reason, if offered). Will use lart
number <id> from the database when <id> is given. <channel> is only
necessary if the message isn't sent in the channel itself.
"""
channel = privmsgs.getChannel(msg, args)
(id, nick) = privmsgs.getArgs(args, optional=1)
id = self._validId(irc, id, error=False)
if id is None:
nick = privmsgs.getArgs(args)
nick = nick.rstrip('.')
if not nick:
raise callbacks.ArgumentError
if nick == irc.nick:
nick = msg.nick
try:
(nick, reason) = itertools.imap(' '.join,
utils.itersplit('for'.__eq__, nick.split(), 1))
except ValueError:
reason = ''
if id:
try:
lart = self.db.get(channel, 'lart', id)
t = (id, lart)
except KeyError:
irc.error('There is no such lart.')
return
else:
t = self.db.random(channel, 'lart')
if t is None:
irc.error('There are currently no available larts.')
else:
(id, lart) = t
nick = self._replaceFirstPerson(nick, msg.nick)
reason = self._replaceFirstPerson(reason, msg.nick)
s = lart.replace('$who', nick)
if reason:
s = '%s for %s' % (s, reason)
s = s.rstrip('.')
irc.reply(self._formatResponse(s, id, channel), action=True)
def praise(self, irc, msg, args):
"""[<channel>] [<id>] <text> [for <reason>]
Uses a praise on <text> (giving the reason, if offered). Will use
praise number <id> from the database when <id> is given.
<channel> is only necessary if the message isn't sent in the channel
itself.
"""
channel = privmsgs.getChannel(msg, args)
(id, nick) = privmsgs.getArgs(args, optional=1)
id = self._validId(irc, id, error=False)
if id is None:
nick = privmsgs.getArgs(args)
nick = nick.rstrip('.')
if not nick:
raise callbacks.ArgumentError
try:
(nick, reason) = itertools.imap(' '.join,
utils.itersplit('for'.__eq__, nick.split(), 1))
except ValueError:
reason = ''
if id:
try:
praise = self.db.get(channel, 'praise', id)
t = (id, praise)
except KeyError:
irc.error('There is no such praise.')
return
else:
t = self.db.random(channel, 'praise')
if t is None:
irc.error('There are currently no available praises.')
else:
(id, praise) = t
nick = self._replaceFirstPerson(nick, msg.nick)
reason = self._replaceFirstPerson(reason, msg.nick)
s = praise.replace('$who', nick)
if reason:
s = '%s for %s' % (s, reason)
s = s.rstrip('.')
irc.reply(self._formatResponse(s, id, channel), action=True)
Class = FunDB
# vim:set shiftwidth=4 tabstop=8 expandtab textwidth=78: