#!/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. ### """ The Lookup plugin handles looking up various values by their key. """ __revision__ = "$Id$" import plugins import os import re import sys import sets import getopt import string import conf import utils import privmsgs import registry import callbacks try: import sqlite except ImportError: raise callbacks.Error, 'You need to have PySQLite installed to use this ' \ 'plugin. Download it at ' def configure(advanced): from questions import output, expect, anything, something, yn conf.registerPlugin('Lookup', True) lookups = conf.supybot.plugins.Lookup.lookups output("""This module allows you to define commands that do a simple key lookup and return some simple value. It has a command "add" that takes a command name and a file from the data dir and adds a command with that name that responds with the mapping from that file. The file itself should be composed of lines of the form key:value.""") while yn('Would you like to add a file?'): filename = something('What\'s the filename?') try: dataDir = conf.supybot.directories.data() fd = file(os.path.join(dataDir, filename)) except EnvironmentError, e: output('I couldn\'t open that file: %s' % e) continue counter = 1 try: for line in fd: line = line.rstrip('\r\n') if not line or line.startswith('#'): continue (key, value) = line.split(':', 1) counter += 1 except ValueError: output('That\'s not a valid file; ' 'line #%s is malformed.' % counter) continue command = something('What would you like the command to be?') conf.registerGlobalValue(lookups,command, registry.String(filename,'')) conf.registerPlugin('Lookup') conf.registerGroup(conf.supybot.plugins.Lookup, 'lookups') class LookupDB(plugins.DBHandler): def makeDb(self, filename): return sqlite.connect(filename) class Lookup(callbacks.Privmsg): def __init__(self): callbacks.Privmsg.__init__(self) self.lookupDomains = sets.Set() dataDir = conf.supybot.directories.data() self.dbHandler = LookupDB(name=os.path.join(dataDir, 'Lookup')) for (name, value) in registry._cache.iteritems(): name = name.lower() if name.startswith('supybot.plugins.lookup.lookups.'): name = name[len('supybot.plugins.lookup.lookups.'):] if '.' in name: continue self.addRegistryValue(name, value) group = conf.supybot.plugins.Lookup.lookups for (name, value) in group.getValues(fullNames=False): name = name.lower() # Just in case. filename = value() try: self.addDatabase(name, filename) self.addCommand(name) except Exception, e: self.log.warning('Couldn\'t add lookup %s: %s', name, e) def _shrink(self, s): return utils.ellipsisify(s, 50) def die(self): self.dbHandler.die() def remove(self, irc, msg, args): """ Removes the lookup for . """ name = privmsgs.getArgs(args) name = callbacks.canonicalName(name) if name not in self.lookupDomains: irc.error('That\'s not a valid lookup to remove.') return db = self.dbHandler.getDb() cursor = db.cursor() try: cursor.execute("""DROP TABLE %s""" % name) db.commit() delattr(self.__class__, name) irc.replySuccess() except sqlite.DatabaseError: irc.error('No such lookup exists.') remove = privmsgs.checkCapability(remove, 'admin') _splitRe = re.compile(r'(? Adds a lookup for with the key/value pairs specified in the colon-delimited file specified by . is searched for in conf.supybot.directories.data. If is not singular, we try to make it singular before creating the command. """ (name, filename) = privmsgs.getArgs(args, required=2) name = utils.depluralize(name) name = callbacks.canonicalName(name) if hasattr(self, name): s = 'I already have a command in this plugin named %s' % name irc.error(s) return db = self.dbHandler.getDb() cursor = db.cursor() try: cursor.execute("""SELECT * FROM %s LIMIT 1""" % name) self.addCommand(name) except sqlite.DatabaseError: try: self.addDatabase(name, filename) except EnvironmentError, e: irc.error('Could not open %s: %s' % (filename, e.args[1])) return self.addCommand(name) self.addRegistryValue(name, filename) irc.replySuccess('Lookup %s added.' % name) add = privmsgs.checkCapability(add, 'admin') def addRegistryValue(self, name, filename): v = registry.String(filename, '') conf.supybot.plugins.Lookup.lookups.register(name, v) def addDatabase(self, name, filename): db = self.dbHandler.getDb() cursor = db.cursor() dataDir = conf.supybot.directories.data() filename = os.path.join(dataDir, filename) fd = file(filename) try: cursor.execute("""SELECT COUNT(*) FROM %s""" % name) except sqlite.DatabaseError: cursor.execute("CREATE TABLE %s (key TEXT, value TEXT)" % name) sql = "INSERT INTO %s VALUES (%%s, %%s)" % name for line in utils.nonCommentNonEmptyLines(fd): line = line.rstrip('\r\n') try: (key, value) = self._splitRe.split(line, 1) key = key.replace('\\:', ':') except ValueError: cursor.execute("""DROP TABLE %s""" % name) s = 'Invalid line in %s: %r' % (filename, line) raise callbacks.Error, s cursor.execute(sql, key, value) cursor.execute("CREATE INDEX %s_keys ON %s (key)" % (name, name)) db.commit() def addCommand(self, name): def f(self, irc, msg, args): args.insert(0, name) self._lookup(irc, msg, args) db = self.dbHandler.getDb() cursor = db.cursor() cursor.execute("""SELECT COUNT(*) FROM %s""" % name) rows = int(cursor.fetchone()[0]) docstring = """[] If is given, looks up in the %s database. Otherwise, returns a random key: value pair from the database. There are %s in the database. """ % (name, utils.nItems(name, rows)) f = utils.changeFunctionName(f, name, docstring) self.lookupDomains.add(name) setattr(self.__class__, name, f) _sqlTrans = string.maketrans('*?', '%_') def search(self, irc, msg, args): """[--{regexp}=] [--values] Searches the domain for lookups matching . If --regexp is given, its associated value is taken as a regexp and matched against the lookups. If --values is given, search the values rather than the keys. """ column = 'key' while '--values' in args: column = 'value' args.remove('--values') (options, rest) = getopt.getopt(args, '', ['regexp=']) (name, globs) = privmsgs.getArgs(rest, optional=1) db = self.dbHandler.getDb() criteria = [] formats = [] predicateName = 'p' for (option, arg) in options: if option == '--regexp': criteria.append('%s(%s)' % (predicateName, column)) try: r = utils.perlReToPythonRe(arg) except ValueError, e: irc.error('%r is not a valid regular expression' % arg) return def p(s, r=r): return int(bool(r.search(s))) db.create_function(predicateName, 1, p) predicateName += 'p' for glob in globs.split(): if '?' not in glob and '*' not in glob: glob = '*%s*' % glob criteria.append('%s LIKE %%s' % column) formats.append(glob.translate(self._sqlTrans)) if not criteria: raise callbacks.ArgumentError #print 'criteria: %s' % repr(criteria) #print 'formats: %s' % repr(formats) cursor = db.cursor() sql = """SELECT key, value FROM %s WHERE %s""" % \ (name, ' AND '.join(criteria)) #print 'sql: %s' % sql cursor.execute(sql, formats) if cursor.rowcount == 0: irc.reply('No entries in %s matched that query.' % name) else: lookups = ['%s: %s' % (item[0], self._shrink(item[1])) for item in cursor.fetchall()] irc.reply(utils.commaAndify(lookups)) def _lookup(self, irc, msg, args): """ Looks up the value of in the domain . """ (name, key) = privmsgs.getArgs(args, optional=1) db = self.dbHandler.getDb() cursor = db.cursor() if key: sql = """SELECT value FROM %s WHERE key LIKE %%s""" % name try: cursor.execute(sql, key) except sqlite.DatabaseError, e: if 'no such table' in str(e): irc.error('I don\'t have a domain %s' % name) else: irc.error(str(e)) return if cursor.rowcount == 0: irc.error('I couldn\'t find %s in %s.' % (key, name)) elif cursor.rowcount == 1: irc.reply(cursor.fetchone()[0]) else: values = [t[0] for t in cursor.fetchall()] irc.reply('%s could be %s' % (key, ', or '.join(values))) else: sql = """SELECT key, value FROM %s ORDER BY random() LIMIT 1""" % name try: cursor.execute(sql) except sqlite.DatabaseError, e: if 'no such table' in str(e): irc.error('I don\'t have a domain %r' % name) else: irc.error(str(e)) return (key, value) = cursor.fetchone() irc.reply('%s: %s' % (key, value)) Class = Lookup # vim:set shiftwidth=4 tabstop=8 expandtab textwidth=78: