diff --git a/plugins/MessageParser/plugin.py b/plugins/MessageParser/plugin.py index d72516696..45385d878 100644 --- a/plugins/MessageParser/plugin.py +++ b/plugins/MessageParser/plugin.py @@ -78,7 +78,7 @@ class MessageParser(callbacks.Plugin, plugins.ChannelDBHandler): def __init__(self, irc): callbacks.Plugin.__init__(self, irc) plugins.ChannelDBHandler.__init__(self) - + def makeDb(self, filename): """Create the database and connect to it.""" if os.path.exists(filename): @@ -99,7 +99,7 @@ class MessageParser(callbacks.Plugin, plugins.ChannelDBHandler): )""") db.commit() return db - + # override this because sqlite3 doesn't have autocommit # use isolation_level instead. def getDb(self, channel): @@ -113,7 +113,7 @@ class MessageParser(callbacks.Plugin, plugins.ChannelDBHandler): db = self.dbCache[channel] db.isolation_level = None return db - + def _updateRank(self, channel, regexp): if self.registryValue('keepRankInfo', channel): db = self.getDb(channel) @@ -124,15 +124,15 @@ class MessageParser(callbacks.Plugin, plugins.ChannelDBHandler): old_count = cursor.fetchall()[0][0] cursor.execute("UPDATE triggers SET usage_count=? WHERE regexp=?", (old_count + 1, regexp,)) db.commit() - + def _runCommandFunction(self, irc, msg, command): """Run a command from message, as if command was sent over IRC.""" - tokens = callbacks.tokenize(command) + tokens = callbacks.tokenize(command) try: self.Proxy(irc.irc, msg, tokens) except Exception, e: log.exception('Uncaught exception in function called by MessageParser:') - + def _checkManageCapabilities(self, irc, msg, channel): """Check if the user has any of the required capabilities to manage the regexp database.""" @@ -147,7 +147,7 @@ class MessageParser(callbacks.Plugin, plugins.ChannelDBHandler): return False else: return True - + def doPrivmsg(self, irc, msg): channel = msg.args[0] if not irc.isChannel(channel): @@ -170,17 +170,17 @@ class MessageParser(callbacks.Plugin, plugins.ChannelDBHandler): for (i, j) in enumerate(match.groups()): thisaction = re.sub(r'\$' + str(i+1), match.group(i+1), thisaction) actions.append(thisaction) - + for action in actions: self._runCommandFunction(irc, msg, action) - + @internationalizeDocstring def add(self, irc, msg, args, channel, regexp, action): """[] Associates with . is only necessary if the message isn't sent on the channel - itself. Action is echoed upon regexp match, with variables $1, $2, + itself. Action is echoed upon regexp match, with variables $1, $2, etc. being interpolated from the regexp match groups.""" if not self._checkManageCapabilities(irc, msg, channel): capabilities = self.registryValue('requireManageCapability') @@ -213,12 +213,12 @@ class MessageParser(callbacks.Plugin, plugins.ChannelDBHandler): irc.error(_('That trigger is locked.')) return add = wrap(add, ['channel', 'something', 'something']) - + @internationalizeDocstring def remove(self, irc, msg, args, channel, optlist, regexp): """[] [--id] ] - Removes the trigger for from the triggers database. + Removes the trigger for from the triggers database. is only necessary if the message isn't sent in the channel itself. If option --id specified, will retrieve by regexp id, not content. @@ -240,11 +240,11 @@ class MessageParser(callbacks.Plugin, plugins.ChannelDBHandler): else: irc.error(_('There is no such regexp trigger.')) return - + if locked: irc.error(_('This regexp trigger is locked.')) return - + cursor.execute("""DELETE FROM triggers WHERE id=?""", (id,)) db.commit() irc.replySuccess() @@ -303,7 +303,7 @@ class MessageParser(callbacks.Plugin, plugins.ChannelDBHandler): """[] [--id] Looks up the value of in the triggers database. - is only necessary if the message isn't sent in the channel + is only necessary if the message isn't sent in the channel itself. If option --id specified, will retrieve by regexp id, not content. """ @@ -321,9 +321,9 @@ class MessageParser(callbacks.Plugin, plugins.ChannelDBHandler): else: irc.error(_('There is no such regexp trigger.')) return - + irc.reply("The action for regexp trigger \"%s\" is \"%s\"" % (regexp, action)) - show = wrap(show, ['channel', + show = wrap(show, ['channel', getopts({'id': '',}), 'something']) @@ -332,7 +332,7 @@ class MessageParser(callbacks.Plugin, plugins.ChannelDBHandler): """[] [--id] Display information about in the triggers database. - is only necessary if the message isn't sent in the channel + is only necessary if the message isn't sent in the channel itself. If option --id specified, will retrieve by regexp id, not content. """ @@ -346,23 +346,23 @@ class MessageParser(callbacks.Plugin, plugins.ChannelDBHandler): cursor.execute(sql, (regexp,)) results = cursor.fetchall() if len(results) != 0: - (id, regexp, added_by, added_at, usage_count, + (id, regexp, added_by, added_at, usage_count, action, locked) = results[0] else: irc.error(_('There is no such regexp trigger.')) return - + irc.reply(_("The regexp id is %d, regexp is \"%s\", and action is" " \"%s\". It was added by user %s on %s, has been " "triggered %d times, and is %s.") % (id, - regexp, + regexp, action, added_by, time.strftime(conf.supybot.reply.format.time(), time.localtime(int(added_at))), usage_count, locked and _("locked") or _("not locked"),)) - info = wrap(info, ['channel', + info = wrap(info, ['channel', getopts({'id': '',}), 'something']) @@ -371,7 +371,7 @@ class MessageParser(callbacks.Plugin, plugins.ChannelDBHandler): """[] Lists regexps present in the triggers database. - is only necessary if the message isn't sent in the channel + is only necessary if the message isn't sent in the channel itself. Regexp ID listed in paretheses. """ db = self.getDb(channel) @@ -383,7 +383,7 @@ class MessageParser(callbacks.Plugin, plugins.ChannelDBHandler): else: irc.reply(_('There are no regexp triggers in the database.')) return - + s = [ "\"%s\" (%d)" % (regexp[0], regexp[1]) for regexp in regexps ] separator = self.registryValue('listSeparator', channel) irc.reply(separator.join(s)) @@ -392,10 +392,10 @@ class MessageParser(callbacks.Plugin, plugins.ChannelDBHandler): @internationalizeDocstring def rank(self, irc, msg, args, channel): """[] - - Returns a list of top-ranked regexps, sorted by usage count - (rank). The number of regexps returned is set by the - rankListLength registry value. is only necessary if the + + Returns a list of top-ranked regexps, sorted by usage count + (rank). The number of regexps returned is set by the + rankListLength registry value. is only necessary if the message isn't sent in the channel itself. """ numregexps = self.registryValue('rankListLength', channel) @@ -416,12 +416,12 @@ class MessageParser(callbacks.Plugin, plugins.ChannelDBHandler): @internationalizeDocstring def vacuum(self, irc, msg, args, channel): """[] - + Vacuums the database for . See SQLite vacuum doc here: http://www.sqlite.org/lang_vacuum.html - is only necessary if the message isn't sent in + is only necessary if the message isn't sent in the channel itself. - First check if user has the required capability specified in plugin + First check if user has the required capability specified in plugin config requireVacuumCapability. """ capability = self.registryValue('requireVacuumCapability') diff --git a/plugins/PluginDownloader/README.txt b/plugins/PluginDownloader/README.txt new file mode 100644 index 000000000..d60b47a97 --- /dev/null +++ b/plugins/PluginDownloader/README.txt @@ -0,0 +1 @@ +Insert a description of your plugin here, with any notes, etc. about using it. diff --git a/plugins/PluginDownloader/__init__.py b/plugins/PluginDownloader/__init__.py new file mode 100644 index 000000000..7d92643b3 --- /dev/null +++ b/plugins/PluginDownloader/__init__.py @@ -0,0 +1,66 @@ +### +# Copyright (c) 2011, Valentin Lorentz +# 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. + +### + +""" +Add a description of the plugin (to be presented to the user inside the wizard) +here. This should describe *what* the plugin does. +""" + +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__ = "" + +# XXX Replace this with an appropriate author or supybot.Author instance. +__author__ = supybot.authors.progval + +# 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/PluginDownloader/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/PluginDownloader/config.py b/plugins/PluginDownloader/config.py new file mode 100644 index 000000000..0a8db927f --- /dev/null +++ b/plugins/PluginDownloader/config.py @@ -0,0 +1,52 @@ +### +# Copyright (c) 2011, Valentin Lorentz +# 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 +from supybot.i18n import PluginInternationalization, internationalizeDocstring + +_ = PluginInternationalization('PluginDownloader') + +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('PluginDownloader', True) + + +PluginDownloader = conf.registerPlugin('PluginDownloader') +# This is where your configuration variables (if any) should go. For example: +# conf.registerGlobalValue(PluginDownloader, 'someConfigVariableName', +# registry.Boolean(False, _("""Help for someConfigVariableName."""))) + + +# vim:set shiftwidth=4 tabstop=4 expandtab textwidth=79: diff --git a/plugins/PluginDownloader/local/__init__.py b/plugins/PluginDownloader/local/__init__.py new file mode 100644 index 000000000..e86e97b86 --- /dev/null +++ b/plugins/PluginDownloader/local/__init__.py @@ -0,0 +1 @@ +# Stub so local is a module, used for third-party modules diff --git a/plugins/PluginDownloader/plugin.py b/plugins/PluginDownloader/plugin.py new file mode 100644 index 000000000..756707c03 --- /dev/null +++ b/plugins/PluginDownloader/plugin.py @@ -0,0 +1,271 @@ +### +# Copyright (c) 2011, Valentin Lorentz +# 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 json +import urllib +import urllib2 +import tarfile +from cStringIO import StringIO + + +import supybot.log as log +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 +from supybot.i18n import PluginInternationalization, internationalizeDocstring + +_ = PluginInternationalization('PluginDownloader') + +class Repository: + pass + +class VersionnedRepository(Repository): + pass + +class GitRepository(VersionnedRepository): + pass + +class GithubRepository(GitRepository): + def __init__(self, username, reponame, path='/'): + self._username = username + self._reponame = reponame + if not path.startswith('/'): + path = '/' + path + if not path.endswith('/'): + path += '/' + self._path = path + + self._downloadUrl = 'https://github.com/%s/%s/tarball/master' % \ + ( + self._username, + self._reponame, + ) + + + _apiUrl = 'http://github.com/api/v2/json' + def _query(self, type_, uri_end, args={}): + args = dict([(x,y) for x,y in args.items() if y is not None]) + url = '%s/%s/%s?%s' % (self._apiUrl, type_, uri_end, + urllib.urlencode(args)) + return json.load(utils.web.getUrlFd(url)) + + def getPluginList(self): + latestCommit = self._query( + 'repos', + 'show/%s/%s/branches' % ( + self._username, + self._reponame, + ) + )['branches']['master'] + path = [x for x in self._path.split('/') if x != ''] + treeHash = self._navigate(latestCommit, path) + if treeHash is None: + log.error(( + 'Cannot get plugins list from repository %s/%s ' + 'at Github' + ) % (self._username, self._reponame)) + return [] + nodes = self._query( + 'tree', + 'show/%s/%s/%s' % ( + self._username, + self._reponame, + treeHash, + ) + )['tree'] + plugins = [x['name'] for x in nodes if x['type'] == 'tree'] + return plugins + + def _navigate(self, treeHash, path): + if path == []: + return treeHash + tree = self._query( + 'tree', + 'show/%s/%s/%s' % ( + self._username, + self._reponame, + treeHash, + ) + )['tree'] + nodeName = path.pop(0) + for node in tree: + if node['name'] != nodeName: + continue + if node['type'] != 'tree': + return None + else: + return self._navigate(node['sha'], path) + # Remember we pop(0)ed the path + return None + + def install(self, plugin): + directories = conf.supybot.directories.plugins() + directory = self._getWritableDirectoryFromList(directories) + assert directory is not None + dirname = ''.join((self._path, plugin)) + + fileObject = urllib2.urlopen(self._downloadUrl) + fileObject2 = StringIO() + fileObject2.write(fileObject.read()) + fileObject.close() + fileObject2.seek(0) + archive = tarfile.open(fileobj=fileObject2, mode='r:gz') + prefix = archive.getnames()[0] + try: + assert archive.getmember(prefix + dirname).isdir() + + for file in archive.getmembers(): + if file.name.startswith(prefix + dirname): + extractedFile = archive.extractfile(file) + newFileName = os.path.join(*file.name.split('/')[1:]) + newFileName = newFileName[len(self._path)-1:] + newFileName = os.path.join(directory, newFileName) + if extractedFile is None: + os.mkdir(newFileName) + else: + open(newFileName, 'a').write(extractedFile.read()) + finally: + archive.close() + fileObject2.close() + del archive, fileObject, fileObject2 + + def _getWritableDirectoryFromList(self, directories): + for directory in directories: + if os.access(directory, os.W_OK): + return directory + return None + + +repositories = { + 'ProgVal': GithubRepository( + 'ProgVal', + 'Supybot-plugins' + ), + 'quantumlemur': GithubRepository( + 'quantumlemur', + 'Supybot-plugins', + ), + 'stepnem': GithubRepository( + 'stepnem', + 'supybot-plugins', + ), + 'gsf-snapshot': GithubRepository( + 'gsf', + 'supybot-plugins', + 'Supybot-plugins-20060723', + ), + 'gsf-edsu': GithubRepository( + 'gsf', + 'supybot-plugins', + 'edsu-plugins', + ), + 'gsf': GithubRepository( + 'gsf', + 'supybot-plugins', + 'plugins', + ), + 'nanotube-bitcoin': GithubRepository( + 'nanotube', + 'supybot-bitcoin-' + 'marketmonitor', + ), + 'mtughan-weather': GithubRepository( + 'mtughan', + 'Supybot-Weather', + ), + 'SpiderDave': GithubRepository( + 'SpiderDave', + 'spidey-supybot-plugins', + 'Plugins', + ), + } + +class PluginDownloader(callbacks.Plugin): + """Add the help for "@plugin help PluginDownloader" here + This should describe *how* to use this plugin.""" + + @internationalizeDocstring + def repolist(self, irc, msg, args, repository): + """[] + + Displays the list of plugins in the . + If is not given, returns a list of available + repositories.""" + + global repositories + if repository is None: + irc.reply(_(', ').join([x for x in repositories])) + elif repository not in repositories: + irc.error(_( + 'This repository does not exist or is not known by ' + 'this bot.' + )) + else: + plugins = repositories[repository].getPluginList() + if plugins == []: + irc.error(_('No plugin found in this repository.')) + else: + irc.reply(_(', ').join([x for x in plugins])) + repolist = wrap(repolist, [optional('something')]) + + @internationalizeDocstring + def install(self, irc, msg, args, repository, plugin): + """ + + Downloads and installs the from the .""" + global repositories + if repository not in repositories: + irc.error(_( + 'This repository does not exist or is not known by ' + 'this bot.' + )) + elif plugin not in repositories[repository].getPluginList(): + irc.error(_('This plugin does not exist in this repository.')) + else: + try: + repositories[repository].install(plugin) + irc.replySuccess() + except Exception as e: + #FIXME: more detailed error message + log.error(str(e)) + irc.error('The plugin could not be installed.') + + install = wrap(install, ['owner', 'something', 'something']) + + +PluginDownloader = internationalizeDocstring(PluginDownloader) +Class = PluginDownloader + + +# vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: diff --git a/plugins/PluginDownloader/test.py b/plugins/PluginDownloader/test.py new file mode 100644 index 000000000..91da9a362 --- /dev/null +++ b/plugins/PluginDownloader/test.py @@ -0,0 +1,112 @@ +### +# Copyright (c) 2011, Valentin Lorentz +# 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 shutil + +from supybot.test import * + +pluginsPath = '%s/test-plugins' % os.getcwd() + +class PluginDownloaderTestCase(PluginTestCase): + plugins = ('PluginDownloader',) + config = {'supybot.directories.plugins': [pluginsPath]} + + def setUp(self): + PluginTestCase.setUp(self) + try: + shutil.rmtree(pluginsPath) + except: + pass + os.mkdir(pluginsPath) + + def tearDown(self): + try: + shutil.rmtree(pluginsPath) + finally: + PluginTestCase.tearDown(self) + + def _testPluginInstalled(self, name): + assert os.path.isdir(pluginsPath + '/%s/' % name) + assert os.path.isfile(pluginsPath + '/%s/plugin.py' % name) + assert os.path.isfile(pluginsPath + '/%s/config.py' % name) + + def testRepolist(self): + self.assertRegexp('repolist', '(.*, )?ProgVal(, .*)?') + self.assertRegexp('repolist', '(.*, )?quantumlemur(, .*)?') + self.assertRegexp('repolist ProgVal', '(.*, )?AttackProtector(, .*)?') + + def testInstallProgVal(self): + self.assertError('plugindownloader install ProgVal Listener') + self.assertNotError('plugindownloader install ProgVal AttackProtector') + self.assertError('plugindownloader install ProgVal Listener') + self._testPluginInstalled('AttackProtector') + + def testInstallQuantumlemur(self): + self.assertError('plugindownloader install quantumlemur AttackProtector') + self.assertNotError('plugindownloader install quantumlemur Listener') + self.assertError('plugindownloader install quantumlemur AttackProtector') + self._testPluginInstalled('Listener') + + def testInstallStepnem(self): + self.assertNotError('plugindownloader install stepnem Freenode') + self._testPluginInstalled('Freenode') + + def testInstallGsf(self): + self.assertNotError('plugindownloader install gsf-snapshot Debian') + self._testPluginInstalled('Debian') + self.assertError('plugindownloader install gsf-snapshot Anagram') + self.assertError('plugindownloader install gsf-snapshot Acronym') + + self.assertNotError('plugindownloader install gsf-edsu Anagram') + self._testPluginInstalled('Anagram') + self.assertError('plugindownloader install gsf-edsu Debian') + self.assertError('plugindownloader install gsf-edsu Acronym') + + self.assertNotError('plugindownloader install gsf Acronym') + self._testPluginInstalled('Acronym') + self.assertError('plugindownloader install gsf Anagram') + self.assertError('plugindownloader install gsf Debian') + + def testInstallNanotubeBitcoin(self): + self.assertNotError('plugindownloader install nanotube-bitcoin GPG') + self._testPluginInstalled('GPG') + + def testInstallMtughanWeather(self): + self.assertNotError('plugindownloader install mtughan-weather ' + 'WunderWeather') + self._testPluginInstalled('WunderWeather') + + def testInstallSpiderDave(self): + self.assertNotError('plugindownloader install SpiderDave Pastebin') + self._testPluginInstalled('Pastebin') + + +# vim:set shiftwidth=4 tabstop=4 expandtab textwidth=79: diff --git a/plugins/Seen/plugin.py b/plugins/Seen/plugin.py index 7f1fbd718..565a28c54 100644 --- a/plugins/Seen/plugin.py +++ b/plugins/Seen/plugin.py @@ -222,10 +222,10 @@ class Seen(callbacks.Plugin): Returns the last time was seen and what was last seen saying. is only necessary if the message isn't sent on the - channel itself. + channel itself. may contain * as a wildcard. """ self._seen(irc, channel, name) - seen = wrap(seen, ['channel', 'nick']) + seen = wrap(seen, ['channel', 'something']) @internationalizeDocstring def any(self, irc, msg, args, channel, optlist, name): diff --git a/src/__init__.py b/src/__init__.py index 932094d1a..e4b25e2fb 100644 --- a/src/__init__.py +++ b/src/__init__.py @@ -32,7 +32,12 @@ import sys import os.path import dynamicScope -import supybot.utils as utils +try: + import supybot.utils as utils +except ImportError: # We are running setup.py + import src + sys.modules['supybot'] = src + import src.utils as utils (__builtins__ if isinstance(__builtins__, dict) else __builtins__.__dict__)['format'] = utils.str.format diff --git a/src/i18n.py b/src/i18n.py index d54ccfb5a..8c812d212 100644 --- a/src/i18n.py +++ b/src/i18n.py @@ -146,9 +146,12 @@ class _PluginInternationalization: self._loadL10nCode() try: - translationFile = open(getLocalePath(self.name, localeName, 'po'), - 'ru') # ru is the mode, not the beginning - # of 'russian' ;) + try: + translationFile = open(getLocalePath(self.name, + localeName, 'po'), 'ru') + except ValueError: # We are using Windows + translationFile = open(getLocalePath(self.name, + localeName, 'po'), 'r') self._parse(translationFile) except IOError: # The translation is unavailable self.translations = {} diff --git a/src/utils/net.py b/src/utils/net.py index fa78fdcc3..370f3e6fc 100644 --- a/src/utils/net.py +++ b/src/utils/net.py @@ -34,9 +34,50 @@ Simple utility modules. import re import socket -emailRe = re.compile(r"^(\w&.+-]+!)*[\w&.+-]+@" - r"(([0-9a-z]([0-9a-z-]*[0-9a-z])?\.)[a-z]{2,6}|" - r"([0-9]{1,3}\.){3}[0-9]{1,3})$", re.I) +class EmailRe: + """Fake class used for backward compatibility.""" + + rfc822_specials = '()<>@,;:\\"[]' + def match(self, addr): + # From http://www.secureprogramming.com/?action=view&feature=recipes&recipeid=1 + + # First we validate the name portion (name@domain) + c = 0 + while c < len(addr): + if addr[c] == '"' and (not c or addr[c - 1] == '.' or addr[c - 1] == '"'): + c = c + 1 + while c < len(addr): + if addr[c] == '"': break + if addr[c] == '\\' and addr[c + 1] == ' ': + c = c + 2 + continue + if ord(addr[c]) < 32 or ord(addr[c]) >= 127: return 0 + c = c + 1 + else: return 0 + if addr[c] == '@': break + if addr[c] != '.': return 0 + c = c + 1 + continue + if addr[c] == '@': break + if ord(addr[c]) <= 32 or ord(addr[c]) >= 127: return 0 + if addr[c] in self.rfc822_specials: return 0 + c = c + 1 + if not c or addr[c - 1] == '.': return 0 + + # Next we validate the domain portion (name@domain) + domain = c = c + 1 + if domain >= len(addr): return 0 + count = 0 + while c < len(addr): + if addr[c] == '.': + if c == domain or addr[c - 1] == '.': return 0 + count = count + 1 + if ord(addr[c]) <= 32 or ord(addr[c]) >= 127: return 0 + if addr[c] in self.rfc822_specials: return 0 + c = c + 1 + + return count >= 1 +emailRe = EmailRe() def getSocket(host): """Returns a socket of the correct AF_INET type (v4 or v6) in order to diff --git a/src/version.py b/src/version.py index b73d4b4e4..c772ecd95 100644 --- a/src/version.py +++ b/src/version.py @@ -1,3 +1,3 @@ """stick the various versioning attributes in here, so we only have to change them once.""" -version = '0.83.4.1+limnoria (2011-04-26T10:32:24+0200)' +version = '0.83.4.1+limnoria (2011-05-27T18:16:23+0200)'