Limnoria/sandbox/Infobot.py

457 lines
16 KiB
Python
Executable File

#!/usr/bin/env python
# -*- coding:utf-8 -*-
###
# Copyright (c) 2004, Stéphan Kochen
# 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.
###
"""
A plugin that tries to emulate Infobot somewhat faithfully.
"""
__revision__ = "$Id$"
import plugins
import re
import anydbm
import random
import os.path
import log
import conf
import ircmsgs
import ircutils
import privmsgs
import registry
import callbacks
conf.registerPlugin('Infobot')
conf.registerGlobalValue(conf.supybot.plugins.Infobot, 'infobotStyleStatus',
registry.Boolean(False, """Whether to reply to the status command with an
original Infobot style message or with a short message."""))
# FIXME: rename; description
conf.registerChannelValue(conf.supybot.plugins.Infobot,
'catchWhenNotAddressed', registry.Boolean(True, """Whether to catch
non-addressed stuff at all."""))
conf.registerChannelValue(conf.supybot.plugins.Infobot,
'replyQuestionWhenNotAddressed', registry.Boolean(True, """Whether to
answer to non-addressed stuff we know about."""))
conf.registerChannelValue(conf.supybot.plugins.Infobot,
'replyDontKnowWhenNotAddressed', registry.Boolean(False, """Whether to
answer to non-addressed stuff we don't know about."""))
conf.registerChannelValue(conf.supybot.plugins.Infobot,
'replyWhenNotAddressed', registry.Boolean(False, """Whether to answer to
non-addressed, non-question stuff."""))
# Replies
# XXX: Move this to a plaintext db of some sort?
# XXX: random.choice() doesn't like sets? :/
repliesHello = [
'Hello', 'Hi',
'Hey', 'Niihau',
'Bonjour', 'Hola',
'Salut', 'Que tal',
'Privet', 'What\'s up'
]
repliesWelcome = [
'No problem', 'My pleasure',
'Sure thing', 'No worries',
'De nada', 'De rien',
'Bitte', 'Pas de quoi'
]
repliesDontKnow = [
'I don\'t know.', 'Wish I knew',
'I don\'t have a clue.', 'No idea.',
'Bugger all, I dunno.'
]
repliesConfirm = [
'Gotcha', 'Ok',
'10-4', 'I hear ya',
'Got it'
]
repliesStatement = [
'%(key)s %(verb)s %(value)s',
'I think %(key)s %(verb)s %(value)s',
'Hmmm... %(key)s %(verb)s %(value)s',
'It has been said that %(key)s %(verb)s %(value)s',
'%(key)s %(verb)s probably %(value)s',
'Rumour has it %(key)s %(verb)s %(value)s',
'I heard %(key)s %(overb)s %(value)s',
'Somebody said %(key)s %(overb)s %(value)s',
'I guess ${key}s %(verb)s %(value)s',
'Well, %(key)s %(verb)s %(key)s',
'%(key)s is, like, %(value)s',
'I\'m pretty sure %(key)s is %(value)s'
]
# XXX: Most of these are a bunch of regexps taken from Infobot.
# Hopefully this is compatible with it's license (I think it is), but
# I'm not a lawyer, so someone should take a look at it:
# http://www.opensource.org/licenses/artistic-license.php
# We should atleast leave that link and Infobot's address here I think.
canonicalizeRe = [(re.compile(r[0], re.I), r[1]) for r in [
(r"(.*)", r" \1 "),
(r"\s\s+", r" "),
(r" si ", r" is "),
(r" teh ", r "the "),
# where blah is -> where is blah
# XXX: Why two?
(r" (where|what|who) (\S+) (is|are) ", r" \1 \3 \2 "),
# XXX: Non greedy (.*) ?
(r" (where|what|who) (.*) (is|are) ", r" \1 \3 \2 "),
# XXX: Does absolutely nothing?
#(r"^\s*(.*?)\s*", r"\1"),
(r" be tellin'?g? ", r" tell "),
(r" '?bout ", r" about "),
(r",? any(hoo?w?|ways?) ", r" "),
(r",?\s?(pretty )*please\?? $", r"\?"),
# Profanity filters; just delete it.
(r" th(e|at|is) (((m(o|u)th(a|er) ?)?fuck(in'?g?)?|hell|heck|(god-?)?damn?(ed)?) ?)+ ", r" "),
(r" wtf ", r" where "),
(r" this (.*) thingy? ", r" \1 "),
(r" this thingy?( called)? ", r" "),
(r" ha(s|ve) (an?y?|some|ne) (idea|clue|guess|seen) ", r" know "),
(r" does (any|ne|some) ?(1|one|body) know ", r" "),
(r" do you know ", r" "),
(r" can (you|u|((any|ne|some) ?(1|one|body)))( please)? tell (me|us|him|her) ", r" "),
(r" where (\S+) can \S+( (a|an|the))? ", r" "),
(r" (can|do) (i|you|one|we|he|she) (find|get)( this)? ", r" is "),
(r" (i|one|we|he|she) can (find|get) ", r" is "),
(r" (the )?(add?ress?|url) (for|to) ", r" "), # this should be more specific
(r" (where is )+", r" where is "),
# Switch person.
(" \x00[s'] ", " \x00's "), # fix genitives
(r" i'm ", " \x00 is "),
(r" i('?ve| have) ", " \x00 has "),
(r" i haven'?t ", " \x00 has not "),
(r" i ", " \x00 "),
(r" am ", " is "),
(r" (me|myself) ", " \x00 "),
(r" my ", " \x00's "), # turn 'my' into name's
(r" you'?re ", r" you are "),
(r" were ", r" are "),
(r" was ", r" is ")
]]
# Regexps which replace with the bot's nickname
replaceWithSelfRe = [(re.compile(r[0], re.I), r[1]) for r in [
(r" are you ", " is \x00 "),
(r" you are ", " \x00 is "),
(r" you ", " \x00 "),
(r" your ", " \x00's "),
(r" yourself ", " \x00 ")
]]
# Check if we're addressed like: "No, supybot, bla is..."
# XXX: We assume the nickname is followed by punctuation here.
addressedRe = re.compile('^(no)[, ]+\x00\\s*([-:,]+)', re.I)
# XXX: Kinda messy
def canonicalSentence(s, myNick, hisNick, addressed):
# Because we can't alter the pattern in compiled regexps to match a
# nickname, we substitute it with \x00 here, just search for that
# instead and at the end reverse it back to normal nicks.
nickReplaceRe = re.compile(re.escape(hisNick), re.I)
s = nickReplaceRe.sub('\x00', s)
for (r, subst) in canonicalizeRe:
s = r.sub(subst, s)
s = s.replace('\x00', hisNick)
nickReplaceRe = re.compile(re.escape(myNick), re.I)
s = nickReplaceRe.sub('\x00', s)
(s, n) = addressedRe.subn(r'\1\2', s)
if n:
addressed = True
if addressed:
for (r, subst) in replaceWithSelfRe:
s = r.sub(subst, s)
s = s.replace('\x00', myNick)
return (s.strip(), addressed)
# FIXME: This should become a subclass of the database backend later on
# Factoids get stored in the db as db['is/are key'] = 'keyvalue'
# This is because the actual key in the database is lowercase so we can do
# lowercase searches, but get the actual string from the value.
class InfobotDatabase(object):
def __init__(self, filename=None):
if not filename:
filename = 'Infobot.db'
filename = os.path.join(conf.supybot.directories.data(), filename)
self._db = anydbm.open(filename, 'c')
def die(self):
self._db.close()
# Hardly any try-statements here. db[] raises our exceptions for us.
def getFactoid(self, verb, key):
factoid = {}
verb = verb.lower()
key = key.lower()
dbKey = '%s %s' % (verb, key)
factoid['key'] = self._db[dbKey][:len(key)]
factoid['verb'] = verb
if verb == 'are':
factoid['overb'] = 'were'
else:
factoid['overb'] = 'was'
factoid['value'] = self._db[dbKey][len(key):]
return factoid
def hasFactoid(self, verb, key):
verb = verb.lower()
dbKey = key.lower()
dbKey = '%s %s' % (verb, dbKey)
try:
self._db[dbKey]
return True
except KeyError:
return False
# FIXME: debugs, baleet
def insertFactoid(self, key, verb, value):
verb = verb.lower()
dbKey = key.lower()
dbKey = '%s %s' % (verb, dbKey)
try:
self._db[dbKey]
except KeyError:
log.info('ins: %s =%s= %s' % (key, verb, value))
self._db[dbKey] = '%s%s' % (key, value)
return
log.info('dup: %s !%s! %s' % (key, verb, value))
raise KeyError, key
def setFactoid(self, key, verb, value):
verb = verb.lower()
dbKey = key.lower()
dbKey = '%s %s' % (verb, dbKey)
self._db[dbKey] = '%s%s' % (key, value)
log.info('set: %s => %s' % (key, value))
def addFactoid(self, key, verb, value):
verb = verb.lower()
dbKey = key.lower()
dbKey = '%s %s' % (verb, dbKey)
# XXX: should we create new entries here too
# if the factoid doesn't exist already?
currentValue = self._db[dbKey]
newValue = '%s, or %s' % (currentValue, value)
self._db[dbKey] = newValue
log.info('add: %s +%s+ %s' % (key, verb, value))
def deleteFactoid(self, verb, key):
verb = verb.lower()
dbKey = key.lower()
dbKey = '%s %s' % (verb, dbKey)
del db[dbKey]
log.info('forgot: %s' % key)
def getNumberOfFactoids(self):
return len(self._db)
class Infobot(callbacks.Privmsg):
def __init__(self):
callbacks.Privmsg.__init__(self)
self.db = InfobotDatabase()
# Patterns for processing private messages.
statementRe = re.compile(r'^(no[-:, ]+)?(.+?)\s+(is|are|was|were)'
r'\s+(also\s+)?(.+)$', re.I)
questionRe = re.compile(r'^(?:what|where|when)\s+(is|are|was|were)'
r'\s+(.*)$', re.I)
# FIXME: Matches everything with a question mark stuck to the end. o_O
shortQuestionRe = re.compile(r'^(.+)$')
# XXX: If possible, extend this, because people tend to use
# periods inside sentences as well, not just at the end.
# (indicating a pause with triple dots for example)
splitRe = re.compile(r'\s*([^?!.]+)([?!.]*)\s*')
def doPrivmsg(self, irc, msg):
channel = privmsgs.getChannel(msg, None, raiseError=False)
message = callbacks.addressed(irc.nick, msg)
addressed = bool(message)
if not addressed:
message = msg.args[1]
message = ircutils.stripFormatting(message)
for m in self.splitRe.finditer(message):
(s, ending) = m.groups()
question = '?' in ending
(s, addressed) = canonicalSentence(s, irc.nick, msg.nick,
addressed)
log.debug('canonicalSentence(): %s' % s)
if not (addressed or self.registryValue('catchWhenNotAddressed')):
continue
# FIXME: This is a friggin mess.
# XXX: PrivmsgCommandAndRegexp should take care of this?
# (probably not)
match = self.questionRe.search(s)
proxy = callbacks.IrcObjectProxyRegexp(irc, msg)
if match:
self.question(proxy, match, addressed)
elif not question:
match = self.statementRe.search(s)
if match:
self.statement(proxy, match, addressed)
if not match and question:
match = self.shortQuestionRe.search(s)
if match:
self.shortQuestion(proxy, match, addressed)
def statement(self, irc, match, addressed):
(correction, key, verb, addition, value) = match.groups()
if self.db.hasFactoid(verb, key):
if correction:
self.db.setFactoid(key, verb, value)
elif addition:
self.db.addFactoid(key, verb, value)
elif addressed or self.registryValue('replyWhenNotAddressed'):
factoid = self.db.getFactoid(verb, key)
if factoid['value'].lower() == value.lower():
irc.reply('I already had it like that.')
else:
irc.reply('...but %s %s %s...' % (key, verb, value))
return
else:
self.db.insertFactoid(key, verb, value)
if addressed or self.registryValue('replyWhenNotAddressed'):
irc.reply(random.choice(repliesConfirm))
def question(self, irc, match, addressed):
(verb, key) = match.groups()
try:
factoid = self.db.getFactoid(verb, key)
except KeyError:
if addressed or
self.registryValue('replyDontKnowWhenNotAddressed'):
irc.reply(random.choice(repliesDontKnow))
return
if addressed or self.registryValue('replyQuestionWhenNotAddressed'):
irc.reply(random.choice(repliesStatement) % factoid)
def shortQuestion(self, irc, match, addressed):
# FIXME: clean up that regexp first.
return
key = match.group(1)
try:
factoid = self.db.getFactoid('is', key)
except KeyError:
try:
factoid = self.db.getFactoid('are', key)
except KeyError:
if addressed or
self.registryValue('replyDontKnowWhenNotAddressed'):
irc.reply(random.choice(repliesDontKnow))
return
if addressed or self.registryValue('replyQuestionWhenNotAddressed'):
irc.reply(random.choice(repliesStatement) % factoid)
def forget(self, irc, msg, args):
"""<factoid>
Make the bot forget about <factoid>."""
key = privmsgs.getArgs()
try:
self.db.deleteFactoid('is', key)
except KeyError:
try:
self.db.deleteFactoid('are', key)
except KeyError:
irc.reply('I didn\'t know about that in the first place.')
return
irc.reply('I forgot %s' % key)
def whatis(self, irc, msg, args):
"""<factoid>
Explain what <factoid> is."""
key = privmsgs.getArgs()
try:
factoid = self.db.getFactoid('is', key)
except KeyError:
try:
factoid = self.db.getFactoid('are', key)
except KeyError:
irc.reply('I don\'t know anything about %s' % key)
return
irc.reply(random.choice(repliesStatement) % factoid)
def stats(self, irc, msg, args):
"""<requires no arguments>
Display some statistics on the infobot database."""
num = self.db.getNumberOfFactoids()
if self.registryValue('infobotStyleStatus'):
# FIXME: Original infobot style status message
# <Inf0bot> Since Sat Apr 17 19:31:21 2004, there have been 0
# modifications and 0 questions. I have been awake for
# 59 seconds this session, and currently reference
# 20 factoids. Addressing is in optional mode.
pass
else:
irc.reply('I know about %s factoids.' % num)
Class = Infobot
if __name__ == '__main__':
import sys
if len(sys.argv) < 2 and sys.argv[1] not in ('is', 'are'):
print 'Usage: %s <is|are> <factpack> [<factpack> ...]' % sys.argv[0]
sys.exit(-1)
r = re.compile(r'\s+=>\s+')
db = InfobotDatabase()
for filename in sys.argv[2:]:
fd = file(filename)
for line in fd:
line = line.strip()
if not line or line[0] in ('*', '#'):
continue
else:
try:
(key, value) = r.split(line, 1)
db.setFactoid(key, sys.argv[1], value)
except Exception, e:
print 'Invalid line (%s): %r' %(utils.exnToString(e),line)
# vim:set shiftwidth=4 tabstop=8 expandtab textwidth=78: