#!/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): """ Make the bot forget about .""" 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): """ Explain what 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): """ Display some statistics on the infobot database.""" num = self.db.getNumberOfFactoids() if self.registryValue('infobotStyleStatus'): # FIXME: Original infobot style status message # 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 [ ...]' % 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: