From 9693685f62ca7360bb5515ab4273cf617f19d542 Mon Sep 17 00:00:00 2001 From: James Vega Date: Tue, 10 May 2005 03:02:15 +0000 Subject: [PATCH] Add News in the new plugin format. --- plugins/News/README.txt | 2 + plugins/News/__init__.py | 64 +++++++++++ plugins/News/config.py | 48 ++++++++ plugins/News/plugin.py | 231 +++++++++++++++++++++++++++++++++++++++ plugins/News/test.py | 87 +++++++++++++++ 5 files changed, 432 insertions(+) create mode 100644 plugins/News/README.txt create mode 100644 plugins/News/__init__.py create mode 100644 plugins/News/config.py create mode 100644 plugins/News/plugin.py create mode 100644 plugins/News/test.py diff --git a/plugins/News/README.txt b/plugins/News/README.txt new file mode 100644 index 000000000..cf5e48f6c --- /dev/null +++ b/plugins/News/README.txt @@ -0,0 +1,2 @@ +This plugin provides a means of maintaining News for a channel. It was +partially inspired by the news system used on #debian's bot. diff --git a/plugins/News/__init__.py b/plugins/News/__init__.py new file mode 100644 index 000000000..8ce384fdd --- /dev/null +++ b/plugins/News/__init__.py @@ -0,0 +1,64 @@ +### +# 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. +### + +""" +A module to allow each channel to have "news". News items may have expiration +dates. +""" + +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__ = "%%VERSION%%" + +__author__ = supybot.authors.strike + +# This is a dictionary mapping supybot.Author instances to lists of +# contributions. +__contributors__ = {} + +# This is a url where the most recent plugin package can be downloaded. +__url__ = '' # 'http://supybot.com/Members/yourname/News/download' + +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=4 expandtab textwidth=79: diff --git a/plugins/News/config.py b/plugins/News/config.py new file mode 100644 index 000000000..0a5fe5dee --- /dev/null +++ b/plugins/News/config.py @@ -0,0 +1,48 @@ +### +# 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('News', True) + + +News = conf.registerPlugin('News') +# This is where your configuration variables (if any) should go. For example: +# conf.registerGlobalValue(News, 'someConfigVariableName', +# registry.Boolean(False, """Help for someConfigVariableName.""")) + + +# vim:set shiftwidth=4 tabstop=4 expandtab textwidth=79: diff --git a/plugins/News/plugin.py b/plugins/News/plugin.py new file mode 100644 index 000000000..058568a32 --- /dev/null +++ b/plugins/News/plugin.py @@ -0,0 +1,231 @@ +### +# 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 time + +import supybot.dbi as dbi +import supybot.conf as conf +import supybot.utils as utils +from supybot.commands import * +import supybot.plugins as plugins +import supybot.ircutils as ircutils +import supybot.callbacks as callbacks + + +class DbiNewsDB(plugins.DbiChannelDB): + class DB(dbi.DB): + class Record(dbi.Record): + __fields__ = [ + 'subject', + 'text', + 'at', + 'expires', + 'by', + ] + + def __str__(self): + user = plugins.getUserName(self.by) + if self.expires == 0: + s = format('%s (Subject: %q, added by %s on %s)', + self.text, self.subject, self.by, + utils.str.timestamp(self.at)) + else: + s = format('%s (Subject: %q, added by %s on %s, ' + 'expires at %s)', + self.text, self.subject, user, + utils.str.timestamp(self.at), + utils.str.timestamp(self.expires)) + return s + + def __init__(self, filename): + # We use self.__class__ here because apparently DB isn't in our + # scope. python-- + self.__parent = super(self.__class__, self) + self.__parent.__init__(filename) + + def add(self, subject, text, at, expires, by): + return self.__parent.add(self.Record(at=at, by=by, text=text, + subject=subject, expires=expires)) + + def getOld(self, id=None): + now = time.time() + if id: + return self.get(id) + else: + L = [R for R in self if R.expires < now and R.expires != 0] + if not L: + raise dbi.NoRecordError + else: + return L + + def get(self, id=None): + now = time.time() + if id: + return self.__parent.get(id) + else: + L = [R for R in self if R.expires >= now or R.expires == 0] + if not L: + raise dbi.NoRecordError + return L + + def change(self, id, f): + news = self.get(id) + s = '%s: %s' % (news.subject, news.text) + s = f(s) + (news.subject, news.text) = s.split(': ', 1) + self.set(id, news) + +NewsDB = plugins.DB('News', {'flat': DbiNewsDB}) + +class News(callbacks.Plugin): + def __init__(self, irc): + self.__parent = super(News, self) + self.__parent.__init__(irc) + self.db = NewsDB() + + def die(self): + self.__parent.die() + self.db.close() + + def add(self, irc, msg, args, channel, user, at, expires, news): + """[] : + + Adds a given news item of to a channel with the given . + If isn't 0, that news item will expire seconds from + now. is only necessary if the message isn't sent in the + channel itself. + """ + try: + (subject, text) = news.split(': ', 1) + except ValueError: + raise callbacks.ArgumentError + id = self.db.add(channel, subject, text, at, expires, user.id) + irc.replySuccess(format('(News item #%i added)', id)) + add = wrap(add, ['channeldb', 'user', 'now', 'expiry', 'text']) + + def news(self, irc, msg, args, channel, id): + """[] [] + + Display the news items for in the format of '(#id) subject'. + If is given, retrieve only that news item; otherwise retrieve all + news items. is only necessary if the message isn't sent in + the channel itself. + """ + if not id: + try: + records = self.db.get(channel) + items = [format('(#%i) %s', R.id, R.subject) for R in records] + s = format('News for %s: %s', channel, '; '.join(items)) + irc.reply(s) + except dbi.NoRecordError: + irc.reply(format('No news for %s.', channel)) + else: + try: + if id < 1: + raise ValueError + record = self.db.get(channel, id) + irc.reply(str(record)) + except dbi.NoRecordError, id: + irc.errorInvalid('news item id', id) + except ValueError: + irc.errorInvalid('news item id', id, + ' must be a positive integer.') + news = wrap(news, ['channeldb', additional('int')]) + + def remove(self, irc, msg, args, channel, id): + """[] + + Removes the news item with from . is only + necessary if the message isn't sent in the channel itself. + """ + try: + if id < 1: + raise ValueError + self.db.remove(channel, id) + irc.replySuccess() + except dbi.NoRecordError: + irc.errorInvalid('news item id', id) + except ValueError: + irc.errorInvalid('news item id', id, + ' must be a positive integer.') + remove = wrap(remove, ['channeldb', 'int']) + + def change(self, irc, msg, args, channel, id, replacer): + """[] + + Changes the news item with from according to the + regular expression . should be of the form + s/text/replacement/flags. is only necessary if the message + isn't sent on the channel itself. + """ + try: + if id < 1: + raise ValueError + self.db.change(channel, id, replacer) + irc.replySuccess() + except dbi.NoRecordError: + irc.errorInvalid('news item id', id) + except ValueError: + irc.errorInvalid('news item id', id, + ' must be a positive integer.') + change = wrap(change, ['channeldb', 'int', 'regexpReplacer']) + + def old(self, irc, msg, args, channel, id): + """[] [] + + Returns the old news item for with . If no number is + given, returns all the old news items in reverse order. is + only necessary if the message isn't sent in the channel itself. + """ + if id: + try: + if id < 1: + raise ValueError + record = self.db.getOld(channel, id) + irc.reply(str(record)) + except dbi.NoRecordError, id: + irc.errorInvalid('news item id', id) + except ValueError: + irc.errorInvalid('news item id', id, + ' must be a positive integer.') + else: + try: + records = self.db.getOld(channel) + items = [format('(#%i) %s', R.id, R.subject) for R in records] + s = format('Old news for %s: %s', channel, '; '.join(items)) + irc.reply(s) + except dbi.NoRecordError: + irc.reply(format('No old news for %s.', channel)) + old = wrap(old, ['channeldb', additional('int')]) + + +Class = News + + +# vim:set shiftwidth=4 tabstop=4 expandtab textwidth=79: diff --git a/plugins/News/test.py b/plugins/News/test.py new file mode 100644 index 000000000..c29d3d2ce --- /dev/null +++ b/plugins/News/test.py @@ -0,0 +1,87 @@ +### +# 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 time + +from supybot.test import * + +class NewsTestCase(ChannelPluginTestCase): + plugins = ('News','User') + def setUp(self): + ChannelPluginTestCase.setUp(self) + # Create a valid user to use + self.prefix = 'news!bar@baz' + self.irc.feedMsg(ircmsgs.privmsg(self.nick, 'register tester moo', + prefix=self.prefix)) + m = self.irc.takeMsg() # Response to register. + + def testAddnews(self): + self.assertNotError('add 0 subject: foo') + self.assertRegexp('news', 'subject') + self.assertNotError('add 0 subject2: foo2') + self.assertRegexp('news', 'subject.*subject2') + self.assertNotError('add 5 subject3: foo3') + self.assertRegexp('news', 'subject3') + print + print 'Sleeping to expire the news item (testAddnews)' + time.sleep(6) + print 'Done sleeping.' + self.assertNotRegexp('news', 'subject3') + + def testNews(self): + # These should both fail first, as they will have nothing in the DB + self.assertRegexp('news', 'no news') + self.assertRegexp('news #channel', 'no news') + # Now we'll add news and make sure listnews doesn't fail + self.assertNotError('add #channel 0 subject: foo') + self.assertNotError('news #channel') + self.assertNotError('add 0 subject: foo') + self.assertRegexp('news', '#1') + self.assertNotError('news 1') + + def testChangenews(self): + self.assertNotError('add 0 Foo: bar') + self.assertNotError('change 1 s/bar/baz/') + self.assertNotRegexp('news 1', 'bar') + self.assertRegexp('news 1', 'baz') + + def testOldnews(self): + self.assertRegexp('old', 'No old news') + self.assertNotError('add 0 a: b') + self.assertRegexp('old', 'No old news') + self.assertNotError('add 5 foo: bar') + self.assertRegexp('old', 'No old news') + print + print 'Sleeping to expire the news item (testOldnews)' + time.sleep(6) + print 'Done sleeping.' + self.assertNotError('old') + + +# vim:set shiftwidth=4 tabstop=4 expandtab textwidth=79: