Move MF to s-d

This commit is contained in:
James Vega 2005-04-18 16:13:53 +00:00
parent 116f598caa
commit 948571c17f
5 changed files with 1163 additions and 0 deletions

View File

@ -0,0 +1 @@
Insert a description of your plugin here, with any notes, etc. about using it.

View File

@ -0,0 +1,62 @@
###
# Copyright (c) 2003-2005, Daniel DiPaolo
# 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.
###
"""
Moobot factoid compatibility module. Moobot's factoids were originally
designed to emulate Blootbot's factoids, so in either case, you should find
this plugin comfortable.
"""
import supybot
import supybot.world as world
# Use this for the version of this plugin. You may wish to put a CVS keyword
# in here if you're keeping the plugin in CVS or some similar system.
__version__ = "0.1"
__author__ = supybot.authors.strike
# This is a dictionary mapping supybot.Author instances to lists of
# contributions.
__contributors__ = {}
import config
import plugin
reload(plugin) # In case we're being reloaded.
# Add more reloads here if you add third-party modules and want them to be
# reloaded when this plugin is reloaded. Don't forget to import them as well!
if world.testing:
import test
Class = plugin.Class
configure = config.configure
# vim:set shiftwidth=4 tabstop=8 expandtab textwidth=78:

View File

@ -0,0 +1,52 @@
###
# Copyright (c) 2003-2005, Daniel DiPaolo
# 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.
###
import supybot.conf as conf
import supybot.registry as registry
def configure(advanced):
# This will be called by supybot to configure this module. advanced is
# a bool that specifies whether the user identified himself as an advanced
# user or not. You should effect your configuration by manipulating the
# registry as appropriate.
from supybot.questions import expect, anything, something, yn
conf.registerPlugin('MoobotFactoids', True)
MoobotFactoids = conf.registerPlugin('MoobotFactoids')
conf.registerChannelValue(MoobotFactoids,
'showFactoidIfOnlyOneMatch', registry.Boolean(True, """Determines whether
or not the factoid value will be shown when a listkeys search returns only
one factoid key."""))
conf.registerChannelValue(MoobotFactoids,
'mostCount', registry.Integer(10, """Determines how many items are shown
when the 'most' command is called."""))
# vim:set shiftwidth=4 tabstop=8 expandtab textwidth=78

View File

