393 lines
15 KiB
Python
393 lines
15 KiB
Python
###
|
|
# 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 re
|
|
import os
|
|
import sys
|
|
import json
|
|
import tarfile
|
|
import supybot.conf as conf
|
|
import supybot.utils as utils
|
|
import supybot.world as world
|
|
from supybot.commands import *
|
|
import supybot.plugins as plugins
|
|
import supybot.ircutils as ircutils
|
|
import supybot.callbacks as callbacks
|
|
try:
|
|
from supybot.i18n import PluginInternationalization
|
|
from supybot.i18n import internationalizeDocstring
|
|
_ = PluginInternationalization('Packages')
|
|
except:
|
|
# This are useless functions that's allow to run the plugin on a bot
|
|
# without the i18n plugin
|
|
_ = lambda x:x
|
|
internationalizeDocstring = lambda x:x
|
|
|
|
if not hasattr(world, 'features'):
|
|
world.features = {}
|
|
world.features.update({'package-installer': '0.2'})
|
|
|
|
BIGGER = 1
|
|
EQUAL = 0
|
|
LOWER = -1
|
|
|
|
def compareVersions(v1, v2):
|
|
"""Returns -1, 0, or 1, depending on the newest version."""
|
|
def split(version):
|
|
splitted = version.split('+')
|
|
patches = splitted[1:]
|
|
numbers = splitted[0].split('.')
|
|
return numbers.extend(patches)
|
|
for index in range(0, min(len(v1), len(v2))):
|
|
if v1[index] < v2[index]:
|
|
return LOWER
|
|
elif v1[index] > v2[index]:
|
|
return BIGGER
|
|
if len(v1) < len(v2):
|
|
return LOWER
|
|
if len(v1) > len(v2):
|
|
return BIGGER
|
|
return EQUAL
|
|
|
|
def getDirectory(file_):
|
|
"""Tries to find the directory where plugin files are. Returns None
|
|
if it is not found or if any file is missing."""
|
|
directory = None
|
|
names = []
|
|
for name in file_.getnames():
|
|
assert not name.startswith('/')
|
|
assert not name.startswith('../')
|
|
assert ':' not in name # Prevents Windows drives and bad formed names
|
|
if directory is not None and not name.startswith(directory + '/'):
|
|
# No more than one directory at root
|
|
return False
|
|
elif directory is None:
|
|
directory = name.split('/')[0]
|
|
if '/' in name:
|
|
assert name.startswith(directory + '/')
|
|
else:
|
|
assert name == directory
|
|
names.append(name[len(directory)+1:])
|
|
if directory is None:
|
|
return None
|
|
if not all([x in names for x in ('__init__.py', 'config.py',
|
|
'plugin.py', 'packaging.py',
|
|
'test.py')]):
|
|
# I know, test.py is not necessary. But people who don't write
|
|
# test case suck. More over, supybot-plugin-create automatically
|
|
# creates this file for a long time, so it should be there.
|
|
return None
|
|
return directory
|
|
|
|
def getWritableDirectoryFromList(directories):
|
|
for directory in directories:
|
|
if os.access(directory, os.W_OK):
|
|
return directory
|
|
return None
|
|
|
|
|
|
@internationalizeDocstring
|
|
class Packages(callbacks.Plugin):
|
|
"""Add the help for "@plugin help Packages" here
|
|
This should describe *how* to use this plugin."""
|
|
threaded = True
|
|
|
|
@internationalizeDocstring
|
|
def install(self, irc, msg, args, filename, optlist):
|
|
"""<filename> [--force]
|
|
|
|
Installs the package. If the package has been downloaded with Packages,
|
|
just give the package name; otherwise, give the full path (including
|
|
the extension).
|
|
If given, --force disables sanity checks (usage is deprecated)."""
|
|
filename = os.path.expanduser(filename)
|
|
if os.path.sep not in filename:
|
|
filename = os.path.join(conf.supybot.directories.data(), filename)
|
|
filename += '.tar'
|
|
try:
|
|
file_ = tarfile.open(name=filename, mode='r:*')
|
|
except:
|
|
irc.error(_('Cannot open the package. Are you sure it is '
|
|
'readable, it is a tarball and it is not '
|
|
'corrupted?'))
|
|
return
|
|
directory = getDirectory(file_)
|
|
if not directory:
|
|
irc.error(_('The file is not a valid package.'))
|
|
return
|
|
class packaging:
|
|
"""Namespace for runned code"""
|
|
exec(file_.extractfile('%s/packaging.py' % directory).read())
|
|
if not ('force', True) in optlist:
|
|
failures = []
|
|
for feature, version in packaging.requires.items():
|
|
if feature not in world.features:
|
|
failures.append(_('%s (missing)') % feature)
|
|
elif compareVersions(world.features[feature], version) == LOWER:
|
|
failures.append(_('%s (>=%s needed, but %s available)') %
|
|
(feature, version, world.features[feature]))
|
|
if failures != []:
|
|
irc.error(_('Missing dependency(ies): ') +
|
|
', '.join(failures))
|
|
return
|
|
directories = conf.supybot.directories.plugins()
|
|
directory = getWritableDirectoryFromList(directories)
|
|
if directory is None:
|
|
irc.error(_('No writable plugin directory found.'))
|
|
return
|
|
file_.extractall(directory)
|
|
irc.replySuccess()
|
|
if hasattr(packaging, 'additionalReply'):
|
|
irc.reply('The plugin provides this additional information: %s' %
|
|
packaging.additionalReply)
|
|
install = wrap(install, ['owner', 'filename', getopts({'force': ''})])
|
|
|
|
@internationalizeDocstring
|
|
def download(self, irc, msg, args, name, optlist):
|
|
"""<package> [--version <version>] [--repo <repository url>]
|
|
|
|
Downloads the <package> at the <repository url>.
|
|
<version> defaults to the latest version available.
|
|
<repository url> defaults to http://packages.supybot.fr.cr/"""
|
|
# Parse and check parameters
|
|
version = None
|
|
repo = 'http://packages.supybot.fr.cr/'
|
|
for key, value in optlist:
|
|
if key == 'version': version = value
|
|
elif key == 'repo': repo = value
|
|
if __builtins__['any']([x in repo for x in ('?', '&')]):
|
|
# Supybot rewrites any() in commands.py
|
|
irc.error(_('Bad formed url.'))
|
|
return
|
|
selectedPackage = None
|
|
|
|
# Get server's index
|
|
try:
|
|
index = json.load(utils.web.getUrlFd(repo))
|
|
except ValueError:
|
|
irc.error(_('Server\'s JSON is bad formed.'))
|
|
return
|
|
|
|
# Crawl the available packages list
|
|
for package in index['packages']:
|
|
if not package['name'] == name:
|
|
continue
|
|
if version is None and (
|
|
selectedPackage == None or
|
|
compareVersions(selectedPackage['version'],
|
|
package['version']) == LOWER):
|
|
# If not version given, and [no selected package
|
|
# or selected package is older than this one]
|
|
selectedPackage = package
|
|
elif package['version'] == version:
|
|
selectedPackage = package
|
|
if selectedPackage is None:
|
|
irc.error(_('No packages matches your query.'))
|
|
return
|
|
|
|
# Determines the package's real URL
|
|
# TODO: handle relative URL starting with /
|
|
# FIXME: URL ending with /foobar.txt
|
|
packageUrl = selectedPackage['download-url']
|
|
if packageUrl.startswith('./'):
|
|
packageUrl = repo
|
|
if not packageUrl.endswith('/'):
|
|
packageUrl += '/'
|
|
packageUrl += selectedPackage['download-url']
|
|
|
|
# Write the package to the disk
|
|
directory = conf.supybot.directories.data()
|
|
assert os.access(directory, os.W_OK)
|
|
path = os.path.join(directory, '%s.tar' % name)
|
|
try:
|
|
os.unlink(path)
|
|
except OSError:
|
|
# Does not exist
|
|
pass
|
|
with open(path, 'ab') as file_:
|
|
try:
|
|
file_.write(utils.web.getUrlFd(packageUrl).read())
|
|
except utils.web.Error as e:
|
|
irc.reply(e.args[0])
|
|
return
|
|
irc.replySuccess()
|
|
download = wrap(download, ['owner', 'something',
|
|
getopts({'version': 'something',
|
|
'repo': 'httpUrl'})])
|
|
|
|
@internationalizeDocstring
|
|
def checkupdates(self, irc, msg, args, repo):
|
|
"""[<repository url>]
|
|
|
|
Checks for updates for loaded plugins at the <repository url>.
|
|
<repository url> defaults to http://packages.supybot.fr.cr/"""
|
|
if repo is None:
|
|
repo = 'http://packages.supybot.fr.cr/'
|
|
|
|
# Get server's index
|
|
try:
|
|
index = json.load(utils.web.getUrlFd(repo))
|
|
except ValueError:
|
|
irc.error(_('Server\'s JSON is bad formed.'))
|
|
return
|
|
|
|
# Crawl the index
|
|
needUpdate = {}
|
|
for package in index['packages']:
|
|
if package['name'] in sys.modules and (
|
|
not hasattr(sys.modules[package['name']], '__version__') or
|
|
compareVersions(sys.modules[package['name']].__version__,
|
|
package['version']) == LOWER):
|
|
if package['name'] in needUpdate:
|
|
if compareVersions(needUpdate[package['name']].__version__,
|
|
package['version']) != LOWER:
|
|
continue
|
|
needUpdate.update({package['name']: package})
|
|
|
|
# Display results
|
|
if needUpdate == {}:
|
|
irc.reply(_('All loaded plugins are up to date :)'))
|
|
else:
|
|
irc.reply(', '.join(['%s (%s)' % (y['name'],y['version'])
|
|
for x,y in needUpdate.items()]))
|
|
checkupdates = wrap(checkupdates, ['owner', optional('httpUrl')])
|
|
|
|
def search(self, irc, msg, args, repo, optlist, description):
|
|
"""[<repository url>] [--name <name>] [--version <version>]\
|
|
[--author <author>] [<description>]
|
|
|
|
Searches the packages matching the query in the <repository url>.
|
|
<repository url> defaults to http://packages.supybot.fr.cr"""
|
|
# Parse the arguments
|
|
if repo is None:
|
|
repo = 'http://packages.supybot.fr.cr/'
|
|
if description is None:
|
|
description = ''
|
|
if not __builtins__['any'](x in description for x in '*?'):
|
|
description = '*%s*' % description
|
|
optlist.append(('description', description))
|
|
def glob2matcher(glob):
|
|
glob = utils.python.glob2re(glob)
|
|
return re.compile(glob).match
|
|
matchers = {}
|
|
for key, value in optlist:
|
|
if value != None:
|
|
matchers.update({key: glob2matcher(value)})
|
|
|
|
# Get server's index
|
|
try:
|
|
index = json.load(utils.web.getUrlFd(repo))
|
|
except ValueError:
|
|
irc.error(_('Server\'s JSON is bad formed.'))
|
|
return
|
|
|
|
# Crawl packages index
|
|
results = []
|
|
for package in index['packages']:
|
|
ok = True
|
|
for key, matcher in matchers.items():
|
|
if key in package and not matcher(str(package[key])):
|
|
# If the packages index doesn't have this key, we consider
|
|
# the key matched.
|
|
ok = False
|
|
break
|
|
if ok:
|
|
results.append(package)
|
|
|
|
# Display results
|
|
reply = ['%s (%s)' % (x['name'],x['version']) for x in results]
|
|
reply.sort()
|
|
irc.reply(', '.join(reply))
|
|
options = ['name', 'version', 'author']
|
|
search = wrap(search, [optional('httpUrl'),
|
|
getopts(dict([(x,'anything') for x in options])),
|
|
optional('text')])
|
|
|
|
def info(self, irc, msg, args, repo, name, version, optlist):
|
|
"""[<repository url>] <package> [<version>] [--author-full]
|
|
|
|
Displays informations about the <package>, at the given <version>.
|
|
<repository url> defaults to http://packages.supybot.fr.cr/ and
|
|
<version> defaults to the latest available."""
|
|
# Parse the arguments
|
|
if repo is None:
|
|
repo = 'http://packages.supybot.fr.cr/'
|
|
if version == '--author-full': # Bug in wrap()
|
|
version = None
|
|
optlist.append(('--author-full', True))
|
|
|
|
# Get server's index
|
|
try:
|
|
index = json.load(utils.web.getUrlFd(repo))
|
|
except ValueError:
|
|
irc.error(_('Server\'s JSON is bad formed.'))
|
|
return
|
|
|
|
# Crawl the index
|
|
selectedPackage = None
|
|
for package in index['packages']:
|
|
if package['name'] == name:
|
|
if version is not None and package['version'] != version:
|
|
continue
|
|
if version is None and selectedPackage is not None and \
|
|
compareVersions(selectedPackage['version'],
|
|
package['version']) != LOWER:
|
|
continue
|
|
selectedPackage = package
|
|
|
|
# Display result
|
|
if selectedPackage is None:
|
|
irc.error('No such package/version.')
|
|
return
|
|
selectedPackage['author-name'] = selectedPackage['author'][0]
|
|
selectedPackage['author-nick'] = selectedPackage['author'][1]
|
|
selectedPackage['author-email'] = selectedPackage['author'][2]
|
|
if ('author-full', True) in optlist:
|
|
selectedPackage['author-string'] = '%s "%s" <%s>' % \
|
|
tuple(selectedPackage['author'])
|
|
else:
|
|
selectedPackage['author-string'] = selectedPackage['author-name']
|
|
for key in ('requires', 'suggests', 'provides'):
|
|
selectedPackage[key] = ', '.join('%s (%s)' % x for x in
|
|
selectedPackage[key].items())
|
|
irc.reply(('%(name)s (version %(version)s) has been written by '
|
|
'%(author-string)s and requires the fellowing flags: '
|
|
'%(requires)s') % selectedPackage)
|
|
info = wrap(info, [optional('httpUrl'), 'something',
|
|
optional('something'), getopts({'author-full': ''})])
|
|
|
|
|
|
|
|
|
|
Class = Packages
|
|
|
|
|
|
# vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79:
|