From 0e9a3c326e97bbc0f9d8f71b1f057742359ce72e Mon Sep 17 00:00:00 2001 From: Jeremy Fincher Date: Wed, 19 Jan 2005 23:34:25 +0000 Subject: [PATCH] Added the Dict plugin in the new plugin format. --- plugins/Dict/README.txt | 1 + plugins/Dict/__init__.py | 54 +++++++ plugins/Dict/config.py | 50 ++++++ plugins/Dict/dictclient.py | 323 +++++++++++++++++++++++++++++++++++++ plugins/Dict/plugin.py | 129 +++++++++++++++ plugins/Dict/test.py | 47 ++++++ setup.py | 1 + 7 files changed, 605 insertions(+) create mode 100644 plugins/Dict/README.txt create mode 100644 plugins/Dict/__init__.py create mode 100644 plugins/Dict/config.py create mode 100644 plugins/Dict/dictclient.py create mode 100644 plugins/Dict/plugin.py create mode 100644 plugins/Dict/test.py diff --git a/plugins/Dict/README.txt b/plugins/Dict/README.txt new file mode 100644 index 000000000..d60b47a97 --- /dev/null +++ b/plugins/Dict/README.txt @@ -0,0 +1 @@ +Insert a description of your plugin here, with any notes, etc. about using it. diff --git a/plugins/Dict/__init__.py b/plugins/Dict/__init__.py new file mode 100644 index 000000000..eef9bb99f --- /dev/null +++ b/plugins/Dict/__init__.py @@ -0,0 +1,54 @@ +### +# Copyright (c) 2004, 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. +### + +""" +Commands that use the dictd protocol to define word. +""" + +import supybot +import supybot.world as world + +__author__ = supybot.authors.jemfinch + +# 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. + +if world.testing: + import test + +Class = plugin.Class +configure = config.configure + + +# vim:set shiftwidth=4 tabstop=8 expandtab textwidth=78: diff --git a/plugins/Dict/config.py b/plugins/Dict/config.py new file mode 100644 index 000000000..5a52befb1 --- /dev/null +++ b/plugins/Dict/config.py @@ -0,0 +1,50 @@ +### +# Copyright (c) 2004, 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. +### + +import supybot.conf as conf +import supybot.registry as registry + +def configure(advanced): + from supybot.questions import output, expect, anything, something, yn + conf.registerPlugin('Dict', True) + output('The default dictd server is dict.org.') + if yn('Would you like to specify a different dictd server?'): + server = something('What server?') + conf.supybot.plugins.Dict.server.set(server) + +Dict = conf.registerPlugin('Dict') +conf.registerGlobalValue(Dict, 'server', + registry.String('dict.org', """Determines what server the bot will + retrieve definitions from.""")) +conf.registerChannelValue(Dict, 'default', + registry.String('', """Determines the default dictionary the bot will + ask for definitions in. If this value is '*' (without the quotes) the bot + will use all dictionaries to define words.""")) + +# vim:set shiftwidth=4 tabstop=8 expandtab textwidth=78 diff --git a/plugins/Dict/dictclient.py b/plugins/Dict/dictclient.py new file mode 100644 index 000000000..f381d1d18 --- /dev/null +++ b/plugins/Dict/dictclient.py @@ -0,0 +1,323 @@ +# Client for the DICT protocol (RFC2229) +# +# Copyright (C) 2002 John Goerzen +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +# Retrieved from: http://gopher.quux.org:80/devel + +import socket, re + +version = '1.0' + +def dequote(s): + """Will remove single or double quotes from the start and end of a string + and return the result.""" + quotechars = "'\"" + while len(s) and s[0] in quotechars: + s = s[1:] + while len(s) and s[-1] in quotechars: + s = s[0:-1] + return s + +def enquote(s): + """This function will put a string in double quotes, properly + escaping any existing double quotes with a backslash. It will + return the result.""" + return '"%s"' % s.replace('"', "\\\"") + +class Connection: + """This class is used to establish a connection to a database server. + You will usually use this as the first call into the dictclient library. + Instantiating it takes two optional arguments: a hostname (a string) + and a port (an int). The hostname defaults to localhost + and the port to 2628, the port specified in RFC.""" + def __init__(self, hostname='localhost', port=2628): + self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + self.sock.connect((hostname, port)) + self.rfile = self.sock.makefile("rt") + self.wfile = self.sock.makefile("wt", 0) + self.saveconnectioninfo() + + def getresultcode(self): + """Generic function to get a result code. It will return a list + consisting of two items: the integer result code and the text + following. You will not usually use this function directly.""" + line = self.rfile.readline().strip() + code, text = line.split(' ', 1) + return [int(code), text] + + def get200result(self): + """Used when expecting a single line of text -- a 200-class + result. Returns [intcode, remaindertext]""" + + code, text = self.getresultcode() + if code < 200 or code >= 300: + raise Exception, "Got '%s' when 200-class response expected" % \ + line + return [code, text] + + def get100block(self): + """Used when expecting multiple lines of text -- gets the block + part only. Does not get any codes or anything! Returns a string.""" + data = [] + while 1: + line = self.rfile.readline().strip() + if line == '.': + break + data.append(line) + return "\n".join(data) + + def get100result(self): + """Used when expecting multiple lines of text, terminated by a period + and a 200 code. Returns: [initialcode, [bodytext_1lineperentry], + finalcode]""" + code, text = self.getresultcode() + if code < 100 or code >= 200: + raise Exception, "Got '%s' when 100-class response expected" % \ + code + + bodylines = self.get100block().split("\n") + + code2 = self.get200result()[0] + return [code, bodylines, code2] + + def get100dict(self): + """Used when expecting a dictionary of results. Will read from + the initial 100 code, to a period and the 200 code.""" + dict = {} + for line in self.get100result()[1]: + key, val = line.split(' ', 1) + dict[key] = dequote(val) + return dict + + def saveconnectioninfo(self): + """Called by __init__ to handle the initial connection. Will + save off the capabilities and messageid.""" + code, string = self.get200result() + assert code == 220 + m = re.search('<(.*)> (<.*>)$', string) + assert m is not None + capstr, msgid = m.groups() + self.capabilities = capstr.split('.') + self.messageid = msgid + + def getcapabilities(self): + """Returns a list of the capabilities advertised by the server.""" + return self.capabilities + + def getmessageid(self): + """Returns the message id, including angle brackets.""" + return self.messageid + + def getdbdescs(self): + """Gets a dict of available databases. The key is the db name + and the value is the db description. This command may generate + network traffic!""" + if hasattr(self, 'dbdescs'): + return self.dbdescs + + self.sendcommand("SHOW DB") + self.dbdescs = self.get100dict() + return self.dbdescs + + def getstratdescs(self): + """Gets a dict of available strategies. The key is the strat + name and the value is the strat description. This call may + generate network traffic!""" + if hasattr(self, 'stratdescs'): + return self.stratdescs + + self.sendcommand("SHOW STRAT") + self.stratdescs = self.get100dict() + return self.stratdescs + + def getdbobj(self, dbname): + """Gets a Database object corresponding to the database name passed + in. This function explicitly will *not* generate network traffic. + If you have not yet run getdbdescs(), it will fail.""" + if not hasattr(self, 'dbobjs'): + self.dbobjs = {} + + if self.dbobjs.has_key(dbname): + return self.dbobjs[dbname] + + # We use self.dbdescs explicitly since we don't want to + # generate net traffic with this request! + + if dbname != '*' and dbname != '!' and \ + not dbname in self.dbdescs.keys(): + raise Exception, "Invalid database name '%s'" % dbname + + self.dbobjs[dbname] = Database(self, dbname) + return self.dbobjs[dbname] + + def sendcommand(self, command): + """Takes a command, without a newline character, and sends it to + the server.""" + self.wfile.write(command + "\n") + + def define(self, database, word): + """Returns a list of Definition objects for each matching + definition. Parameters are the database name and the word + to look up. This is one of the main functions you will use + to interact with the server. Returns a list of Definition + objects. If there are no matches, an empty list is returned. + + Note: database may be '*' which means to search all databases, + or '!' which means to return matches from the first database that + has a match.""" + self.getdbdescs() # Prime the cache + + if database != '*' and database != '!' and \ + not database in self.getdbdescs(): + raise Exception, "Invalid database '%s' specified" % database + + self.sendcommand("DEFINE " + enquote(database) + " " + enquote(word)) + code = self.getresultcode()[0] + + retval = [] + + if code == 552: + # No definitions. + return [] + if code != 150: + raise Exception, "Unknown code %d" % code + + while 1: + code, text = self.getresultcode() + if code != 151: + break + + m = re.search('^"(.+)" (\S+)', text) + assert m is not None + resultword, resultdb = m.groups() + defstr = self.get100block() + retval.append(Definition(self, self.getdbobj(resultdb), + resultword, defstr)) + return retval + + def match(self, database, strategy, word): + """Gets matches for a query. Arguments are database name, + the strategy (see available ones in getstratdescs()), and the + pattern/word to look for. Returns a list of Definition objects. + If there is no match, an empty list is returned. + + Note: database may be '*' which means to search all databases, + or '!' which means to return matches from the first database that + has a match.""" + self.getstratdescs() # Prime the cache + self.getdbdescs() # Prime the cache + if not strategy in self.getstratdescs().keys(): + raise Exception, "Invalid strategy '%s'" % strategy + if database != '*' and database != '!' and \ + not database in self.getdbdescs().keys(): + raise Exception, "Invalid database name '%s'" % database + + self.sendcommand("MATCH %s %s %s" % (enquote(database), + enquote(strategy), + enquote(word))) + code = self.getresultcode()[0] + if code == 552: + # No Matches + return [] + if code != 152: + raise Exception, "Unexpected code %d" % code + + retval = [] + + for matchline in self.get100block().split("\n"): + matchdict, matchword = matchline.split(" ", 1) + retval.append(Definition(self, self.getdbobj(matchdict), + dequote(matchword))) + if self.getresultcode()[0] != 250: + raise Exception, "Unexpected end-of-list code %d" % code + return retval + +class Database: + """An object corresponding to a particular database in a server.""" + def __init__(self, dictconn, dbname): + """Initialize the object -- requires a Connection object and + a database name.""" + self.conn = dictconn + self.name = dbname + + def getname(self): + """Returns the short name for this database.""" + return self.name + + def getdescription(self): + if hasattr(self, 'description'): + return self.description + if self.getname() == '*': + self.description = 'All Databases' + elif self.getname() == '!': + self.description = 'First matching database' + else: + self.description = self.conn.getdbdescs()[self.getname()] + return self.description + + def getinfo(self): + """Returns a string of info describing this database.""" + if hasattr(self, 'info'): + return self.info + + if self.getname() == '*': + self.info = "This special database will search all databases on the system." + elif self.getname() == '!': + self.info = "This special database will return matches from the first matching database." + else: + self.conn.sendcommand("SHOW INFO " + self.name) + self.info = "\n".join(self.conn.get100result()[1]) + return self.info + + def define(self, word): + """Get a definition from within this database. + The argument, word, is the word to look up. The return value is the + same as from Connection.define().""" + return self.conn.define(self.getname(), word) + + def match(self, strategy, word): + """Get a match from within this database. + The argument, word, is the word to look up. The return value is + the same as from Connection.define().""" + return self.conn.match(self.getname(), strategy, word) + +class Definition: + """An object corresponding to a single definition.""" + def __init__(self, dictconn, db, word, defstr = None): + """Instantiate the object. Requires: a Connection object, + a Database object (NOT corresponding to '*' or '!' databases), + a word. Optional: a definition string. If not supplied, + it will be fetched if/when it is requested.""" + self.conn = dictconn + self.db = db + self.word = word + self.defstr = defstr + + def getdb(self): + """Get the Database object corresponding to this definition.""" + return self.db + + def getdefstr(self): + """Get the definition string (the actual content) of this + definition.""" + if not self.defstr: + self.defstr = self.conn.define(self.getdb().getname(), self.word)[0].getdefstr() + return self.defstr + + def getword(self): + """Get the word this object describes.""" + return self.word diff --git a/plugins/Dict/plugin.py b/plugins/Dict/plugin.py new file mode 100644 index 000000000..8dabba905 --- /dev/null +++ b/plugins/Dict/plugin.py @@ -0,0 +1,129 @@ +### +# Copyright (c) 2002-2004, 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. +### + +import sets +import random +import socket + +import dictclient + +import supybot.conf as conf +import supybot.utils as utils +from supybot.commands import * +import supybot.ircutils as ircutils +import supybot.registry as registry +import supybot.webutils as webutils +import supybot.callbacks as callbacks + +class Dict(callbacks.Privmsg): + threaded = True + def dictionaries(self, irc, msg, args): + """takes no arguments + + Returns the dictionaries valid for the dict command. + """ + try: + server = conf.supybot.plugins.Dict.server() + conn = dictclient.Connection(server) + dbs = conn.getdbdescs().keys() + dbs.sort() + irc.reply(utils.commaAndify(dbs)) + except socket.error, e: + irc.error(webutils.strError(e)) + dictionaries = wrap(dictionaries) + + def random(self, irc, msg, args): + """takes no arguments + + Returns a random valid dictionary. + """ + try: + server = conf.supybot.plugins.Dict.server() + conn = dictclient.Connection(server) + dbs = conn.getdbdescs().keys() + irc.reply(random.choice(dbs)) + except socket.error, e: + irc.error(webutils.strError(e)) + random = wrap(random) + + def dict(self, irc, msg, args, words): + """[] + + Looks up the definition of on dict.org's dictd server. + """ + try: + server = conf.supybot.plugins.Dict.server() + conn = dictclient.Connection(server) + except socket.error, e: + irc.error(webutils.strError(e), Raise=True) + dbs = sets.Set(conn.getdbdescs()) + if words[0] in dbs: + dictionary = words.pop(0) + else: + default = self.registryValue('default', msg.args[0]) + if default in dbs: + dictionary = default + else: + if default: + self.log.info('Default dict for %s is not a supported ' + 'dictionary: %s.', msg.args[0], default) + dictionary = '*' + if not words: + irc.error('You must give a word to define.', Raise=True) + word = ' '.join(words) + definitions = conn.define(dictionary, word) + dbs = sets.Set() + if not definitions: + if dictionary == '*': + irc.reply('No definition for %s could be found.' % + utils.quoted(word)) + else: + irc.reply('No definition for %s could be found in %s' % + (utils.quoted(word), ircutils.bold(dictionary))) + return + L = [] + for d in definitions: + dbs.add(ircutils.bold(d.getdb().getname())) + (db, s) = (d.getdb().getname(), d.getdefstr()) + db = ircutils.bold(db) + s = utils.normalizeWhitespace(s).rstrip(';.,') + L.append('%s: %s' % (db, s)) + utils.sortBy(len, L) + if dictionary == '*' and len(dbs) > 1: + s = '%s responded: %s' % (utils.commaAndify(dbs), '; '.join(L)) + else: + s = '; '.join(L) + irc.reply(s) + dict = wrap(dict, [many('something')]) + + +Class = Dict + + +# vim:set shiftwidth=4 tabstop=8 expandtab textwidth=78: diff --git a/plugins/Dict/test.py b/plugins/Dict/test.py new file mode 100644 index 000000000..cdc38ad60 --- /dev/null +++ b/plugins/Dict/test.py @@ -0,0 +1,47 @@ +### +# Copyright (c) 2002-2004, 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. +### + +from supybot.test import * + +class DictTestCase(PluginTestCase): + plugins = ('Dict',) + if network: + def testDict(self): + self.assertNotError('dict slash') + self.assertNotRegexp('dict web1913 slash', 'foldoc') + self.assertError('dict ""') + + def testDictionaries(self): + self.assertNotError('dictionaries') + + def testRandomDictionary(self): + self.assertNotError('random') + self.assertNotError('dict [random] moo') + +# vim:set shiftwidth=4 tabstop=8 expandtab textwidth=78: diff --git a/setup.py b/setup.py index 93aab918f..1077884d7 100644 --- a/setup.py +++ b/setup.py @@ -33,6 +33,7 @@ plugins = [ 'Admin', 'Channel', 'Config', + 'Dict', 'Math', 'Misc', 'Owner',