@ -0,0 +1,705 @@
###
# Copyright (c) 2003-2005, Daniel DiPaolo
# 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.
###
import os
import time
import shlex
import string
from cStringIO import StringIO
import supybot.conf as conf
import supybot.ircdb as ircdb
import supybot.utils as utils
from supybot.commands import *
import supybot.plugins as plugins
import supybot.ircutils as ircutils
import supybot.callbacks as callbacks
allchars = string.maketrans('', '')
class OptionList(object):
validChars = allchars.translate(allchars, '|()')
def _insideParens(self, lexer):
ret = []
while True:
token = lexer.get_token()
if not token:
return '(%s' % ''.join(ret) #)
elif token == ')':
if len(ret) > 1:
if '|' in ret:
L = map(''.join,
utils.iter.split('|'.__eq__, ret,
yieldEmpty=True))
return utils.iter.choice(L)
else:
return ''.join(ret)
return [x for x in ret if x != '|']
elif len(ret) == 1:
return '(%s)' % ret[0]
else:
return '()'
elif token == '(':
ret.append(self._insideParens(lexer))
elif token == '|':
ret.append(token)
else:
ret.append(token)
def tokenize(self, s):
lexer = shlex.shlex(StringIO(s))
lexer.commenters = ''
lexer.quotes = ''
lexer.whitespace = ''
lexer.wordchars = self.validChars
ret = []
while True:
token = lexer.get_token()
if not token:
break
elif token == '(':
ret.append(self._insideParens(lexer))
else:
ret.append(token)
return ''.join(ret)
def pickOptions(s):
return OptionList().tokenize(s)
class SqliteMoobotDB(object):
def __init__(self, filename):
self.filename = filename
self.dbs = ircutils.IrcDict()
def close(self):
for db in self.dbs.itervalues():
db.close()
self.dbs.clear()
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/>'
if channel in self.dbs:
return self.dbs[channel]
filename = plugins.makeChannelFilename(self.filename, channel)
if os.path.exists(filename):
self.dbs[channel] = sqlite.connect(filename)
return self.dbs[channel]
db = sqlite.connect(filename)
self.dbs[channel] = db
cursor = db.cursor()
cursor.execute("""CREATE TABLE factoids (
key TEXT PRIMARY KEY,
created_by INTEGER,
created_at TIMESTAMP,
modified_by INTEGER,
modified_at TIMESTAMP,
locked_at TIMESTAMP,
locked_by INTEGER,
last_requested_by TEXT,
last_requested_at TIMESTAMP,
fact TEXT,
requested_count INTEGER
)""")
db.commit()
return db
def getFactoid(self, channel, key):
db = self._getDb(channel)
cursor = db.cursor()
cursor.execute("""SELECT fact FROM factoids
WHERE key LIKE %s""", key)
if cursor.rowcount == 0:
return None
else:
return cursor.fetchall()[0]
def getFactinfo(self, channel, key):
db = self._getDb(channel)
cursor = db.cursor()
cursor.execute("""SELECT created_by, created_at,
modified_by, modified_at,
last_requested_by, last_requested_at,
requested_count, locked_by, locked_at
FROM factoids
WHERE key LIKE %s""", key)
if cursor.rowcount == 0:
return None
else:
return cursor.fetchone()
def randomFactoid(self, channel):
db = self._getDb(channel)
cursor = db.cursor()
cursor.execute("""SELECT fact, key FROM factoids
ORDER BY random() LIMIT 1""")
if cursor.rowcount == 0:
return None
else:
return cursor.fetchone()
def addFactoid(self, channel, key, value, creator_id):
db = self._getDb(channel)
cursor = db.cursor()
cursor.execute("""INSERT INTO factoids VALUES
(%s, %s, %s, NULL, NULL, NULL, NULL,
NULL, NULL, %s, 0)""",
key, creator_id, int(time.time()), value)
db.commit()
def updateFactoid(self, channel, key, newvalue, modifier_id):
db = self._getDb(channel)
cursor = db.cursor()
cursor.execute("""UPDATE factoids
SET fact=%s, modified_by=%s,
modified_at=%s WHERE key LIKE %s""",
newvalue, modifier_id, int(time.time()), key)
db.commit()
def updateRequest(self, channel, key, hostmask):
db = self._getDb(channel)
cursor = db.cursor()
cursor.execute("""UPDATE factoids SET
last_requested_by = %s,
last_requested_at = %s,
requested_count = requested_count + 1
WHERE key = %s""",
hostmask, int(time.time()), key)
db.commit()
def removeFactoid(self, channel, key):
db = self._getDb(channel)
cursor = db.cursor()
cursor.execute("""DELETE FROM factoids WHERE key LIKE %s""",
key)
db.commit()
def locked(self, channel, key):
db = self._getDb(channel)
cursor = db.cursor()
cursor.execute ("""SELECT locked_by FROM factoids
WHERE key LIKE %s""", key)
if cursor.fetchone()[0] is None:
return False
else:
return True
def lock(self, channel, key, locker_id):
db = self._getDb(channel)
cursor = db.cursor()
cursor.execute("""UPDATE factoids
SET locked_by=%s, locked_at=%s
WHERE key LIKE %s""",
locker_id, int(time.time()), key)
db.commit()
def unlock(self, channel, key):
db = self._getDb(channel)
cursor = db.cursor()
cursor.execute("""UPDATE factoids
SET locked_by=%s, locked_at=%s
WHERE key LIKE %s""", None, None, key)
db.commit()
def mostAuthored(self, channel, limit):
db = self._getDb(channel)
cursor = db.cursor()
cursor.execute("""SELECT created_by, count(key) FROM factoids
GROUP BY created_by
ORDER BY count(key) DESC LIMIT %s""", limit)
return cursor.fetchall()
def mostRecent(self, channel, limit):
db = self._getDb(channel)
cursor = db.cursor()
cursor.execute("""SELECT key FROM factoids
ORDER BY created_at DESC LIMIT %s""", limit)
return cursor.fetchall()
def mostPopular(self, channel, limit):
db = self._getDb(channel)
cursor = db.cursor()
cursor.execute("""SELECT key, requested_count FROM factoids
WHERE requested_count > 0
ORDER BY requested_count DESC LIMIT %s""", limit)
if cursor.rowcount == 0:
return []
else:
return cursor.fetchall()
def getKeysByAuthor(self, channel, authorId):
db = self._getDb(channel)
cursor = db.cursor()
cursor.execute("""SELECT key FROM factoids WHERE created_by=%s
ORDER BY key""", authorId)
if cursor.rowcount == 0:
return []
else:
return cursor.fetchall()
def getKeysByGlob(self, channel, glob):
db = self._getDb(channel)
cursor = db.cursor()
glob = '%%%s%%' % glob
cursor.execute("""SELECT key FROM factoids WHERE key LIKE %s
ORDER BY key""", glob)
if cursor.rowcount == 0:
return []
else:
return cursor.fetchall()
def getKeysByValueGlob(self, channel, glob):
db = self._getDb(channel)
cursor = db.cursor()
glob = '%%%s%%' % glob
cursor.execute("""SELECT key FROM factoids WHERE fact LIKE %s
ORDER BY key""", glob)
if cursor.rowcount == 0:
return []
else:
return cursor.fetchall()
MoobotDB = plugins.DB('MoobotFactoids', {'sqlite': SqliteMoobotDB})
class MoobotFactoids(callbacks.Plugin):
"""Add the help for "@help MoobotFactoids" here (assuming you don't implement a MoobotFactoids
command). This should describe *how* to use this plugin."""
callBefore = ['Dunno']
def __init__(self, irc):
self.db = MoobotDB()
self.__parent = super(MoobotFactoids, self)
self.__parent.__init__(irc)
def die(self):
self.__parent.die()
self.db.close()
def reset(self):
self.db.close()
_replyTag = '<reply>'
_actionTag = '<action>'
def _parseFactoid(self, irc, msg, fact):
type = 'define' # Default is to just spit the factoid back as a
# definition of what the key is (i.e., "foo is bar")
newfact = pickOptions(fact)
if newfact.startswith(self._replyTag):
newfact = newfact[len(self._replyTag):]
type = 'reply'
elif newfact.startswith(self._actionTag):
newfact = newfact[len(self._actionTag):]
type = 'action'
newfact = newfact.strip()
newfact = ircutils.standardSubstitute(irc, msg, newfact)
return (type, newfact)
def invalidCommand(self, irc, msg, tokens):
if '=~' in tokens:
self.changeFactoid(irc, msg, tokens)
elif tokens and tokens[0] in ('no', 'no,'):
self.replaceFactoid(irc, msg, tokens)
elif ['is', 'also'] in utils.seq.window(tokens, 2):
self.augmentFactoid(irc, msg, tokens)
else:
key = ' '.join(tokens)
key = self._sanitizeKey(key)
channel = plugins.getChannel(msg.args[0])
fact = self.db.getFactoid(channel, key)
if fact:
self.db.updateRequest(channel, key, msg.prefix)
# getFactoid returns "all results", so we need to extract the
# first one.
fact = fact[0]
# Update the requested count/requested by for this key
hostmask = msg.prefix
# Now actually get the factoid and respond accordingly
(type, text) = self._parseFactoid(irc, msg, fact)
if type == 'action':
irc.reply(text, action=True)
elif type == 'reply':
irc.reply(text, prefixName=False)
elif type == 'define':
irc.reply(format('%s is %s', key, text), prefixName=False)
else:
assert False, 'Spurious type from _parseFactoid'
else:
if 'is' in tokens or '_is_' in tokens:
self.addFactoid(irc, msg, tokens)
def _getUserId(self, irc, prefix):
try:
return ircdb.users.getUserId(prefix)
except KeyError:
irc.errorNotRegistered(Raise=True)
def _sanitizeKey(self, key):
return key.rstrip('!? ')
def _checkNotLocked(self, irc, channel, key):
if self.db.locked(channel, key):
irc.error(format('Factoid %q is locked.', key), Raise=True)
def _getFactoid(self, irc, channel, key):
fact = self.db.getFactoid(channel, key)
if fact is not None:
return fact
else:
irc.error(format('Factoid %q not found.', key), Raise=True)
def _getKeyAndFactoid(self, tokens):
if '_is_' in tokens:
p = '_is_'.__eq__
elif 'is' in tokens:
p = 'is'.__eq__
else:
self.log.debug('Invalid tokens for {add,replace}Factoid: %s.',
tokens)
s = 'Missing an \'is\' or \'_is_\'.'
raise ValueError, s
(key, newfact) = map(' '.join, utils.iter.split(p, tokens, maxsplit=1))
key = self._sanitizeKey(key)
return (key, newfact)
def addFactoid(self, irc, msg, tokens):
# First, check and see if the entire message matches a factoid key
channel = plugins.getChannel(msg.args[0])
id = self._getUserId(irc, msg.prefix)
try:
(key, fact) = self._getKeyAndFactoid(tokens)
except ValueError, e:
irc.error(str(e), Raise=True)
# Check and make sure it's not in the DB already
if self.db.getFactoid(channel, key):
irc.error(format('Factoid %q already exists.', key), Raise=True)
self.db.addFactoid(channel, key, fact, id)
irc.replySuccess()
def changeFactoid(self, irc, msg, tokens):
id = self._getUserId(irc, msg.prefix)
(key, regexp) = map(' '.join,
utils.iter.split('=~'.__eq__, tokens, maxsplit=1))
channel = plugins.getChannel(msg.args[0])
# Check and make sure it's in the DB
fact = self._getFactoid(irc, channel, key)
self._checkNotLocked(irc, channel, key)
# It's fair game if we get to here
try:
r = utils.str.perlReToReplacer(regexp)
except ValueError, e:
irc.errorInvalid('regexp', regexp, Raise=True)
fact = fact[0]
new_fact = r(fact)
self.db.updateFactoid(channel, key, new_fact, id)
irc.replySuccess()
def augmentFactoid(self, irc, msg, tokens):
# Must be registered!
id = self._getUserId(irc, msg.prefix)
pairs = list(utils.seq.window(tokens, 2))
isAlso = pairs.index(['is', 'also'])
key = ' '.join(tokens[:isAlso])
new_text = ' '.join(tokens[isAlso+2:])
channel = plugins.getChannel(msg.args[0])
fact = self._getFactoid(irc, channel, key)
self._checkNotLocked(irc, channel, key)
# It's fair game if we get to here
fact = fact[0]
new_fact = format('%s, or %s', fact, new_text)
self.db.updateFactoid(channel, key, new_fact, id)
irc.replySuccess()
def replaceFactoid(self, irc, msg, tokens):
# Must be registered!
channel = plugins.getChannel(msg.args[0])
id = self._getUserId(irc, msg.prefix)
del tokens[0] # remove the "no,"
try:
(key, fact) = self._getKeyAndFactoid(tokens)
except ValueError, e:
irc.error(str(e), Raise=True)
_ = self._getFactoid(irc, channel, key)
self._checkNotLocked(irc, channel, key)
self.db.removeFactoid(channel, key)
self.db.addFactoid(channel, key, fact, id)
irc.replySuccess()
def literal(self, irc, msg, args, channel, key):
"""[<channel>] <factoid key>
Returns the literal factoid for the given factoid key. No parsing of
the factoid value is done as it is with normal retrieval. <channel>
is only necessary if the message isn't sent in the channel itself.
"""
fact = self._getFactoid(irc, channel, key)
fact = fact[0]
irc.reply(fact)
literal = wrap(literal, ['channeldb', 'text'])
def factinfo(self, irc, msg, args, channel, key):
"""[<channel>] <factoid key>
Returns the various bits of info on the factoid for the given key.
<channel> is only necessary if the message isn't sent in the channel
itself.
"""
# Start building the response string
s = key + ': '
# Next, get all the info and build the response piece by piece
info = self.db.getFactinfo(channel, key)
if not info:
irc.error(format('No such factoid: %q', key))
return
(created_by, created_at, modified_by, modified_at, last_requested_by,
last_requested_at, requested_count, locked_by, locked_at) = info
# First, creation info.
# Map the integer created_by to the username
created_by = plugins.getUserName(created_by)
created_at = time.strftime(conf.supybot.reply.format.time(),
time.localtime(int(created_at)))
s += format('Created by %s on %s.', created_by, created_at)
# Next, modification info, if any.
if modified_by is not None:
modified_by = plugins.getUserName(modified_by)
modified_at = time.strftime(conf.supybot.reply.format.time(),
time.localtime(int(modified_at)))
s += format(' Last modified by %s on %s.',modified_by, modified_at)
# Next, last requested info, if any
if last_requested_by is not None:
last_by = last_requested_by # not an int user id
last_at = time.strftime(conf.supybot.reply.format.time(),
time.localtime(int(last_requested_at)))
req_count = requested_count
s += format(' Last requested by %s on %s, requested %n.',
last_by, last_at, (requested_count, 'time'))
# Last, locked info
if locked_at is not None:
lock_at = time.strftime(conf.supybot.reply.format.time(),
time.localtime(int(locked_at)))
lock_by = plugins.getUserName(locked_by)
s += format(' Locked by %s on %s.', lock_by, lock_at)
irc.reply(s)
factinfo = wrap(factinfo, ['channeldb', 'text'])
def _lock(self, irc, msg, channel, user, key, locking=True):
#self.log.debug('in _lock')
#self.log.debug('id: %s' % id)
id = user.id
info = self.db.getFactinfo(channel, key)
if not info:
irc.error(format('No such factoid: %q', key))
return
(created_by, _, _, _, _, _, _, locked_by, _) = info
# Don't perform redundant operations
if locking and locked_by is not None:
irc.error(format('Factoid %q is already locked.', key))
return
if not locking and locked_by is None:
irc.error(format('Factoid %q is not locked.', key))
return
# Can only lock/unlock own factoids unless you're an admin
#self.log.debug('admin?: %s' % ircdb.checkCapability(id, 'admin'))
#self.log.debug('created_by: %s' % created_by)
if not (ircdb.checkCapability(id, 'admin') or created_by == id):
if locking:
s = 'lock'
else:
s = 'unlock'
irc.error(format('Cannot %s someone else\'s factoid unless you '
'are an admin.', s))
return
# Okay, we're done, ready to lock/unlock
if locking:
self.db.lock(channel, key, id)
else:
self.db.unlock(channel, key)
irc.replySuccess()
def lock(self, irc, msg, args, channel, user, key):
"""[<channel>] <factoid key>
Locks the factoid with the given factoid key. Requires that the user
be registered and have created the factoid originally. <channel> is
only necessary if the message isn't sent in the channel itself.
"""
self._lock(irc, msg, channel, user, key, True)
lock = wrap(lock, ['channeldb', 'user', 'text'])
def unlock(self, irc, msg, args, channel, user, key):
"""[<channel>] <factoid key>
Unlocks the factoid with the given factoid key. Requires that the
user be registered and have locked the factoid. <channel> is only
necessary if the message isn't sent in the channel itself.
"""
self._lock(irc, msg, channel, user, key, False)
unlock = wrap(unlock, ['channeldb', 'user', 'text'])
def most(self, irc, msg, args, channel, method):
"""[<channel>] {popular|authored|recent}
Lists the most {popular|authored|recent} factoids. "popular" lists the
most frequently requested factoids. "authored" lists the author with
the most factoids. "recent" lists the most recently created factoids.
<channel> is only necessary if the message isn't sent in the channel
itself.
"""
method = method.capitalize()
method = getattr(self, '_most%s' % method, None)
if method is None:
raise callbacks.ArgumentError
limit = self.registryValue('mostCount', channel)
method(irc, channel, limit)
most = wrap(most, ['channeldb',
('literal', ('popular', 'authored', 'recent'))])
def _mostAuthored(self, irc, channel, limit):
results = self.db.mostAuthored(channel, limit)
L = ['%s (%s)' % (plugins.getUserName(t[0]), int(t[1]))
for t in results]
if L:
author = 'author'
if len(L) != 1:
author = 'authors'
irc.reply(format('Most prolific %s: %L', author, L))
else:
irc.error('There are no factoids in my database.')
def _mostRecent(self, irc, channel, limit):
results = self.db.mostRecent(channel, limit)
L = [format('%q', t[0]) for t in results]
if L:
irc.reply(format('%n: %L', (len(L), 'latest', 'factoid'), L))
else:
irc.error('There are no factoids in my database.')
def _mostPopular(self, irc, channel, limit):
results = self.db.mostPopular(channel, limit)
L = [format('%q (%s)', t[0], t[1]) for t in results]
if L:
irc.reply(
format('Top %n: %L', (len(L), 'requested', 'factoid'), L))
else:
irc.error('No factoids have been requested from my database.')
def listauth(self, irc, msg, args, channel, author):
"""[<channel>] <author name>
Lists the keys of the factoids with the given author. Note that if an
author has an integer name, you'll have to use that author's id to use
this function (so don't use integer usernames!). <channel> is only
necessary if the message isn't sent in the channel itself.
"""
try:
id = ircdb.users.getUserId(author)
except KeyError:
irc.errorNoUser(name=author, Raise=True)
results = self.db.getKeysByAuthor(channel, id)
if not results:
irc.reply(format('No factoids by %q found.', author))
return
keys = [format('%q', t[0]) for t in results]
s = format('Author search for %q (%i found): %L',
author, len(keys), keys)
irc.reply(s)
listauth = wrap(listauth, ['channeldb', 'something'])
def listkeys(self, irc, msg, args, channel, search):
"""[<channel>] <text>
Lists the keys of the factoids whose key contains the provided text.
<channel> is only necessary if the message isn't sent in the channel
itself.
"""
results = self.db.getKeysByGlob(channel, search)
if not results:
irc.reply(format('No keys matching %q found.', search))
elif len(results) == 1 and \
self.registryValue('showFactoidIfOnlyOneMatch', channel):
key = results[0][0]
self.invalidCommand(irc, msg, [key])
else:
keys = [format('%q', tup[0]) for tup in results]
s = format('Key search for %q (%i found): %L',
search, len(keys), keys)
irc.reply(s)
listkeys = wrap(listkeys, ['channeldb', 'text'])
def listvalues(self, irc, msg, args, channel, search):
"""[<channel>] <text>
Lists the keys of the factoids whose value contains the provided text.
<channel> is only necessary if the message isn't sent in the channel
itself.
"""
results = self.db.getKeysByValueGlob(channel, search)
if not results:
irc.reply(format('No values matching %q found.', search))
return
keys = [format('%q', tup[0]) for tup in results]
s = format('Value search for %q (%i found): %L',
search, len(keys), keys)
irc.reply(s)
listvalues = wrap(listvalues, ['channeldb', 'text'])
def remove(self, irc, msg, args, channel, _, key):
"""[<channel>] <factoid key>
Deletes the factoid with the given key. <channel> is only necessary
if the message isn't sent in the channel itself.
"""
_ = self._getFactoid(irc, channel, key)
self._checkNotLocked(irc, channel, key)
self.db.removeFactoid(channel, key)
irc.replySuccess()
remove = wrap(remove, ['channeldb', 'user', 'text'])
def random(self, irc, msg, args, channel):
"""[<channel>]
Displays a random factoid (along with its key) from the database.
<channel> is only necessary if the message isn't sent in the channel
itself.
"""
results = self.db.randomFactoid(channel)
if not results:
irc.error('No factoids in the database.')
return
(fact, key) = results
irc.reply(format('Random factoid: %q is %q', key, fact))
random = wrap(random, ['channeldb'])
Class = MoobotFactoids
# vim:set shiftwidth=4 tabstop=8 expandtab textwidth=78:

