Change Infobot's *Unaddressed* config variables to be unaddressed.* and add

unaddressed.replyExistingFactoid.
Began Infobot refactoring and changing the dbs so that they can be channel
specific.  This is likely still broken.
This commit is contained in:
James Vega 2004-12-29 06:18:43 +00:00
parent 7217b01cd2
commit c93edfc42e
2 changed files with 470 additions and 375 deletions

View File

@ -33,7 +33,7 @@ Infobot compatibility, for the parts that we don't support already.
import supybot import supybot
deprecated = True #deprecated = True
__revision__ = "$Id$" __revision__ = "$Id$"
__author__ = supybot.authors.jemfinch __author__ = supybot.authors.jemfinch
@ -50,6 +50,7 @@ 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.world as world import supybot.world as world
from supybot.commands import *
import supybot.ircmsgs as ircmsgs import supybot.ircmsgs as ircmsgs
import supybot.ircutils as ircutils import supybot.ircutils as ircutils
import supybot.privmsgs as privmsgs import supybot.privmsgs as privmsgs
@ -58,21 +59,27 @@ import supybot.callbacks as callbacks
conf.registerPlugin('Infobot') conf.registerPlugin('Infobot')
conf.registerGlobalValue(conf.supybot.plugins.Infobot, 'personality', conf.registerChannelValue(conf.supybot.plugins.Infobot, 'personality',
registry.Boolean(True, """Determines whether the bot will respond with registry.Boolean(True, """Determines whether the bot will respond with
personable (Infobot-like) responses rather than its standard messages.""")) personable (Infobot-like) responses rather than its standard messages."""))
conf.registerGlobalValue(conf.supybot.plugins.Infobot, 'boringDunno', conf.registerChannelValue(conf.supybot.plugins.Infobot, 'boringDunno',
registry.String('Dunno.', """Determines what boring dunno should be given registry.String('Dunno.', """Determines what boring dunno should be given
if supybot.plugins.Infobot.personality is False.""")) if supybot.plugins.Infobot.personality is False."""))
conf.registerGlobalValue(conf.supybot.plugins.Infobot,
'snarfUnaddressedDefinitions', registry.Boolean(True, """Determines whether conf.registerGroup(conf.supybot.plugins.Infobot, 'unaddressed')
the bot will snarf definitions given in the channel that weren't directly conf.registerChannelValue(conf.supybot.plugins.Infobot.unaddressed,
addressed to it. Of course, no confirmation will be given if the bot isn't 'snarfDefinitions', registry.Boolean(True, """Determines whether the bot
directly addressed.""")) will snarf definitions given in the channel that weren't directly
conf.registerGlobalValue(conf.supybot.plugins.Infobot, addressed to it. Of course, no confirmation will be given if the bot
'answerUnaddressedQuestions', registry.Boolean(True, """Determines whether isn't directly addressed."""))
the bot will answer questions that weren't directly addressed to it. Of conf.registerChannelValue(conf.supybot.plugins.Infobot.unaddressed,
course, if it doesn't have an answer, it will remain silent.""")) 'answerQuestions', registry.Boolean(True, """Determines whether the bot
will answer questions that weren't directly addressed to it. Of course,
if it doesn't have an answer, it will remain silent."""))
conf.registerChannelValue(conf.supybot.plugins.Infobot.unaddressed,
'replyExistingFactoid', registry.Boolean(False, """Determines whether the
bot will announce that a factoid already exists when it sees a definition
for a pre-existing factoid."""))
def configure(advanced): def configure(advanced):
# This will be called by setup.py to configure this module. Advanced is # This will be called by setup.py to configure this module. Advanced is
@ -128,94 +135,131 @@ initialAre = {'who': NORESPONSE,
class PickleInfobotDB(object): class PickleInfobotDB(object):
def __init__(self, filename): def __init__(self, filename):
self._changes = 0
self._responses = 0
self.filename = filename self.filename = filename
try: self.dbs = ircutils.IrcDict()
fd = file(self.filename) self.changes = ircutils.IrcDict()
except EnvironmentError: self.responses = ircutils.IrcDict()
self._is = utils.InsensitivePreservingDict()
self._are = utils.InsensitivePreservingDict()
for (k, v) in initialIs.iteritems():
self.setIs(k, v)
for (k, v) in initialAre.iteritems():
self.setAre(k, v)
self._changes = 0
else:
try:
(self._is, self._are) = pickle.load(fd)
except cPickle.UnpicklingError, e:
raise dbi.InvalidDBError, str(e)
def flush(self): def _getDb(self, channel):
fd = utils.transactionalFile(self.filename, 'wb') filename = plugins.makeChannelFilename(self.filename, channel)
pickle.dump((self._is, self._are), fd) if filename in self.dbs:
fd.close() pass
elif os.path.exists(filename):
fd = file(filename)
try:
(Is, Are) = pickle.load(fd)
self.dbs[filename] = (Is, Are)
self.changes[filename] = 0
self.responses[filename] = 0
except cPickle.UnpicklingError, e:
fd.close()
raise dbi.InvalidDBError, str(e)
fd.close()
else:
self.dbs[filename] = (utils.InsensitivePreservingDict(),
utils.InsensitivePreservingDict())
for (k, v) in initialIs.iteritems():
self.setIs(channel, k, v)
for (k, v) in initialAre.iteritems():
self.setAre(channel, k, v)
self.changes[filename] = 0
self.responses[filename] = 0
return (self.dbs[filename], filename)
def flush(self, db=None, filename=None):
if db is None and filename is None:
for (filename, db) in self.dbs.iteritems():
fd = utils.transactionalFile(filename, 'wb')
pickle.dump(db, fd)
fd.close()
else:
fd = utils.transactionalFile(filename, 'wb')
pickle.dump(db, fd)
fd.close()
self.dbs[filename] = db
def close(self): def close(self):
self.flush() self.flush()
def changeIs(self, factoid, replacer): def incChanges(self):
filename = dynamic.filename
print '*** self.changes: %s' % self.changes
self.changes[filename] += 1
def incResponses(self):
self.responses[dynamic.filename] += 1
def changeIs(self, channel, factoid, replacer):
((Is, Are), filename) = self._getDb(channel)
try: try:
old = self._is[factoid] old = Is[factoid]
except KeyError: except KeyError:
raise dbi.NoRecordError raise dbi.NoRecordError
if replacer is not None: if replacer is not None:
self._is[factoid] = replacer(old) Is[factoid] = replacer(old)
self.flush() self.flush((Is, Are), filename)
self._changes += 1 self.incChanges()
def getIs(self, factoid): def getIs(self, channel, factoid):
ret = self._is[factoid] ((Is, Are), filename) = self._getDb(channel)
self._responses += 1 ret = Is[factoid]
self.incResponses()
return ret return ret
def setIs(self, fact, oid): def setIs(self, channel, key, value):
self._is[fact] = oid ((Is, Are), filename) = self._getDb(channel)
self.flush() Is[key] = value
self._changes += 1 self.flush((Is, Are), filename)
self.incChanges()
def delIs(self, factoid): def delIs(self, channel, factoid):
((Is, Are), filename) = self._getDb(channel)
try: try:
del self._is[factoid] Is.pop(factoid)
except KeyError: except KeyError:
raise dbi.NoRecordError raise dbi.NoRecordError
self.flush() self.flush((Is, Are), filename)
self._changes += 1 self.incChanges()
def hasIs(self, factoid): def hasIs(self, channel, factoid):
return factoid in self._is ((Is, Are), _) = self._getDb(channel)
return factoid in Is
def changeAre(self, factoid, replacer): def changeAre(self, channel, factoid, replacer):
((Is, Are), filename) = self._getDb(channel)
try: try:
old = self._are[factoid] old = Are[factoid]
except KeyError: except KeyError:
raise dbi.NoRecordError raise dbi.NoRecordError
if replacer is not None: if replacer is not None:
self._are[factoid] = replacer(old) Are[factoid] = replacer(old)
self._changes += 1 self.flush((Is, Are), filename)
self.flush() self.incChanges()
def getAre(self, factoid): def getAre(self, channel, factoid):
ret = self._are[factoid] ((Is, Are), filename) = self._getDb(channel)
self._responses += 1 ret = Are[factoid]
self.incResponses()
return ret return ret
def hasAre(self, factoid): def hasAre(self, channel, factoid):
return factoid in self._are ((Is, Are), _) = self._getDb(channel)
return factoid in Are
def setAre(self, fact, oid): def setAre(self, channel, key, value):
self._are[fact] = oid ((Is, Are), filename) = self._getDb(channel)
self.flush() Are[key] = value
self._changes += 1 self.flush((Is, Are), filename)
self.incChanges()
def delAre(self, factoid): def delAre(self, channel, factoid):
((Is, Are), filename) = self._getDb(channel)
try: try:
del self._are[factoid] Are.pop(factoid)
except KeyError: except KeyError:
raise dbi.NoRecordError raise dbi.NoRecordError
self.flush() self.flush((Is, Are), filename)
self._changes += 1 self.incChanges()
def getDunno(self): def getDunno(self):
return random.choice(dunnos) + random.choice(ends) return random.choice(dunnos) + random.choice(ends)
@ -223,33 +267,46 @@ class PickleInfobotDB(object):
def getConfirm(self): def getConfirm(self):
return random.choice(confirms) + random.choice(ends) return random.choice(confirms) + random.choice(ends)
def getChangeCount(self): def getChangeCount(self, channel):
return self._changes (_, filename) = self._getDb(channel)
return self.changes[filename]
def getResponseCount(self): def getResponseCount(self, channel):
return self._responses (_, filename) = self._getDb(channel)
return self.responses[filename]
def getNumFacts(self): def getNumFacts(self, channel):
return len(self._are.keys()) + len(self._is.keys()) ((Is, Are), _) = self._getDb(channel)
return len(Are.keys()) + len(Is.keys())
class SqliteInfobotDB(object): class SqliteInfobotDB(object):
def __init__(self, filename): def __init__(self, filename):
self.filename = filename
self.dbs = ircutils.IrcDict()
self.changes = ircutils.IrcDict()
self.responses = ircutils.IrcDict()
def _getDb(self, channel):
try: try:
import sqlite import sqlite
except ImportError: except ImportError:
raise callbacks.Error, 'You need to have PySQLite installed to '\ raise callbacks.Error, 'You need to have PySQLite installed to '\
'use this plugin. Download it at '\ 'use this plugin. Download it at '\
'<http://pysqlite.sf.net/>' '<http://pysqlite.sf.net/>'
self._changes = 0
self._responses = 0
self.filename = filename
try: try:
if os.path.exists(self.filename): filename = plugins.makeChannelFilename(self.filename, channel)
self.db = sqlite.connect(self.filename) if filename not in self.changes:
return self.db self.changes[filename] = 0
#else: if filename not in self.responses:
self.db = sqlite.connect(self.filename) self.responses[filename] = 0
cursor = self.db.cursor() if filename in self.dbs:
return (self.dbs[filename], filename)
if os.path.exists(filename):
self.dbs[filename] = sqlite.connect(filename)
return (self.dbs[filename], filename)
db = sqlite.connect(filename)
self.dbs[filename] = db
cursor = db.cursor()
cursor.execute("""CREATE TABLE isFacts ( cursor.execute("""CREATE TABLE isFacts (
key TEXT UNIQUE ON CONFLICT REPLACE, key TEXT UNIQUE ON CONFLICT REPLACE,
value TEXT value TEXT
@ -258,20 +315,31 @@ class SqliteInfobotDB(object):
key TEXT UNIQUE ON CONFLICT REPLACE, key TEXT UNIQUE ON CONFLICT REPLACE,
value TEXT value TEXT
);""") );""")
self.db.commit() db.commit()
for (k, v) in initialIs.iteritems(): for (k, v) in initialIs.iteritems():
self.setIs(k, v) self.setIs(channel, k, v)
for (k, v) in initialAre.iteritems(): for (k, v) in initialAre.iteritems():
self.setAre(k, v) self.setAre(channel, k, v)
self._changes = 0 self.changes[filename] = 0
self.responses[filename] = 0
return (db, filename)
except sqlite.DatabaseError, e: except sqlite.DatabaseError, e:
raise dbi.InvalidDBError, str(e) raise dbi.InvalidDBError, str(e)
def close(self): def close(self):
self.db.close() for db in self.dbs.itervalues():
db.close()
self.dbs.clear()
def changeIs(self, factoid, replacer): def incChanges(self):
cursor = self.db.cursor() self.changes[dynamic.filename] += 1
def incResponses(self):
self.changes[dynamic.filename] += 1
def changeIs(self, channel, factoid, replacer):
(db, filename) = self._getDb(channel)
cursor = db.cursor()
cursor.execute("""SELECT value FROM isFacts WHERE key=%s""", factoid) cursor.execute("""SELECT value FROM isFacts WHERE key=%s""", factoid)
if cursor.rowcount == 0: if cursor.rowcount == 0:
raise dbi.NoRecordError raise dbi.NoRecordError
@ -279,37 +347,42 @@ class SqliteInfobotDB(object):
if replacer is not None: if replacer is not None:
cursor.execute("""UPDATE isFacts SET value=%s WHERE key=%s""", cursor.execute("""UPDATE isFacts SET value=%s WHERE key=%s""",
replacer(old), factoid) replacer(old), factoid)
self.db.commit() db.commit()
self._changes += 1 self.incChanges()
def getIs(self, factoid): def getIs(self, channel, factoid):
cursor = self.db.cursor() (db, filename) = self._getDb(channel)
cursor = db.cursor()
cursor.execute("""SELECT value FROM isFacts WHERE key=%s""", factoid) cursor.execute("""SELECT value FROM isFacts WHERE key=%s""", factoid)
ret = cursor.fetchone()[0] ret = cursor.fetchone()[0]
self._responses += 1 self.incResponses()
return ret return ret
def setIs(self, fact, oid): def setIs(self, channel, fact, oid):
cursor = self.db.cursor() (db, filename) = self._getDb(channel)
cursor = db.cursor()
cursor.execute("""INSERT INTO isFacts VALUES (%s, %s)""", fact, oid) cursor.execute("""INSERT INTO isFacts VALUES (%s, %s)""", fact, oid)
self.db.commit() db.commit()
self._changes += 1 self.incChanges()
def delIs(self, factoid): def delIs(self, channel, factoid):
cursor = self.db.cursor() (db, filename) = self._getDb(channel)
cursor = db.cursor()
cursor.execute("""DELETE FROM isFacts WHERE key=%s""", factoid) cursor.execute("""DELETE FROM isFacts WHERE key=%s""", factoid)
if cursor.rowcount == 0: if cursor.rowcount == 0:
raise dbi.NoRecordError raise dbi.NoRecordError
self.db.commit() db.commit()
self._changes += 1 self.incChanges()
def hasIs(self, factoid): def hasIs(self, channel, factoid):
cursor = self.db.cursor() (db, _) = self._getDb(channel)
cursor = db.cursor()
cursor.execute("""SELECT * FROM isFacts WHERE key=%s""", factoid) cursor.execute("""SELECT * FROM isFacts WHERE key=%s""", factoid)
return cursor.rowcount == 1 return cursor.rowcount == 1
def changeAre(self, factoid, replacer): def changeAre(self, channel, factoid, replacer):
cursor = self.db.cursor() (db, filename) = self._getDb(channel)
cursor = db.cursor()
cursor.execute("""SELECT value FROM areFacts WHERE key=%s""", factoid) cursor.execute("""SELECT value FROM areFacts WHERE key=%s""", factoid)
if cursor.rowcount == 0: if cursor.rowcount == 0:
raise dbi.NoRecordError raise dbi.NoRecordError
@ -317,32 +390,36 @@ class SqliteInfobotDB(object):
if replacer is not None: if replacer is not None:
cursor.execute("""UPDATE areFacts SET value=%s WHERE key=%s""", cursor.execute("""UPDATE areFacts SET value=%s WHERE key=%s""",
replacer(old), factoid) replacer(old), factoid)
self.db.commit() db.commit()
self._changes += 1 self.incChanges()
def getAre(self, factoid): def getAre(self, channel, factoid):
cursor = self.db.cursor() (db, filename) = self._getDb(channel)
cursor = db.cursor()
cursor.execute("""SELECT value FROM areFacts WHERE key=%s""", factoid) cursor.execute("""SELECT value FROM areFacts WHERE key=%s""", factoid)
ret = cursor.fetchone()[0] ret = cursor.fetchone()[0]
self._responses += 1 self.incResponses()
return ret return ret
def setAre(self, fact, oid): def setAre(self, channel, fact, oid):
cursor = self.db.cursor() (db, filename) = self._getDb(channel)
cursor = db.cursor()
cursor.execute("""INSERT INTO areFacts VALUES (%s, %s)""", fact, oid) cursor.execute("""INSERT INTO areFacts VALUES (%s, %s)""", fact, oid)
self.db.commit() db.commit()
self._changes += 1 self.incChanges()
def delAre(self, factoid): def delAre(self, channel, factoid):
cursor = self.db.cursor() (db, filename) = self._getDb(channel)
cursor = db.cursor()
cursor.execute("""DELETE FROM areFacts WHERE key=%s""", factoid) cursor.execute("""DELETE FROM areFacts WHERE key=%s""", factoid)
if cursor.rowcount == 0: if cursor.rowcount == 0:
raise dbi.NoRecordError raise dbi.NoRecordError
self.db.commit() db.commit()
self._changes += 1 self.incChanges()
def hasAre(self, factoid): def hasAre(self, channel, factoid):
cursor = self.db.cursor() (db, _) = self._getDb(channel)
cursor = db.cursor()
cursor.execute("""SELECT * FROM areFacts WHERE key=%s""", factoid) cursor.execute("""SELECT * FROM areFacts WHERE key=%s""", factoid)
return cursor.rowcount == 1 return cursor.rowcount == 1
@ -352,14 +429,23 @@ class SqliteInfobotDB(object):
def getConfirm(self): def getConfirm(self):
return random.choice(confirms) + random.choice(ends) return random.choice(confirms) + random.choice(ends)
def getChangeCount(self): def getChangeCount(self, channel):
return self._changes (_, filename) = self._getDb(channel)
try:
return self.changes[filename]
except KeyError:
return 0
def getResponseCount(self): def getResponseCount(self, channel):
return self._responses (_, filename) = self._getDb(channel)
try:
return self.responses[filename]
except KeyError:
return 0
def getNumFacts(self): def getNumFacts(self, channel):
cursor = self.db.cursor() (db, _) = self._getDb(channel)
cursor = db.cursor()
cursor.execute("""SELECT COUNT(*) FROM areFacts""") cursor.execute("""SELECT COUNT(*) FROM areFacts""")
areFacts = int(cursor.fetchone()[0]) areFacts = int(cursor.fetchone()[0])
cursor.execute("""SELECT COUNT(*) FROM isFacts""") cursor.execute("""SELECT COUNT(*) FROM isFacts""")
@ -375,34 +461,32 @@ class Dunno(Exception):
pass pass
class Infobot(callbacks.PrivmsgCommandAndRegexp): class Infobot(callbacks.PrivmsgCommandAndRegexp):
regexps = ['doForget', 'doChange', 'doFactoid', 'doUnknown'] regexps = ['doForce', 'doForget', 'doFactoid', 'doUnknown']
addressedRegexps = ['doForce', 'doForget', 'doChange', 'doFactoid', 'doUnknown']
def __init__(self): def __init__(self):
self.__parent = super(Infobot, self) self.__parent = super(Infobot, self)
self.__parent.__init__() self.__parent.__init__()
try: self.db = InfobotDB()
self.db = InfobotDB()
except Exception:
self.log.exception('Error loading %s:', self.filename)
raise # So it doesn't get loaded without its database.
self.irc = None self.irc = None
self.msg = None self.msg = None
self.force = False
self.replied = True self.replied = True
self.badForce = False self.changed = False
self.addressed = False self.added = False
self.calledDoPrivmsg = False
def die(self): def die(self):
self.__parent.die() self.__parent.die()
self.db.close() self.db.close()
def reset(self):
self.db.close()
def _error(self, s): def _error(self, s):
if self.addressed: if msg.addressed:
self.irc.error(s) self.irc.error(s)
else: else:
self.log.warning(s) self.log.warning(s)
def reply(self, s, irc=None, msg=None, action=False): def reply(self, s, irc=None, msg=None, action=False, substitute=True):
if self.replied: if self.replied:
self.log.debug('Already replied, not replying again.') self.log.debug('Already replied, not replying again.')
return return
@ -413,8 +497,9 @@ class Infobot(callbacks.PrivmsgCommandAndRegexp):
assert self.msg is not None assert self.msg is not None
msg = self.msg msg = self.msg
self.replied = True self.replied = True
irc.reply(ircutils.standardSubstitute(irc, msg, s), if substitute:
prefixName=False, action=action, msg=msg) s = ircutils.standardSubstitute(irc, msg, s)
irc.reply(s, prefixName=False, action=action, msg=msg)
def confirm(self, irc=None, msg=None): def confirm(self, irc=None, msg=None):
if self.registryValue('personality'): if self.registryValue('personality'):
@ -429,26 +514,40 @@ class Infobot(callbacks.PrivmsgCommandAndRegexp):
else: else:
self.reply(self.registryValue('boringDunno'), irc=irc, msg=msg) self.reply(self.registryValue('boringDunno'), irc=irc, msg=msg)
def factoid(self, key, irc=None, msg=None, dunno=True, prepend=''): def factoid(self, key, irc=None, msg=None, dunno=True, prepend='',
isAre=None):
if irc is None: if irc is None:
assert self.irc is not None assert self.irc is not None
irc = self.irc irc = self.irc
if msg is None: if msg is None:
assert self.msg is not None assert self.msg is not None
msg = self.msg msg = self.msg
isAre = None if isAre is not None:
isAre = isAre.lower()
channel = dynamic.channel
try: try:
if self.db.hasIs(key): if isAre is None:
isAre = 'is' if self.db.hasIs(channel, key):
value = self.db.getIs(key) isAre = 'is'
elif self.db.hasAre(key): value = self.db.getIs(channel, key)
isAre = 'are' elif self.db.hasAre(channel, key):
value = self.db.getAre(key) isAre = 'are'
value = self.db.getAre(channel, key)
elif isAre == 'is':
if not self.db.hasIs(channel, key):
isAre = None
else:
value = self.db.getIs(channel, key)
elif isAre == 'are':
if not self.db.hasAre(channel, key):
isAre = None
else:
value = self.db.getAre(channel, key)
except dbi.InvalidDBError, e: except dbi.InvalidDBError, e:
self._error('Unable to access db: %s' % e) self._error('Unable to access db: %s' % e)
return return
if isAre is None: if isAre is None:
if self.addressed: if msg.addressed:
if dunno: if dunno:
self.dunno(irc=irc, msg=msg) self.dunno(irc=irc, msg=msg)
else: else:
@ -481,72 +580,28 @@ class Infobot(callbacks.PrivmsgCommandAndRegexp):
return s return s
_forceRe = re.compile(r'^no[,: -]+', re.I) _forceRe = re.compile(r'^no[,: -]+', re.I)
_karmaRe = re.compile(r'^(?:\S+|\(.+\))(?:\+\+|--)(?:\s+)?$') _karmaRe = re.compile(r'(?:\+\+|--)(?:\s+)?$')
def doPrivmsg(self, irc, msg): def doPrivmsg(self, irc, msg):
if msg.repliedTo:
self.log.debug('Returning early from doPrivmsg: msg.repliedTo.')
return
if ircmsgs.isCtcp(msg):
self.log.debug('Returning early from doPrivmsg: isCtcp(msg).')
return
if self.calledDoPrivmsg:
self.log.debug('Returning early from doPrivmsg: calledDoPrivmsg.')
self.calledDoPrivmsg = False
return
try: try:
maybeAddressed = callbacks.addressed(irc.nick, msg, if msg.repliedTo:
whenAddressedByNick=True) self.replied = True
if maybeAddressed: if ircmsgs.isCtcp(msg):
self.addressed = True self.log.debug('Returning early from doPrivmsg: isCtcp(msg).')
payload = maybeAddressed
else:
payload = msg.args[1]
if self._karmaRe.search(payload):
self.log.debug('Not snarfing a karma adjustment.')
return return
payload = self.normalize(payload) if self._karmaRe.search(msg.args[1]):
maybeForced = self._forceRe.sub('', payload) self.log.debug('Returning early from doPrivmsg: karma.')
if maybeForced != payload:
self.force = True
# Infobot requires that forces have the form "no, botname, ..."
# We think that's stupid to require the bot name if the bot is
# being directly addressed. The following makes sure both
# "botname: no, botname, ..." and "botname: no, ..." work the
# same and non-addressed forms require the bots nick.
nick = irc.nick.lower()
if not self.addressed:
if not maybeForced.lower().startswith(nick):
self.badForce = True
self.force = False
if maybeForced.lower().startswith(nick):
maybeForced = maybeForced[len(nick):].lstrip(', ')
payload = maybeForced
# Let's make sure we dump out of Infobot if the privmsg is an
# actual command otherwise we could get multiple responses.
if self.addressed and '=~' not in payload:
payload += '?'
if payload.endswith(irc.nick):
self.addressed = True
payload = payload[:-len(irc.nick)]
payload = payload.strip(', ') # Strip punctuation before nick.
payload += '?' # So doUnknown gets called.
if not payload.strip():
self.log.debug('Bailing since we received an empty msg.')
return return
# For later dynamic scoping
channel = plugins.getChannel(msg.args[0])
payload = self.normalize(msg.args[1])
msg = ircmsgs.IrcMsg(args=(msg.args[0], payload), msg=msg) msg = ircmsgs.IrcMsg(args=(msg.args[0], payload), msg=msg)
self.__parent.doPrivmsg(irc, msg) self.__parent.doPrivmsg(irc, msg)
finally: finally:
self.force = False
self.replied = False self.replied = False
self.badForce = False self.changed = False
self.addressed = False self.added = False
def tokenizedCommand(self, irc, msg, tokens):
self.doPrivmsg(irc, msg)
self.calledDoPrivmsg = True
def callCommand(self, name, irc, msg, *L, **kwargs): def callCommand(self, name, irc, msg, *L, **kwargs):
#print '***', name, utils.stackTrace()
try: try:
self.irc = irc self.irc = irc
self.msg = msg self.msg = msg
@ -561,7 +616,7 @@ class Infobot(callbacks.PrivmsgCommandAndRegexp):
deleted = False deleted = False
for method in [self.db.delIs, self.db.delAre]: for method in [self.db.delIs, self.db.delAre]:
try: try:
method(fact) method(dynamic.channel, fact)
deleted = True deleted = True
except dbi.NoRecordError: except dbi.NoRecordError:
pass pass
@ -571,6 +626,41 @@ class Infobot(callbacks.PrivmsgCommandAndRegexp):
# XXX: Should this be genericified? # XXX: Should this be genericified?
self.reply('I\'ve never heard of %s, %s!' % (fact, msg.nick)) self.reply('I\'ve never heard of %s, %s!' % (fact, msg.nick))
def doForce(self, irc, msg, match):
r"^no,\s+(\w+,\s+)?(.+?)\s+(?<!\\)(was|is|am|were|are)\s+(.+?)[?!. ]*$"
(nick, key, isAre, value) = match.groups()
if not msg.addressed:
if nick is None:
self.log.debug('Not forcing because we weren\'t addressed and '
'payload wasn\'t of the form: no, irc.nick, ..')
return
nick = nick.rstrip(' \t,')
if not ircutils.nickEqual(nick, irc.nick):
self.log.debug('Not forcing because the regexp nick didn\'t '
'match our nick.')
return
else:
if nick is not None:
stripped = nick.rstrip(' \t,')
if not ircutils.nickEqual(stripped, irc.nick):
key = nick + key
isAre = isAre.lower()
if self.added:
return
if isAre in ('was', 'is', 'am'):
if self.db.hasIs(dynamic.channel, key):
self.log.debug('Forcing %s to %s.',
utils.quoted(key), utils.quoted(value))
self.added = True
self.db.setIs(dynamic.channel, key, value)
else:
if self.db.hasAre(dynamic.channel, key):
self.log.debug('Forcing %s to %s.',
utils.quoted(key), utils.quoted(value))
self.added = True
self.db.setAre(dynamic.channel, key, value)
self.confirm()
def doChange(self, irc, msg, match): def doChange(self, irc, msg, match):
r"^(.+)\s+=~\s+(.+)$" r"^(.+)\s+=~\s+(.+)$"
(fact, regexp) = match.groups() (fact, regexp) = match.groups()
@ -578,16 +668,17 @@ class Infobot(callbacks.PrivmsgCommandAndRegexp):
try: try:
r = utils.perlReToReplacer(regexp) r = utils.perlReToReplacer(regexp)
except ValueError, e: except ValueError, e:
if self.addressed: if msg.addressed:
irc.error('Invalid regexp: %s' % regexp) irc.errorInvalid('regexp', regexp)
return
else: else:
self.log.debug('Invalid regexp: %s' % regexp) self.log.debug('Invalid regexp: %s' % regexp)
return return
if self.changed:
return
for method in [self.db.changeIs, self.db.changeAre]: for method in [self.db.changeIs, self.db.changeAre]:
try: try:
method(fact, r) method(dynamic.channel, fact, r)
changed = True self.changed = True
except dbi.NoRecordError: except dbi.NoRecordError:
pass pass
if changed: if changed:
@ -597,10 +688,16 @@ class Infobot(callbacks.PrivmsgCommandAndRegexp):
self.reply('I\'ve never heard of %s, %s!' % (fact, msg.nick)) self.reply('I\'ve never heard of %s, %s!' % (fact, msg.nick))
def doUnknown(self, irc, msg, match): def doUnknown(self, irc, msg, match):
r"^(.+?)\s*\?[?!. ]*$" r"^(.+?)\s*(\?[?!. ]*)?$"
key = match.group(1) (key, question) = match.groups()
if self.addressed or self.registryValue('answerUnaddressedQuestions'): if not msg.addressed:
# Does the dunno'ing for us itself. if question is None:
self.log.debug('Not answering question since we weren\'t '
'addressed and there was no question mark.')
return
if self.registryValue('unaddressed.answerQuestions'):
self.factoid(key, prepend=random.choice(starts))
else:
self.factoid(key, prepend=random.choice(starts)) self.factoid(key, prepend=random.choice(starts))
def doFactoid(self, irc, msg, match): def doFactoid(self, irc, msg, match):
@ -609,75 +706,81 @@ class Infobot(callbacks.PrivmsgCommandAndRegexp):
key = key.replace('\\', '') key = key.replace('\\', '')
if key.lower() in ('where', 'what', 'who', 'wtf'): if key.lower() in ('where', 'what', 'who', 'wtf'):
# It's a question. # It's a question.
if self.addressed or \ if msg.addressed or \
self.registryValue('answerUnaddressedQuestions'): self.registryValue('unaddressed.answerQuestions'):
self.factoid(value, prepend=random.choice(starts)) self.factoid(value, isAre=isAre, prepend=random.choice(starts))
return return
if not self.addressed and \ if not msg.addressed and \
not self.registryValue('snarfUnaddressedDefinitions'): not self.registryValue('unaddressed.snarfDefinitions'):
return return
isAre = isAre.lower() isAre = isAre.lower()
if self.added:
return
if isAre in ('was', 'is', 'am'): if isAre in ('was', 'is', 'am'):
if self.db.hasIs(key): if self.db.hasIs(dynamic.channel, key):
if also: if also:
self.log.debug('Adding %s to %s.', self.log.debug('Adding %s to %s.',
utils.quoted(key), utils.quoted(value)) utils.quoted(key), utils.quoted(value))
value = '%s or %s' % (self.db.getIs(key), value) value = '%s or %s' % (self.db.getIs(dynamic.channel, key),
elif self.force: value)
self.log.debug('Forcing %s to %s.', elif not msg.addressed:
utils.quoted(key), utils.quoted(value)) value = self.db.getIs(dynamic.channel, key)
elif self.badForce: if initialIs.get(key) != value:
value = self.db.getIs(key) self.reply('... but %s is %s, %s ...' %
self.reply('... but %s is %s, %s ...' % (key, value, (key, value, msg.nick), substitute=False)
msg.nick)) return
return elif msg.addressed:
elif self.addressed: value = self.db.getIs(dynamic.channel, key)
value = self.db.getIs(key) if initialIs.get(key) != value:
self.reply('But %s is %s, %s.' % (key, value, msg.nick)) self.reply('But %s is %s, %s.' %
return (key, value, msg.nick), substitute=False)
return
else: else:
self.log.debug('Already have a %s key.', self.log.debug('Already have a %s key.',
utils.quoted(key)) utils.quoted(key))
return return
self.db.setIs(key, value) self.added = True
self.db.setIs(dynamic.channel, key, value)
else: else:
if self.db.hasAre(key): if self.db.hasAre(dynamic.channel, key):
if also: if also:
self.log.debug('Adding %s to %s.', self.log.debug('Adding %s to %s.',
utils.quoted(key), utils.quoted(value)) utils.quoted(key), utils.quoted(value))
value = '%s or %s' % (self.db.getAre(key), value) value = '%s or %s' % (self.db.getAre(dynamic.channel, key),
elif self.force: value)
self.log.debug('Forcing %s to %s.', elif not msg.addressed:
utils.quoted(key), utils.quoted(value)) value = self.db.getAre(dynamic.channel, key)
elif self.badForce: if initialAre.get(key) != value:
value = self.db.getAre(key) self.reply('... but %s are %s, %s ...' %
self.reply('... but %s are %s, %s ...' % (key, value, (key, value, msg.nick), substitute=False)
msg.nick)) return
return elif msg.addressed:
elif self.addressed: value = self.db.getAre(dynamic.channel, key)
value = self.db.getAre(key) if initialAre.get(key) != value:
self.reply('But %s are %s, %s.' % (key, value, msg.nick)) self.reply('But %s are %s, %s.' %
return (key, value, msg.nick), substitute=False)
return
else: else:
self.log.debug('Already have a %s key.', self.log.debug('Already have a %s key.',
utils.quoted(key)) utils.quoted(key))
return return
self.db.setAre(key, value) self.added = True
if self.addressed or self.force: self.db.setAre(dynamic.channel, key, value)
if msg.addressed:
self.confirm() self.confirm()
def stats(self, irc, msg, args): def stats(self, irc, msg, args, channel):
"""takes no arguments """takes no arguments
Returns the number of changes and requests made to the Infobot database Returns the number of changes and requests made to the Infobot database
since the plugin was loaded. since the plugin was loaded.
""" """
changes = self.db.getChangeCount() changes = self.db.getChangeCount(channel)
responses = self.db.getResponseCount() responses = self.db.getResponseCount(channel)
now = time.time() now = time.time()
diff = int(now - world.startedAt) diff = int(now - world.startedAt)
mode = {True: 'optional', False: 'require'} mode = {True: 'optional', False: 'require'}
answer = self.registryValue('answerUnaddressedQuestions') answer = self.registryValue('unaddressed.answerQuestions')
irc.reply('Since %s, there %s been %s and %s. I have been awake for %s' irc.reply('Since %s, there %s been %s and %s. I have been awake for %s'
' this session, and currently reference %s. Addressing is in' ' this session, and currently reference %s. Addressing is in'
' %s mode.' % (time.ctime(world.startedAt), ' %s mode.' % (time.ctime(world.startedAt),
@ -685,32 +788,30 @@ class Infobot(callbacks.PrivmsgCommandAndRegexp):
utils.nItems('modification', changes), utils.nItems('modification', changes),
utils.nItems('question', responses), utils.nItems('question', responses),
utils.timeElapsed(int(now - world.startedAt)), utils.timeElapsed(int(now - world.startedAt)),
utils.nItems('factoid',self.db.getNumFacts()), utils.nItems('factoid',
self.db.getNumFacts(channel)),
mode[answer])) mode[answer]))
stats = wrap(stats, ['channeldb'])
status=stats status=stats
def tell(self, irc, msg, args): def tell(self, irc, msg, args, channel, nick, _, factoid):
"""<nick> [about] <factoid> """<nick> [about] <factoid>
Tells <nick> about <factoid>. Tells <nick> about <factoid>.
""" """
if len(args) < 2:
raise callbacks.ArgumentError
if args[1] == 'about':
del args[1]
(nick, factoid) = privmsgs.getArgs(args, required=2)
try: try:
hostmask = irc.state.nickToHostmask(nick) hostmask = irc.state.nickToHostmask(nick)
except KeyError: except KeyError:
irc.error('I haven\'t seen %s, I\'ll let you ' irc.error('I haven\'t seen %s, I\'ll let you '
'do the telling.' % nick) 'do the telling.' % nick, Raise=True)
return
newmsg = ircmsgs.privmsg(irc.nick, factoid+'?', prefix=hostmask) newmsg = ircmsgs.privmsg(irc.nick, factoid+'?', prefix=hostmask)
try: try:
prepend = '%s wants you to know that ' % msg.nick prepend = '%s wants you to know that ' % msg.nick
self.factoid(factoid, msg=newmsg, prepend=prepend) self.factoid(factoid, msg=newmsg, prepend=prepend)
except Dunno: except Dunno:
self.dunno() self.dunno()
tell = wrap(tell, ['channeldb', 'something',
optional(('literal', 'about')), 'text'])
Class = Infobot Class = Infobot

View File

@ -29,122 +29,116 @@
from testsupport import * from testsupport import *
try: import supybot.plugins.Infobot
import sqlite confirms = supybot.plugins.Infobot.confirms
except ImportError: dunnos = supybot.plugins.Infobot.dunnos
sqlite = None ibot = conf.supybot.plugins.Infobot
if sqlite is not None: class InfobotTestCase(ChannelPluginTestCase):
import supybot.plugins.Infobot plugins = ('Infobot',)
confirms = supybot.plugins.Infobot.confirms _endRe = re.compile(r'!|, \S+\.|\.')
dunnos = supybot.plugins.Infobot.dunnos def testIsSnarf(self):
ibot = conf.supybot.plugins.Infobot learn = ibot.unaddressed.snarfDefinitions()
answer = ibot.unaddressed.answerQuestions()
try:
ibot.unaddressed.snarfDefinitions.setValue(True)
ibot.unaddressed.answerQuestions.setValue(True)
self.assertSnarfNoResponse('foo is at http://bar.com/', 2)
self.assertRegexp('infobot stats', '1 modification')
self.assertRegexp('infobot status', '1 modification')
self.assertSnarfRegexp('foo?', r'http://bar.com/')
self.assertSnarfNoResponse('foo is at http://baz.com/', 2)
self.assertSnarfNotRegexp('foo?', 'baz')
m = self.getMsg('bar is at http://foo.com/')
self.failUnless(self._endRe.sub('', m.args[1]) in confirms)
self.assertRegexp('bar?', r'bar.*is.*http://foo.com/')
finally:
ibot.unaddressed.snarfDefinitions.setValue(learn)
ibot.unaddressed.answerQuestions.setValue(answer)
class InfobotTestCase(ChannelPluginTestCase): def testAreSnarf(self):
plugins = ('Infobot', 'Karma') learn = ibot.unaddressed.snarfDefinitions()
_endRe = re.compile(r'!|, \S+\.|\.') answer = ibot.unaddressed.answerQuestions()
def testIsSnarf(self): try:
learn = ibot.snarfUnaddressedDefinitions() ibot.unaddressed.snarfDefinitions.setValue(True)
answer = ibot.answerUnaddressedQuestions() ibot.unaddressed.answerQuestions.setValue(True)
try: self.assertSnarfNoResponse('bars are dirty', 2)
ibot.snarfUnaddressedDefinitions.setValue(True) self.assertSnarfRegexp('bars?', 'bars.*are.*dirty')
ibot.answerUnaddressedQuestions.setValue(True) self.assertSnarfNoResponse('bars are not dirty', 2)
self.assertSnarfNoResponse('foo is at http://bar.com/', 2) self.assertSnarfNotRegexp('bars?', 'not')
self.assertRegexp('infobot stats', '1 modification') finally:
self.assertRegexp('infobot status', '1 modification') ibot.unaddressed.snarfDefinitions.setValue(learn)
self.assertSnarfRegexp('foo?', r'foo.*is.*http://bar.com/') ibot.unaddressed.answerQuestions.setValue(answer)
self.assertSnarfNoResponse('foo is at http://baz.com/', 2)
self.assertSnarfNotRegexp('foo?', 'baz')
m = self.getMsg('bar is at http://foo.com/')
self.failUnless(self._endRe.sub('', m.args[1]) in confirms)
self.assertRegexp('bar?', r'bar.*is.*http://foo.com/')
finally:
ibot.snarfUnaddressedDefinitions.setValue(learn)
ibot.answerUnaddressedQuestions.setValue(answer)
def testAreSnarf(self): def testIsResponses(self):
learn = ibot.snarfUnaddressedDefinitions() learn = ibot.unaddressed.snarfDefinitions()
answer = ibot.answerUnaddressedQuestions() answer = ibot.unaddressed.answerQuestions()
try: try:
ibot.snarfUnaddressedDefinitions.setValue(True) ibot.unaddressed.snarfDefinitions.setValue(True)
ibot.answerUnaddressedQuestions.setValue(True) ibot.unaddressed.answerQuestions.setValue(True)
self.assertSnarfNoResponse('bars are dirty', 2) self.assertSnarfNoResponse('foo is bar', 2)
self.assertSnarfRegexp('bars?', 'bars.*are.*dirty') self.assertSnarfRegexp('foo?', 'foo.*is.*bar')
self.assertSnarfNoResponse('bars are not dirty', 2) self.assertSnarfNoResponse('when is foo?', 2)
self.assertSnarfNotRegexp('bars?', 'not') self.assertSnarfNoResponse('why is foo?', 2)
finally: self.assertSnarfNoResponse('why foo?', 2)
ibot.snarfUnaddressedDefinitions.setValue(learn) self.assertSnarfNoResponse('when is foo?', 2)
ibot.answerUnaddressedQuestions.setValue(answer) finally:
ibot.unaddressed.snarfDefinitions.setValue(learn)
ibot.unaddressed.answerQuestions.setValue(answer)
def testIsResponses(self): def testAnswerUnaddressed(self):
learn = ibot.snarfUnaddressedDefinitions() answer = ibot.unaddressed.answerQuestions()
answer = ibot.answerUnaddressedQuestions() try:
try: ibot.unaddressed.answerQuestions.setValue(True)
ibot.snarfUnaddressedDefinitions.setValue(True) self.assertSnarfNoResponse('foo is bar')
ibot.answerUnaddressedQuestions.setValue(True) self.assertSnarfRegexp('foo?', 'bar')
self.assertSnarfNoResponse('foo is bar', 2) ibot.unaddressed.answerQuestions.setValue(False)
self.assertSnarfRegexp('foo?', 'foo.*is.*bar') self.assertSnarfNoResponse('foo?', 2)
self.assertSnarfNoResponse('when is foo?', 2) finally:
self.assertSnarfNoResponse('why is foo?', 2) ibot.unaddressed.answerQuestions.setValue(answer)
self.assertSnarfNoResponse('why foo?', 2)
self.assertSnarfNoResponse('when is foo?', 2)
finally:
ibot.snarfUnaddressedDefinitions.setValue(learn)
ibot.answerUnaddressedQuestions.setValue(answer)
def testAnswerUnaddressed(self): def testReplaceFactoid(self):
answer = ibot.answerUnaddressedQuestions() answer = ibot.unaddressed.answerQuestions()
try: learn = ibot.unaddressed.snarfDefinitions()
ibot.answerUnaddressedQuestions.setValue(True) try:
self.assertSnarfNoResponse('foo is bar') ibot.unaddressed.answerQuestions.setValue(True)
self.assertSnarfRegexp('foo?', 'bar') ibot.unaddressed.snarfDefinitions.setValue(True)
ibot.answerUnaddressedQuestions.setValue(False) self.assertSnarfNoResponse('forums are good')
self.assertSnarfNoResponse('foo?', 2) self.assertSnarfRegexp('forums?', 'good')
finally: self.assertNotError('no, forums are evil')
ibot.answerUnaddressedQuestions.setValue(answer) self.assertSnarfRegexp('forums?', 'evil')
finally:
ibot.unaddressed.answerQuestions.setValue(answer)
ibot.unaddressed.snarfDefinitions.setValue(learn)
def testReplaceFactoid(self): def testDoubleIsAre(self):
answer = ibot.answerUnaddressedQuestions() answer = ibot.unaddressed.answerQuestions()
learn = ibot.snarfUnaddressedDefinitions() learn = ibot.unaddressed.snarfDefinitions()
try: try:
ibot.answerUnaddressedQuestions.setValue(True) ibot.unaddressed.answerQuestions.setValue(True)
ibot.snarfUnaddressedDefinitions.setValue(True) ibot.unaddressed.snarfDefinitions.setValue(True)
self.assertSnarfNoResponse('forums are good') self.assertSnarfNoResponse('foo is <reply> foo is bar')
self.assertSnarfRegexp('forums?', 'good') self.assertSnarfRegexp('foo?', 'foo is bar')
self.assertNotError('no, forums are evil') self.assertSnarfNoResponse('bars are <reply> bars are good')
self.assertSnarfRegexp('forums?', 'evil') self.assertSnarfRegexp('bars?', 'bars are good')
finally: self.assertSnarfNoResponse('bees are <reply> honey is good')
ibot.answerUnaddressedQuestions.setValue(answer) self.assertSnarfRegexp('bees?', 'honey is good')
ibot.snarfUnaddressedDefinitions.setValue(learn) self.assertSnarfNoResponse('food is <reply> tacos are good')
self.assertSnarfRegexp('food?', 'tacos are good')
finally:
ibot.unaddressed.answerQuestions.setValue(answer)
ibot.unaddressed.snarfDefinitions.setValue(learn)
def testDoubleIsAre(self): # def testNoKarmaDunno(self):
answer = ibot.answerUnaddressedQuestions() # self.assertNoResponse('foo++')
learn = ibot.snarfUnaddressedDefinitions()
try:
ibot.answerUnaddressedQuestions.setValue(True)
ibot.snarfUnaddressedDefinitions.setValue(True)
self.assertSnarfNoResponse('foo is <reply> foo is bar')
self.assertSnarfRegexp('foo?', 'foo is bar')
self.assertSnarfNoResponse('bars are <reply> bars are good')
self.assertSnarfRegexp('bars?', 'bars are good')
self.assertSnarfNoResponse('bees are <reply> honey is good')
self.assertSnarfRegexp('bees?', 'honey is good')
self.assertSnarfNoResponse('food is <reply> tacos are good')
self.assertSnarfRegexp('food?', 'tacos are good')
finally:
ibot.answerUnaddressedQuestions.setValue(answer)
ibot.snarfUnaddressedDefinitions.setValue(learn)
def testNoKarmaDunno(self): def testPredefinedFactoids(self):
self.assertNoResponse('foo++') self.assertSnarfNoResponse('what?', 3)
self.assertRegexp('roses?', 'roses are red')
def testPredefinedFactoids(self): def testAddressedQuestions(self):
self.assertSnarfNoResponse('what?', 3) self.assertNotError('hi is <reply>Hello, $who.')
self.assertRegexp('roses?', 'roses are red') self.assertSnarfNoResponse('hi')
self.assertRegexp('hi', 'Hello')
def testAddressedQuestions(self):
self.assertNotError('hi is <reply>Hello, $who.')
self.assertSnarfRegexp('hi', 'Hello')
self.assertRegexp('hi', 'Hello')
# vim:set shiftwidth=4 tabstop=8 expandtab textwidth=78: # vim:set shiftwidth=4 tabstop=8 expandtab textwidth=78: