From 187ed38eccd1c036373ae712f89a98e78a6d48e9 Mon Sep 17 00:00:00 2001 From: Valentin Lorentz Date: Wed, 27 Apr 2011 14:59:02 +0200 Subject: [PATCH 01/11] PluginDownloader: creation; supports ProgVal and quantumlemur repositories; able to list repositories and plugins in them. --- plugins/PluginDownloader/README.txt | 1 + plugins/PluginDownloader/__init__.py | 66 +++++++++ plugins/PluginDownloader/config.py | 52 +++++++ plugins/PluginDownloader/local/__init__.py | 1 + plugins/PluginDownloader/plugin.py | 153 +++++++++++++++++++++ plugins/PluginDownloader/test.py | 41 ++++++ 6 files changed, 314 insertions(+) create mode 100644 plugins/PluginDownloader/README.txt create mode 100644 plugins/PluginDownloader/__init__.py create mode 100644 plugins/PluginDownloader/config.py create mode 100644 plugins/PluginDownloader/local/__init__.py create mode 100644 plugins/PluginDownloader/plugin.py create mode 100644 plugins/PluginDownloader/test.py 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..cd8e95d7a --- /dev/null +++ b/plugins/PluginDownloader/plugin.py @@ -0,0 +1,153 @@ +### +# 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 json +import urllib + +import git + +import supybot.log as log +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 + self._path = [x for x in path.split('/') if x != ''] + + + _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'] + treeHash = self._navigate(latestCommit, self._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 + +repositories = { + 'ProgVal': GithubRepository('ProgVal', 'Supybot-plugins'), + 'quantumlemur': GithubRepository( + 'quantumlemur', + 'Supybot-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])) + 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')]) + + +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..6e49d0c20 --- /dev/null +++ b/plugins/PluginDownloader/test.py @@ -0,0 +1,41 @@ +### +# 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. + +### + +from supybot.test import * + +class PluginDownloaderTestCase(PluginTestCase): + plugins = ('PluginDownloader',) + + def testRepolist(self): + self.assertResponse('repolist', 'quantumlemur, ProgVal') + self.assertRegexp('repolist ProgVal', '(.*, )?AttackProtector(, .*)?') + + +# vim:set shiftwidth=4 tabstop=4 expandtab textwidth=79: From f18429fdf7624530fbe48e8b312ed3f433ca8c91 Mon Sep 17 00:00:00 2001 From: Valentin Lorentz Date: Thu, 28 Apr 2011 11:38:48 +0200 Subject: [PATCH 02/11] PluginDownloader: add the @install command. --- plugins/PluginDownloader/plugin.py | 82 +++++++++++++++++++++++++++++- plugins/PluginDownloader/test.py | 36 +++++++++++++ 2 files changed, 116 insertions(+), 2 deletions(-) diff --git a/plugins/PluginDownloader/plugin.py b/plugins/PluginDownloader/plugin.py index cd8e95d7a..e4ec3f5fe 100644 --- a/plugins/PluginDownloader/plugin.py +++ b/plugins/PluginDownloader/plugin.py @@ -28,12 +28,17 @@ ### +import os import json import urllib +import urllib2 +import tarfile +from cStringIO import StringIO import git import supybot.log as log +import supybot.conf as conf import supybot.utils as utils from supybot.commands import * import supybot.plugins as plugins @@ -56,8 +61,18 @@ class GithubRepository(GitRepository): def __init__(self, username, reponame, path='/'): self._username = username self._reponame = reponame - self._path = [x for x in path.split('/') if x != ''] + 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={}): @@ -74,7 +89,8 @@ class GithubRepository(GitRepository): self._reponame, ) )['branches']['master'] - treeHash = self._navigate(latestCommit, self._path) + 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 ' @@ -114,6 +130,41 @@ class GithubRepository(GitRepository): # 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 = os.path.join(directory, newFileName) + if extractedFile is None: + os.mkdir(newFileName) + else: + open(newFileName, 'a').write(extractedFile.read()) + finally: + archive.close() + + 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( @@ -137,6 +188,11 @@ class PluginDownloader(callbacks.Plugin): 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 == []: @@ -145,6 +201,28 @@ class PluginDownloader(callbacks.Plugin): 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.' + )) + 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 diff --git a/plugins/PluginDownloader/test.py b/plugins/PluginDownloader/test.py index 6e49d0c20..d19493cce 100644 --- a/plugins/PluginDownloader/test.py +++ b/plugins/PluginDownloader/test.py @@ -28,14 +28,50 @@ ### +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 testRepolist(self): self.assertResponse('repolist', 'quantumlemur, ProgVal') 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') + assert os.path.isdir(pluginsPath + '/AttackProtector/') + assert os.path.isfile(pluginsPath + '/AttackProtector/plugin.py') + assert os.path.isfile(pluginsPath + '/AttackProtector/config.py') + + def testInstallQuantumlemur(self): + self.assertError('plugindownloader install quantumlemur AttackProtector') + self.assertNotError('plugindownloader install quantumlemur Listener') + self.assertError('plugindownloader install quantumlemur AttackProtector') + assert os.path.isdir(pluginsPath + '/Listener/') + assert os.path.isfile(pluginsPath + '/Listener/plugin.py') + assert os.path.isfile(pluginsPath + '/Listener/config.py') + # vim:set shiftwidth=4 tabstop=4 expandtab textwidth=79: From 72600e54af05c7b77ea1a5877eca1ef659e512a3 Mon Sep 17 00:00:00 2001 From: Valentin Lorentz Date: Thu, 28 Apr 2011 13:57:06 +0200 Subject: [PATCH 03/11] PluginDownloader: add stepnem's and gsf's repositories; fix issue with non-root path --- plugins/PluginDownloader/plugin.py | 31 +++++++++++++++++++++++--- plugins/PluginDownloader/test.py | 35 ++++++++++++++++++++++++------ 2 files changed, 56 insertions(+), 10 deletions(-) diff --git a/plugins/PluginDownloader/plugin.py b/plugins/PluginDownloader/plugin.py index e4ec3f5fe..21d408d56 100644 --- a/plugins/PluginDownloader/plugin.py +++ b/plugins/PluginDownloader/plugin.py @@ -66,7 +66,7 @@ class GithubRepository(GitRepository): if not path.endswith('/'): path += '/' self._path = path - + self._downloadUrl = 'https://github.com/%s/%s/tarball/master' % \ ( self._username, @@ -150,6 +150,7 @@ class GithubRepository(GitRepository): 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) @@ -157,6 +158,8 @@ class GithubRepository(GitRepository): open(newFileName, 'a').write(extractedFile.read()) finally: archive.close() + fileObject2.close() + del archive, fileObject, fileObject2 def _getWritableDirectoryFromList(self, directories): for directory in directories: @@ -166,10 +169,32 @@ class GithubRepository(GitRepository): repositories = { - 'ProgVal': GithubRepository('ProgVal', 'Supybot-plugins'), + 'ProgVal': GithubRepository( + 'ProgVal', + 'Supybot-plugins' + ), 'quantumlemur': GithubRepository( 'quantumlemur', - 'Supybot-plugins' + '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', ), } diff --git a/plugins/PluginDownloader/test.py b/plugins/PluginDownloader/test.py index d19493cce..4f4135bff 100644 --- a/plugins/PluginDownloader/test.py +++ b/plugins/PluginDownloader/test.py @@ -53,25 +53,46 @@ class PluginDownloaderTestCase(PluginTestCase): 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.assertResponse('repolist', 'quantumlemur, ProgVal') + 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') - assert os.path.isdir(pluginsPath + '/AttackProtector/') - assert os.path.isfile(pluginsPath + '/AttackProtector/plugin.py') - assert os.path.isfile(pluginsPath + '/AttackProtector/config.py') + self._testPluginInstalled('AttackProtector') def testInstallQuantumlemur(self): self.assertError('plugindownloader install quantumlemur AttackProtector') self.assertNotError('plugindownloader install quantumlemur Listener') self.assertError('plugindownloader install quantumlemur AttackProtector') - assert os.path.isdir(pluginsPath + '/Listener/') - assert os.path.isfile(pluginsPath + '/Listener/plugin.py') - assert os.path.isfile(pluginsPath + '/Listener/config.py') + self._testPluginInstalled('Listener') + def testInstallStepnem(self): + self.assertNotError('plugindownloader install stepnem Freenode') + self._testPluginInstalled('Freenode') + + def testGsf(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') # vim:set shiftwidth=4 tabstop=4 expandtab textwidth=79: From 8fe3e77428eee0b4c4a422e635512ced583c7787 Mon Sep 17 00:00:00 2001 From: Valentin Lorentz Date: Thu, 28 Apr 2011 14:20:36 +0200 Subject: [PATCH 04/11] PluginDownloaded: add nanotube-bitcoin repository and check a plugin exists before downloading the tarball --- plugins/PluginDownloader/plugin.py | 61 +++++++++++++++++------------- plugins/PluginDownloader/test.py | 7 +++- 2 files changed, 40 insertions(+), 28 deletions(-) diff --git a/plugins/PluginDownloader/plugin.py b/plugins/PluginDownloader/plugin.py index 21d408d56..5f710ed49 100644 --- a/plugins/PluginDownloader/plugin.py +++ b/plugins/PluginDownloader/plugin.py @@ -169,33 +169,38 @@ class GithubRepository(GitRepository): 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', - ), + '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', + ), } class PluginDownloader(callbacks.Plugin): @@ -237,6 +242,8 @@ class PluginDownloader(callbacks.Plugin): '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) diff --git a/plugins/PluginDownloader/test.py b/plugins/PluginDownloader/test.py index 4f4135bff..62ed49f20 100644 --- a/plugins/PluginDownloader/test.py +++ b/plugins/PluginDownloader/test.py @@ -79,7 +79,7 @@ class PluginDownloaderTestCase(PluginTestCase): self.assertNotError('plugindownloader install stepnem Freenode') self._testPluginInstalled('Freenode') - def testGsf(self): + def testInstallGsf(self): self.assertNotError('plugindownloader install gsf-snapshot Debian') self._testPluginInstalled('Debian') self.assertError('plugindownloader install gsf-snapshot Anagram') @@ -95,4 +95,9 @@ class PluginDownloaderTestCase(PluginTestCase): 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') + + # vim:set shiftwidth=4 tabstop=4 expandtab textwidth=79: From 6e0016866237d8204ee63b38e5bccd33d5fab9b2 Mon Sep 17 00:00:00 2001 From: Valentin Lorentz Date: Thu, 28 Apr 2011 14:47:25 +0200 Subject: [PATCH 05/11] PluginDownloader: add mtughan-weather and SpiderDave repositories --- plugins/PluginDownloader/plugin.py | 9 +++++++++ plugins/PluginDownloader/test.py | 9 +++++++++ 2 files changed, 18 insertions(+) diff --git a/plugins/PluginDownloader/plugin.py b/plugins/PluginDownloader/plugin.py index 5f710ed49..6aa8eac1e 100644 --- a/plugins/PluginDownloader/plugin.py +++ b/plugins/PluginDownloader/plugin.py @@ -201,6 +201,15 @@ repositories = { 'supybot-bitcoin-' 'marketmonitor', ), + 'mtughan-weather': GithubRepository( + 'mtughan', + 'Supybot-Weather', + ), + 'SpiderDave': GithubRepository( + 'SpiderDave', + 'spidey-supybot-plugins', + 'Plugins', + ), } class PluginDownloader(callbacks.Plugin): diff --git a/plugins/PluginDownloader/test.py b/plugins/PluginDownloader/test.py index 62ed49f20..91da9a362 100644 --- a/plugins/PluginDownloader/test.py +++ b/plugins/PluginDownloader/test.py @@ -99,5 +99,14 @@ class PluginDownloaderTestCase(PluginTestCase): 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: From 64b4a61d3afff3df4e2444d1529f15f9471eb69c Mon Sep 17 00:00:00 2001 From: Valentin Lorentz Date: Thu, 28 Apr 2011 16:54:25 +0200 Subject: [PATCH 06/11] PluginDownloader: remove useless import --- plugins/PluginDownloader/plugin.py | 1 - 1 file changed, 1 deletion(-) diff --git a/plugins/PluginDownloader/plugin.py b/plugins/PluginDownloader/plugin.py index 6aa8eac1e..756707c03 100644 --- a/plugins/PluginDownloader/plugin.py +++ b/plugins/PluginDownloader/plugin.py @@ -35,7 +35,6 @@ import urllib2 import tarfile from cStringIO import StringIO -import git import supybot.log as log import supybot.conf as conf From d6cd8a5427803a9143b05af6bfa46fc407659cdc Mon Sep 17 00:00:00 2001 From: Valentin Lorentz Date: Tue, 3 May 2011 20:23:20 +0200 Subject: [PATCH 07/11] Fix email regexp to be RFC-compliant --- src/utils/net.py | 47 ++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 44 insertions(+), 3 deletions(-) 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 From ce29bf8b5c07f28766bfa4de0ff1900a1d94a22d Mon Sep 17 00:00:00 2001 From: Valentin Lorentz Date: Fri, 6 May 2011 19:34:46 +0200 Subject: [PATCH 08/11] Fix installation on a system that never had Supybot installed (fix commit 05c9482759) --- src/__init__.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) 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 From 8979475e139ba8fabcda2a6cacc7b0e7fe071138 Mon Sep 17 00:00:00 2001 From: Valentin Lorentz Date: Sat, 7 May 2011 09:12:03 +0200 Subject: [PATCH 09/11] Fix compatibility with Windows ('u' flag not supported by open()) --- src/i18n.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) 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 = {} From ef6fe23e02f73aaa5fd0965fb57923de104d27d4 Mon Sep 17 00:00:00 2001 From: Daniel Folkinshteyn Date: Mon, 4 Apr 2011 16:30:52 -0400 Subject: [PATCH 10/11] Seen: fix tests so they pass. fix seen command so it properly accepts nick wildcards. Conflicts: src/version.py --- plugins/Seen/plugin.py | 4 ++-- src/version.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) 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/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)' From 865bd93244755616f51f2a5bd16846bfce7710f2 Mon Sep 17 00:00:00 2001 From: Valentin Lorentz Date: Fri, 27 May 2011 18:18:53 +0200 Subject: [PATCH 11/11] MessageParser: remove redundant spaces --- plugins/MessageParser/plugin.py | 62 ++++++++++++++++----------------- 1 file changed, 31 insertions(+), 31 deletions(-) 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')