View File

@ -0,0 +1,343 @@
# -*- encoding: utf-8 -*-
###
# Copyright (c) 2003-2005, Daniel DiPaolo
# 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.
###
from supybot.test import *
#import supybot.plugin as plugin
import supybot.ircutils as ircutils
try:
import sqlite
except ImportError:
sqlite = None
MF = plugin.loadPluginModule('MoobotFactoids')
MFconf = conf.supybot.plugins.MoobotFactoids
class OptionListTestCase(SupyTestCase):
maxIterations = 267
def _testOptions(self, s, L):
max = self.maxIterations
original = L[:]
while max and L:
max -= 1
option = MF.plugin.pickOptions(s)
self.failUnless(option in original,
'Option %s not in %s' % (option, original))
if option in L:
L.remove(option)
self.failIf(L, 'Some options never seen: %s' % L)
def testPickOptions(self):
self._testOptions('(a|b)', ['a', 'b'])
self._testOptions('a', ['a'])
self._testOptions('(a|b (c|d))', ['a', 'b c', 'b d'])
self._testOptions('(a|(b|)c)', ['a', 'bc', 'c'])
self._testOptions('(a(b|)|(c|)d)', ['a', 'ab', 'cd', 'd'])
self._testOptions('(a|)', ['a', ''])
self._testOptions('(|a)', ['a', ''])
self._testOptions('((a)|(b))', ['(a)', '(b)'])
class FactoidsTestCase(ChannelPluginTestCase):
plugins = ('MoobotFactoids', 'User', 'String', 'Utilities')
config = {'reply.whenNotCommand': False}
def setUp(self):
ChannelPluginTestCase.setUp(self)
# Create a valid user to use
self.prefix = 'mf!bar@baz'
self.irc.feedMsg(ircmsgs.privmsg(self.nick, 'register tester moo',
prefix=self.prefix))
m = self.irc.takeMsg() # Response to register.
def testAddFactoid(self):
self.assertNotError('moo is foo')
# Check stripping punctuation
self.assertError('moo!? is foo') # 'moo' already exists
self.assertNotError('foo!? is foo')
self.assertResponse('foo', 'foo is foo')
self.assertNotError('bar is <reply>moo is moo')
self.assertResponse('bar', 'moo is moo')
# Check substitution
self.assertNotError('who is <reply>$who')
self.assertResponse('who', ircutils.nickFromHostmask(self.prefix))
# Check that actions ("\x01ACTION...") don't match
m = ircmsgs.action(self.channel, 'is doing something')
self.irc.feedMsg(m)
self.assertNoResponse(' ', 1)
def testLiteral(self):
self.assertError('literal moo') # no factoids yet
self.assertNotError('moo is <reply>foo')
self.assertResponse('literal moo', '<reply>foo')
self.assertNotError('moo2 is moo!')
self.assertResponse('literal moo2', 'moo!')
self.assertNotError('moo3 is <action>foo')
self.assertResponse('literal moo3', '<action>foo')
def testGetFactoid(self):
self.assertNotError('moo is <reply>foo')
self.assertResponse('moo', 'foo')
self.assertNotError('moo2 is moo!')
self.assertResponse('moo2', 'moo2 is moo!')
self.assertNotError('moo3 is <action>foo')
self.assertAction('moo3', 'foo')
# Test and make sure it's parsing
self.assertNotError('moo4 is <reply>(1|2|3)')
self.assertRegexp('moo4', '^(1|2|3)$')
# Check case-insensitivity
self.assertResponse('MOO', 'foo')
self.assertResponse('mOo', 'foo')
self.assertResponse('MoO', 'foo')
# Check the "_is_" ability
self.assertNotError('remove moo')
self.assertNotError('moo _is_ <reply>foo')
self.assertResponse('moo', 'foo')
self.assertNotError('foo is bar _is_ baz')
self.assertResponse('foo is bar', 'foo is bar is baz')
def testFactinfo(self):
self.assertNotError('moo is <reply>foo')
self.assertRegexp('factinfo moo', '^moo: Created by tester on.*$')
self.assertNotError('moo')
self.assertRegexp('factinfo moo', self.prefix + '.*1 time')
self.assertNotError('moo')
self.assertRegexp('factinfo moo', self.prefix + '.*2 times')
self.assertNotError('moo =~ s/foo/bar/')
self.assertRegexp('factinfo moo',
'^moo: Created by tester on'
'.*?\. Last modified by tester on .*?\. '
'Last requested by %s on .*?, '
'requested 2 times.$' % self.prefix)
self.assertNotError('lock moo')
self.assertRegexp('factinfo moo',
'^moo: Created by tester on'
'.*?\. Last modified by tester on .*?\. '
'Last requested by %s on .*?, '
'requested 2 times. '
'Locked by tester on .*\.$' % self.prefix)
self.assertNotError('unlock moo')
self.assertRegexp('factinfo moo',
'^moo: Created by tester on'
'.*?\. Last modified by tester on .*?\. '
'Last requested by %s on .*?, '
'requested 2 times.$' % self.prefix)
# Make sure I solved this bug
# Check and make sure all the other stuff is reset
self.assertNotError('foo is bar')
self.assertNotError('foo =~ s/bar/blah/')
self.assertNotError('foo')
self.assertNotError('no foo is baz')
self.assertRegexp('factinfo foo',
'^foo: Created by tester on'
'(?!(request|modif)).*?\.$')
def testLockUnlock(self):
# disable world.testing since we want new users to not
# magically be endowed with the admin capability
try:
world.testing = False
self.assertNotError('moo is <reply>moo')
self.assertNotError('lock moo')
self.assertRegexp('factinfo moo',
'^moo: Created by tester on'
'.*?\. Locked by tester on .*?\.')
# switch user
original = self.prefix
self.prefix = 'moo!moo@moo'
self.assertNotError('register nottester moo', private=True)
self.assertError('unlock moo')
self.assertRegexp('factinfo moo',
'^moo: Created by tester on'
'.*?\. Locked by tester on .*?\.')
# switch back
self.prefix = original
self.assertNotError('identify tester moo', private=True)
self.assertNotError('unlock moo')
self.assertRegexp('factinfo moo',
'^moo: Created by tester on.*?\.')
finally:
world.testing = True
def testChangeFactoid(self):
self.assertNotError('moo is <reply>moo')
self.assertNotError('moo =~ s/moo/moos/')
self.assertResponse('moo', 'moos')
self.assertNotError('moo =~ s/reply/action/')
self.assertAction('moo', 'moos')
self.assertNotError('moo =~ s/moos/(moos|woofs)/')
self.assertActionRegexp('moo', '^(moos|woofs)$')
self.assertError('moo =~ s/moo/')
def testMost(self):
userPrefix1 = 'moo!bar@baz'; userNick1 = 'moo'
userPrefix2 = 'boo!bar@baz'; userNick2 = 'boo'
self.assertNotError('register %s bar' % userNick1,
frm=userPrefix1, private=True)
self.assertNotError('register %s bar' % userNick2,
frm=userPrefix2, private=True)
# Check an empty database
self.assertError('most popular')
self.assertError('most authored')
self.assertError('most recent')
# Check singularity response
self.prefix = userPrefix1
self.assertNotError('moogle is <reply>moo')
self.assertError('most popular')
self.assertResponse('most authored',
'Most prolific author: moo (1)')
self.assertRegexp('most recent', "1 latest factoid:.*moogle")
self.assertResponse('moogle', 'moo')
self.assertRegexp('most popular',
"Top 1 requested factoid:.*moogle.*(1)")
# Check plural response
self.prefix = userPrefix2
self.assertNotError('mogle is <reply>mo')
self.assertRegexp('most authored',
'Most prolific authors:.*moo.*(1).*boo.*(1)')
self.assertRegexp('most recent',
"2 latest factoids:.*mogle.*moogle.*")
self.assertResponse('moogle', 'moo')
self.assertRegexp('most popular',
"Top 1 requested factoid:.*moogle.*(2)")
self.assertResponse('mogle', 'mo')
self.assertRegexp('most popular',
"Top 2 requested factoids:.*"
"moogle.*(2).*mogle.*(1)")
# Check most author ordering
self.assertNotError('moo is <reply>oom')
self.assertRegexp('most authored',
'Most prolific authors:.*boo.*(2).*moo.*(1)')
def testListkeys(self):
self.assertResponse('listkeys %', 'No keys matching "%" found.')
self.assertNotError('moo is <reply>moo')
# With this set, if only one key matches, it should respond with
# the factoid
orig = MFconf.showFactoidIfOnlyOneMatch()
try:
MFconf.showFactoidIfOnlyOneMatch.setValue(True)
self.assertResponse('listkeys moo', 'moo')
self.assertResponse('listkeys foo', 'No keys matching "foo" '
'found.')
# Throw in a bunch more
for i in range(10):
self.assertNotError('moo%s is <reply>moo' % i)
self.assertRegexp('listkeys moo',
'^Key search for "moo" '
'\(11 found\): ("moo\d*", )+and "moo9"$')
self.assertNotError('foo is bar')
self.assertRegexp('listkeys %',
'^Key search for "\%" '
'\(12 found\): "foo", ("moo\d*", )+and '
'"moo9"$')
# Check quoting
self.assertNotError('foo\' is bar')
self.assertResponse('listkeys foo',
'Key search for "foo" '
'(2 found): "foo" and "foo\'"')
# Check unicode stuff
self.assertResponse('listkeys Б', 'No keys matching "Б" found.')
self.assertNotError('АБВГДЕЖ is foo')
self.assertNotError('АБВГДЕЖЗИ is foo')
self.assertResponse('listkeys Б',
'Key search for "Б" '
'(2 found): "АБВГДЕЖ" and "АБВГДЕЖЗИ"')
finally:
MFconf.showFactoidIfOnlyOneMatch.setValue(orig)
def testListvalues(self):
self.assertNotError('moo is moo')
self.assertResponse('listvalues moo',
'Value search for "moo" (1 found): "moo"')
def testListauth(self):
self.assertNotError('moo is <reply>moo')
self.assertRegexp('listauth tester', 'tester.*\(1 found\):.*moo')
self.assertError('listauth moo')
def testRemove(self):
self.assertNotError('moo is <reply>moo')
self.assertNotError('lock moo')
self.assertError('remove moo')
self.assertNotError('unlock moo')
self.assertNotError('remove moo')
def testAugmentFactoid(self):
self.assertNotError('moo is foo')
self.assertNotError('moo is also bar')
self.assertResponse('moo', 'moo is foo, or bar')
self.assertNotError('moo is bar _is_ foo')
self.assertNotError('moo is bar is also foo')
self.assertResponse('moo is bar', 'moo is bar is foo, or foo')
def testReplaceFactoid(self):
self.assertNotError('moo is foo')
self.assertNotError('no moo is bar')
self.assertResponse('moo', 'moo is bar')
self.assertNotError('no, moo is baz')
self.assertResponse('moo', 'moo is baz')
self.assertNotError('lock moo')
self.assertError('no moo is qux')
self.assertNotError('foo is bar _is_ foo')
self.assertNotError('no foo is bar _is_ baz')
self.assertResponse('foo is bar', 'foo is bar is baz')
def testRegexpNotCalledIfAlreadyHandled(self):
self.assertResponse('echo foo is bar', 'foo is bar')
self.assertNoResponse(' ', 3)
def testNoResponseToCtcp(self):
self.assertNotError('foo is bar')
self.assertResponse('foo', 'foo is bar')
self.irc.feedMsg(ircmsgs.privmsg(self.irc.nick, '\x01VERSION\x01'))
m = self.irc.takeMsg()
self.failIf(m)
def testAddFactoidNotCalledWithBadNestingSyntax(self):
self.assertError('re s/Error:.*/jbm is a tard/ ]')
self.assertNoResponse(' ', 3)
def testConfigShowFactoidIfOnlyOneMatch(self):
# man these are long
MFconf = conf.supybot.plugins.MoobotFactoids
self.assertNotError('foo is bar')
# Default to saying the factoid value
self.assertResponse('listkeys foo', 'foo is bar')
# Check the False setting
MFconf.showFactoidIfOnlyOneMatch.setValue(False)
self.assertResponse('listkeys foo', 'Key search for "foo" '
'(1 found): "foo"')
def testRandom(self):
self.assertNotError('foo is <reply>bar')
self.assertNotError('bar is <reply>baz')
self.assertRegexp('random', r'bar|baz')
# vim:set shiftwidth=4 tabstop=8 expandtab textwidth=78: