#!/usr/bin/env python ### # Copyright (c) 2002, Jeremiah Fincher # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, # this list of conditions, and the following disclaimer. # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions, and the following disclaimer in the # documentation and/or other materials provided with the distribution. # * Neither the name of the author of this software nor the name of # contributors to this software may be used to endorse or promote products # derived from this software without specific prior written consent. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. ### """ Handles "factoids," little tidbits of information held in a database and available on demand via several commands. """ from baseplugin import * import time import os.path import sqlite import conf import utils import ircdb import ircutils import privmsgs import callbacks example = utils.wrapLines(""" @list Factoids jemfinch: factoidinfo, learn, lock, randomfactoid, forget, unlock, whatis @learn jemfinch as the primary author of supybot. jemfinch: The operation succeeded. @factoidinfo jemfinch jemfinch: Key 'jemfinch' is not locked and has 1 factoids associated with it: #0 was added by jemfinch at 03:57 AM, August 29, 2003 @lock jemfinch jemfinch: The operation succeeded. @factoidinfo jemfinch jemfinch: Key 'jemfinch' is locked and has 1 factoids associated with it: #0 was added by jemfinch at 03:57 AM, August 29, 2003 @learn jemfinch as a horrible assembly programmer jemfinch: Error: That factoid is locked. @unlock jemfinch jemfinch: The operation succeeded. @learn jemfinch as a horrible assembly programmer jemfinch: The operation succeeded. @whatis jemfinch jemfinch: jemfinch could be (#0) the primary author of supybot., or (#1) a horrible assembly programmer. @forget jemfinch jemfinch: Error: 2 factoids have that key. Please specify which one to remove. @forget jemfinch 1 jemfinch: The operation succeeded. @whatis jemfinch jemfinch: jemfinch could be (#0) the primary author of supybot.. @forget jemfinch 0 jemfinch: The operation succeeded. @whatis jemfinch G jemfinch: Error: No factoid matches that key. @randomfactoid jemfinch: "sf.net-website": https://sourceforge.net/docman/display_doc.php?docid=4297&group_id=1; "private-attributes": http://groups.google.com/groups?q=jp+calderone+private+attributes&hl=en&lr=&ie=UTF-8&oe=UTF-8&selm=mailman.1050275130.5456.python-list%40python.org&rnum=1; "CFnews": http://inkedmn.homelinux.org/~inkedmn/vbcustom1.html; "CFnews": something else @whatis cfnews jemfinch: cfnews could be (#0) http://inkedmn.homelinux.org/~inkedmn/vbcustom1.html, or (#1) something else. @searchfactoids /^.f/i jemfinch: 'CFnews' and 'sf.net-website' """) class Factoids(ChannelDBHandler, callbacks.Privmsg): def __init__(self): ChannelDBHandler.__init__(self) callbacks.Privmsg.__init__(self) def makeDb(self, filename): if os.path.exists(filename): return sqlite.connect(filename) db = sqlite.connect(filename) cursor = db.cursor() cursor.execute("""CREATE TABLE keys ( id INTEGER PRIMARY KEY, key TEXT UNIQUE ON CONFLICT IGNORE, locked BOOLEAN )""") cursor.execute("""CREATE TABLE factoids ( id INTEGER PRIMARY KEY, key_id INTEGER, added_by TEXT, added_at TIMESTAMP, fact TEXT )""") cursor.execute("""CREATE TRIGGER remove_factoids BEFORE DELETE ON keys BEGIN DELETE FROM factoids WHERE key_id = old.id; END """) db.commit() return db def learn(self, irc, msg, args): """[] as Associates with . is only necessary if the message isn't sent on the channel itself. """ channel = privmsgs.getChannel(msg, args) try: i = args.index('as') except ValueError: raise callbacks.ArgumentError args.pop(i) key = ' '.join(args[:i]) factoid = ' '.join(args[i:]) db = self.getDb(channel) cursor = db.cursor() cursor.execute("SELECT id, locked FROM keys WHERE key LIKE %s", key) if cursor.rowcount == 0: cursor.execute("""INSERT INTO keys VALUES (NULL, %s, 0)""", key) db.commit() cursor.execute("SELECT id, locked FROM keys WHERE key LIKE %s",key) (id, locked) = map(int, cursor.fetchone()) capability = ircdb.makeChannelCapability(channel, 'factoids') if not locked: if not ircdb.checkCapability(msg.prefix, capability): irc.error(msg, conf.replyNoCapability % capability) return if ircdb.users.hasUser(msg.prefix): name = ircdb.users.getUser(msg.prefix).name else: name = msg.nick cursor.execute("""INSERT INTO factoids VALUES (NULL, %s, %s, %s, %s)""", id, name, int(time.time()), factoid) db.commit() irc.reply(msg, conf.replySuccess) else: irc.error(msg, 'That factoid is locked.') def whatis(self, irc, msg, args): """[] [] Looks up the value of in the factoid database. If given a number, will return only that exact factoid. is only necessary if the message isn't sent in the channel itself. """ channel = privmsgs.getChannel(msg, args) key = privmsgs.getArgs(args) db = self.getDb(channel) cursor = db.cursor() cursor.execute("""SELECT factoids.fact FROM factoids, keys WHERE keys.key LIKE %s AND factoids.key_id=keys.id ORDER BY factoids.id LIMIT 20""", key) if cursor.rowcount == 0: irc.error(msg, 'No factoid matches that key.') else: factoids = [] counter = 0 for result in cursor.fetchall(): factoids.append('(#%s) %s' % (counter, result[0])) counter += 1 irc.reply(msg, '%r could be %s' % (key, ', or '.join(factoids))) def lock(self, irc, msg, args): """[] Locks the factoid(s) associated with so that they cannot be removed or added to. is only necessary if the message isn't sent in the channel itself. """ channel = privmsgs.getChannel(msg, args) key = privmsgs.getArgs(args) db = self.getDb(channel) capability = ircdb.makeChannelCapability(channel, 'factoids') if ircdb.checkCapability(msg.prefix, capability): cursor = db.cursor() cursor.execute("UPDATE keys SET locked=1 WHERE key LIKE %s", key) db.commit() irc.reply(msg, conf.replySuccess) else: irc.error(msg, conf.replyNoCapability % capability) def unlock(self, irc, msg, args): """[] Unlocks the factoid(s) associated with so that they can be removed or added to. is only necessary if the message isn't sent in the channel itself. """ channel = privmsgs.getChannel(msg, args) key = privmsgs.getArgs(args) db = self.getDb(channel) capability = ircdb.makeChannelCapability(channel, 'factoids') if ircdb.checkCapability(msg.prefix, capability): cursor = db.cursor() cursor.execute("UPDATE keys SET locked=0 WHERE key LIKE %s",key) db.commit() irc.reply(msg, conf.replySuccess) else: irc.error(msg, conf.replyNoCapability % capability) def forget(self, irc, msg, args): """[] [|*] Removes the factoid from the factoids database. If there are more than one factoid with such a key, a number is necessary to determine which one should be removed. A * can be used to remove all factoids associated with a key. is only necessary if the message isn't sent in the channel itself. """ channel = privmsgs.getChannel(msg, args) if args[-1].isdigit(): number = int(args.pop()) elif args[-1] == '*': del args[-1] number = True else: number = None key = privmsgs.getArgs(args) db = self.getDb(channel) capability = ircdb.makeChannelCapability(channel, 'factoids') if ircdb.checkCapability(msg.prefix, capability): cursor = db.cursor() cursor.execute("""SELECT keys.id, factoids.id FROM keys, factoids WHERE key LIKE %s AND factoids.key_id=keys.id""", key) if cursor.rowcount == 0: irc.error(msg, 'There is no such factoid.') elif cursor.rowcount == 1 or number is True: (id, _) = cursor.fetchone() cursor.execute("""DELETE FROM factoids WHERE key_id=%s""", id) cursor.execute("""DELETE FROM keys WHERE key LIKE %s""", key) db.commit() irc.reply(msg, conf.replySuccess) else: if number is not None: results = cursor.fetchall() try: (_, id) = results[number] except IndexError: irc.error(msg, 'Invalid factoid number.') return cursor.execute("DELETE FROM factoids WHERE id=%s", id) db.commit() irc.reply(msg, conf.replySuccess) else: irc.error(msg, '%s factoids have that key. ' \ 'Please specify which one to remove, ' \ 'or use * to designate all of them.' % \ cursor.rowcount) else: irc.error(msg, conf.replyNoCapability % capability) def randomfactoid(self, irc, msg, args): """[] Returns a random factoid from the database for . is only necessary if the message isn't sent in the channel itself. """ channel = privmsgs.getChannel(msg, args) db = self.getDb(channel) cursor = db.cursor() cursor.execute("""SELECT fact, key_id FROM factoids ORDER BY random() LIMIT 3""") if cursor.rowcount != 0: L = [] for (factoid, id) in cursor.fetchall(): cursor.execute("""SELECT key FROM keys WHERE id=%s""", id) (key,) = cursor.fetchone() L.append('"%s": %s' % (ircutils.bold(key), factoid)) irc.reply(msg, '; '.join(L)) else: irc.error(msg, 'I couldn\'t find a factoid.') def factoidinfo(self, irc, msg, args): """[] Gives information about the factoid(s) associated with . is only necessary if the message isn't sent in the channel itself. """ channel = privmsgs.getChannel(msg, args) key = privmsgs.getArgs(args) db = self.getDb(channel) cursor = db.cursor() cursor.execute("SELECT id, locked FROM keys WHERE key LIKE %s", key) if cursor.rowcount == 0: irc.error(msg, 'No factoid matches that key.') return (id, locked) = map(int, cursor.fetchone()) cursor.execute("""SELECT added_by, added_at FROM factoids WHERE key_id=%s ORDER BY id""", id) factoids = cursor.fetchall() L = [] counter = 0 for (added_by, added_at) in factoids: added_at = time.strftime(conf.humanTimestampFormat, time.localtime(int(added_at))) L.append('#%s was added by %s at %s' % (counter,added_by,added_at)) counter += 1 factoids = '; '.join(L) s = 'Key %r is %s and has %s factoids associated with it: %s' % \ (key, locked and 'locked' or 'not locked', counter, factoids) irc.reply(msg, s) _sqlTrans = string.maketrans('*?', '%_') def searchfactoids(self, irc, msg, args): """[] Searches the keyspace for keys matching . If isn't a regexp (i.e, it's not of the form m/foo/ or /bar/) then the literal string is searched for. """ channel = privmsgs.getChannel(msg, args) regexp = privmsgs.getArgs(args) try: r = utils.perlReToPythonRe(regexp) def p(s): return int(bool(r.search(s))) except ValueError, e: if not regexp.startswith('m/') or regexp[0] == '/' == regexp[-1]: def p(s): return int(regexp in s) else: irc.error(msg, 'Invalid regular expression.') return db = self.getDb(channel) db.create_function('p', 1, p) cursor = db.cursor() cursor.execute("""SELECT key FROM keys WHERE p(key)""") if cursor.rowcount == 0: irc.reply(msg, 'No keys matched that query.') elif cursor.rowcount > 100: irc.reply(msg, 'More than 100 keys matched that query; ' 'please narrow your query.') else: keys = [repr(t[0]) for t in cursor.fetchall()] s = utils.commaAndify(keys) if len(s) > 450-len(irc.prefix): irc.reply(msg, '%s matched that query; ' 'please narrow your query.' % \ utils.nItems(cursor.rowcount, 'key')) else: irc.reply(msg, s) Class = Factoids # vim:set shiftwidth=4 tabstop=8 expandtab textwidth=78: