407 lines
18 KiB
Python
407 lines
18 KiB
Python
###
|
|
# Copyright (c) 2003-2005, James Vega
|
|
# 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 re
|
|
import time
|
|
import urllib
|
|
import fnmatch
|
|
|
|
import bs4 as BeautifulSoup
|
|
|
|
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
|
|
from supybot.utils.iter import all
|
|
|
|
class Debian(callbacks.Plugin):
|
|
threaded = True
|
|
|
|
_debreflags = re.DOTALL | re.MULTILINE
|
|
_deblistreFileExact = re.compile(r'<a href="/[^/>]+/[^/>]+">([^<]+)</a>',
|
|
_debreflags)
|
|
def file(self, irc, msg, args, optlist, filename):
|
|
"""[--exact] \
|
|
[--mode {path,filename,exactfilename}] \
|
|
[--branch {oldstable,stable,testing,unstable,experimental}] \
|
|
[--arch <architecture>] \
|
|
[--section {main,contrib,non-free}] <file name>
|
|
|
|
Returns the package(s) containing the <file name>.
|
|
--mode defaults to path, and defines how to search.
|
|
--branch defaults to stable, and defines in what branch to search."""
|
|
url = 'http://packages.debian.org/search?searchon=contents' + \
|
|
'&keywords=%(keywords)s&mode=%(mode)s&suite=%(suite)s' + \
|
|
'&arch=%(arch)s'
|
|
def reg(name):
|
|
return self.registryValue('defaults.file.%s' % name, msg.args[0])
|
|
args = {'keywords': None,
|
|
'mode': reg('mode'),
|
|
'suite': reg('branch'),
|
|
'section': reg('section'),
|
|
'arch': reg('arch')}
|
|
exact = ('exact', True) in optlist
|
|
for (key, value) in optlist:
|
|
if key == 'branch':
|
|
args['suite'] = value
|
|
elif key == 'section':
|
|
args['section'] = value
|
|
elif key == 'arch':
|
|
args['arch'] = value
|
|
elif key == 'mode':
|
|
args['mode'] = value
|
|
responses = []
|
|
if '*' in filename:
|
|
irc.error('Wildcard characters can not be specified.', Raise=True)
|
|
args['keywords'] = utils.web.urlquote(filename, '')
|
|
url %= args
|
|
try:
|
|
html = utils.web.getUrl(url).decode()
|
|
except utils.web.Error as e:
|
|
irc.error(format('I couldn\'t reach the search page (%s).', e),
|
|
Raise=True)
|
|
if 'is down at the moment' in html:
|
|
irc.error('Packages.debian.org is down at the moment. '
|
|
'Please try again later.', Raise=True)
|
|
step = 0
|
|
pkgs = []
|
|
for line in html.split('\n'):
|
|
if '<span class="keyword">' in line:
|
|
step += 1
|
|
elif step == 1 or (step >= 1 and not exact):
|
|
pkgs.extend(self._deblistreFileExact.findall(line))
|
|
if pkgs == []:
|
|
irc.reply(format('No filename found for %s (%s)',
|
|
utils.web.urlunquote(filename), args['suite']))
|
|
else:
|
|
# Filter duplicated
|
|
pkgs = dict(map(lambda x:(x, None), pkgs)).keys()
|
|
irc.reply(format('%i matches found: %s (%s)',
|
|
len(pkgs), '; '.join(pkgs), args['suite']))
|
|
file = wrap(file, [getopts({'exact': '',
|
|
'branch': ('literal', ('oldstable',
|
|
'stable',
|
|
'testing',
|
|
'unstable',
|
|
'experimental')),
|
|
'mode': ('literal', ('path',
|
|
'exactfilename',
|
|
'filename')),
|
|
'section': ('literal', ('main',
|
|
'contrib',
|
|
'non-free')),
|
|
'arch': 'somethingWithoutSpaces'}),
|
|
'text'])
|
|
|
|
_debreflags = re.DOTALL | re.IGNORECASE
|
|
_deblistreVersion = re.compile(r'<h3>Package ([^<]+)</h3>(.*?)</ul>', _debreflags)
|
|
def version(self, irc, msg, args, optlist, package):
|
|
"""[--exact] \
|
|
[--searchon {names,all,sourcenames}] \
|
|
[--branch {oldstable,stable,testing,unstable,experimental}] \
|
|
[--section {main,contrib,non-free}] <package name>
|
|
|
|
Returns the current version(s) of the Debian package <package name>.
|
|
--exact, if given, means you want only the <package name>, and not
|
|
package names containing this name.
|
|
--searchon defaults to names, and defines where to search.
|
|
--branch defaults to all, and defines in what branch to search.
|
|
--section defaults to all, and defines in what section to search."""
|
|
url = 'http://packages.debian.org/search?keywords=%(keywords)s' + \
|
|
'&searchon=%(searchon)s&suite=%(suite)s§ion=%(section)s'
|
|
def reg(name):
|
|
return self.registryValue('defaults.version.%s' % name, msg.args[0])
|
|
args = {'keywords': None,
|
|
'searchon': reg('searchon'),
|
|
'suite': reg('branch'),
|
|
'section': reg('section')}
|
|
for (key, value) in optlist:
|
|
if key == 'exact':
|
|
url += '&exact=1'
|
|
elif key == 'branch':
|
|
args['suite'] = value
|
|
elif key == 'section':
|
|
args['section'] = value
|
|
elif key == 'searchon':
|
|
args['searchon'] = value
|
|
responses = []
|
|
if '*' in package:
|
|
irc.error('Wildcard characters can not be specified.', Raise=True)
|
|
args['keywords'] = utils.web.urlquote(package)
|
|
url %= args
|
|
try:
|
|
html = utils.web.getUrl(url).decode()
|
|
except utils.web.Error as e:
|
|
irc.error(format('I couldn\'t reach the search page (%s).', e),
|
|
Raise=True)
|
|
if 'is down at the moment' in html:
|
|
irc.error('Packages.debian.org is down at the moment. '
|
|
'Please try again later.', Raise=True)
|
|
pkgs = self._deblistreVersion.findall(html)
|
|
if not pkgs:
|
|
irc.reply(format('No package found for %s (%s)',
|
|
utils.web.urlunquote(package), args['suite']))
|
|
else:
|
|
for pkg in pkgs:
|
|
pkgMatch = pkg[0]
|
|
soup = BeautifulSoup.BeautifulSoup(pkg[1])
|
|
liBranches = soup.find_all('li')
|
|
branches = []
|
|
versions = []
|
|
def branchVers(br):
|
|
vers = [b.next.string.strip() for b in br]
|
|
return [utils.str.rsplit(v, ':', 1)[0] for v in vers]
|
|
for li in liBranches:
|
|
branches.append(li.a.string)
|
|
versions.append(branchVers(li.find_all('br')))
|
|
if branches and versions:
|
|
for pairs in zip(branches, versions):
|
|
branch = pairs[0]
|
|
ver = ', '.join(pairs[1])
|
|
s = format('%s (%s)', pkgMatch,
|
|
': '.join([branch, ver]))
|
|
responses.append(s)
|
|
resp = format('%i matches found: %s',
|
|
len(responses), '; '.join(responses))
|
|
irc.reply(resp)
|
|
version = wrap(version, [getopts({'exact': '',
|
|
'searchon': ('literal', ('names',
|
|
'all',
|
|
'sourcenames')),
|
|
'branch': ('literal', ('oldstable',
|
|
'stable',
|
|
'testing',
|
|
'unstable',
|
|
'experimental')),
|
|
'arch': ('literal', ('main',
|
|
'contrib',
|
|
'non-free'))}),
|
|
'text'])
|
|
|
|
_incomingRe = re.compile(r'<a href="(.*?\.deb)">', re.I)
|
|
def incoming(self, irc, msg, args, optlist, globs):
|
|
"""[--{regexp,arch} <value>] [<glob> ...]
|
|
|
|
Checks debian incoming for a matching package name. The arch
|
|
parameter defaults to i386; --regexp returns only those package names
|
|
that match a given regexp, and normal matches use standard *nix
|
|
globbing.
|
|
"""
|
|
predicates = []
|
|
archPredicate = lambda s: ('_i386.' in s)
|
|
for (option, arg) in optlist:
|
|
if option == 'regexp':
|
|
predicates.append(r.search)
|
|
elif option == 'arch':
|
|
arg = '_%s.' % arg
|
|
archPredicate = lambda s, arg=arg: (arg in s)
|
|
predicates.append(archPredicate)
|
|
for glob in globs:
|
|
glob = fnmatch.translate(glob)
|
|
predicates.append(re.compile(glob).search)
|
|
packages = []
|
|
try:
|
|
fd = utils.web.getUrlFd('http://incoming.debian.org/')
|
|
except utils.web.Error as e:
|
|
irc.error(str(e), Raise=True)
|
|
for line in fd:
|
|
m = self._incomingRe.search(line.decode())
|
|
if m:
|
|
name = m.group(1)
|
|
if all(None, map(lambda p: p(name), predicates)):
|
|
realname = utils.str.rsplit(name, '_', 1)[0]
|
|
packages.append(realname)
|
|
if len(packages) == 0:
|
|
irc.error('No packages matched that search.')
|
|
else:
|
|
irc.reply(format('%L', packages))
|
|
incoming = thread(wrap(incoming,
|
|
[getopts({'regexp': 'regexpMatcher',
|
|
'arch': 'something'}),
|
|
any('glob')]))
|
|
|
|
def bold(self, s):
|
|
if self.registryValue('bold', dynamic.channel):
|
|
return ircutils.bold(s)
|
|
return s
|
|
|
|
_update = re.compile(r' : ([^<]+)</body')
|
|
_bugsCategoryTitle = re.compile(r'<dt id="bugs_.." title="([^>]+)">')
|
|
_latestVersion = re.compile(r'<span id="latest_version">(.+)</span>')
|
|
_maintainer = re.compile(r'<a href=".*login=(?P<email>[^<]+)">.*'
|
|
'<span class="name" title="maintainer">'
|
|
'(?P<name>[^<]+)</span>', re.S)
|
|
def stats(self, irc, msg, args, pkg):
|
|
"""<source package>
|
|
|
|
Reports various statistics (from http://packages.qa.debian.org/) about
|
|
<source package>.
|
|
"""
|
|
pkg = pkg.lower()
|
|
try:
|
|
text = utils.web.getUrl('http://packages.qa.debian.org/%s/%s.html' %
|
|
(pkg[0], pkg)).decode('utf8')
|
|
except utils.web.Error:
|
|
irc.errorInvalid('source package name')
|
|
for line in text.split('\n'):
|
|
match = self._latestVersion.search(text)
|
|
if match is not None:
|
|
break
|
|
assert match is not None
|
|
version = '%s: %s' % (self.bold('Last version'),
|
|
match.group(1))
|
|
updated = None
|
|
m = self._update.search(text)
|
|
if m:
|
|
updated = m.group(1)
|
|
soup = BeautifulSoup.BeautifulSoup(text)
|
|
pairs = zip(soup.find_all('dt'),
|
|
soup.find_all('dd'))
|
|
for (label, content) in pairs:
|
|
try:
|
|
title = self._bugsCategoryTitle.search(str(label)).group(1)
|
|
except AttributeError: # Didn't match
|
|
if str(label).startswith('<dt id="bugs_all">'):
|
|
title = 'All bugs'
|
|
elif str(label) == '<dt title="Maintainer and Uploaders">' + \
|
|
'maint</dt>':
|
|
title = 'Maintainer and Uploaders'
|
|
else:
|
|
continue
|
|
if title == 'Maintainer and Uploaders':
|
|
match = self._maintainer.search(str(content))
|
|
name, email = match.group('name'), match.group('email')
|
|
maintainer = format('%s: %s %u', self.bold('Maintainer'),
|
|
name, utils.web.mungeEmail(email))
|
|
elif title == 'All bugs':
|
|
bugsAll = format('%i Total', content.span.string)
|
|
elif title == 'Release Critical':
|
|
bugsRC = format('%i RC', content.span.string)
|
|
elif title == 'Important and Normal':
|
|
bugs = format('%i Important/Normal',
|
|
content.span.string)
|
|
elif title == 'Minor and Wishlist':
|
|
bugsMinor = format('%i Minor/Wishlist',
|
|
content.span.string)
|
|
elif title == 'Fixed and Pending':
|
|
bugsFixed = format('%i Fixed/Pending',
|
|
content.span.string)
|
|
bugL = (bugsAll, bugsRC, bugs, bugsMinor, bugsFixed)
|
|
s = '. '.join((version, maintainer,
|
|
'%s: %s' % (self.bold('Bugs'), '; '.join(bugL))))
|
|
if updated:
|
|
s = 'As of %s, %s' % (updated, s)
|
|
irc.reply(s)
|
|
stats = wrap(stats, ['somethingWithoutSpaces'])
|
|
|
|
_newpkgre = re.compile(r'<li><a href[^>/]+>([^<]+)</a>')
|
|
def new(self, irc, msg, args, section, version, glob):
|
|
"""[{main,contrib,non-free}] [<version>] [<glob>]
|
|
|
|
Checks for packages that have been added to Debian's unstable branch
|
|
in the past week. If no glob is specified, returns a list of all
|
|
packages. If no section is specified, defaults to main.
|
|
"""
|
|
if version is None:
|
|
version = 'unstable'
|
|
try:
|
|
fd = utils.web.getUrlFd('http://packages.debian.org/%s/%s/newpkg' %
|
|
(version, section))
|
|
except utils.web.Error as e:
|
|
irc.error(str(e), Raise=True)
|
|
packages = []
|
|
for line in fd:
|
|
m = self._newpkgre.search(line.decode())
|
|
if m:
|
|
m = m.group(1)
|
|
if fnmatch.fnmatch(m, glob):
|
|
packages.append(m)
|
|
fd.close()
|
|
if packages:
|
|
irc.reply(format('%L', packages))
|
|
else:
|
|
irc.error('No packages matched that search.')
|
|
new = wrap(new, [optional(('literal', ('main', 'contrib', 'non-free')),
|
|
'main'),
|
|
optional('something'),
|
|
additional('glob', '*')])
|
|
|
|
_severity = re.compile(r'<p>Severity: ([^<]+)</p>', re.I)
|
|
_package = re.compile(r'<pre class="message">Package: ([^<\n]+)\n',
|
|
re.I | re.S)
|
|
_reporter = re.compile(r'Reported by: <[^>]+>([^<]+)<', re.I | re.S)
|
|
_subject = re.compile(r'<span class="headerfield">Subject:</span> [^:]+: ([^<]+)</div>', re.I | re.S)
|
|
_date = re.compile(r'<span class="headerfield">Date:</span> ([^\n]+)\n</div>', re.I | re.S)
|
|
_tags = re.compile(r'<p>Tags: ([^<]+)</p>', re.I)
|
|
_searches = (_package, _subject, _reporter, _date)
|
|
def bug(self, irc, msg, args, bug):
|
|
"""<num>
|
|
|
|
Returns a description of the bug with bug id <num>.
|
|
"""
|
|
url = 'http://bugs.debian.org/cgi-bin/bugreport.cgi?bug=%s' % bug
|
|
try:
|
|
text = utils.web.getUrl(url).decode()
|
|
except utils.web.Error as e:
|
|
irc.error(str(e), Raise=True)
|
|
if "There is no record of Bug" in text:
|
|
irc.error('I could not find a bug report matching that number.',
|
|
Raise=True)
|
|
searches = list(map(lambda p: p.search(text), self._searches))
|
|
sev = self._severity.search(text)
|
|
tags = self._tags.search(text)
|
|
# This section should be cleaned up to ease future modifications
|
|
if all(None, searches):
|
|
L = map(self.bold, ('Package', 'Subject', 'Reported'))
|
|
resp = format('%s: %%s; %s: %%s; %s: by %%s on %%s', *L)
|
|
L = map(utils.web.htmlToText, map(lambda p: p.group(1), searches))
|
|
resp = format(resp, *L)
|
|
if sev:
|
|
sev = list(filter(None, sev.groups()))
|
|
if sev:
|
|
sev = utils.web.htmlToText(sev[0])
|
|
resp += format('; %s: %s', self.bold('Severity'), sev)
|
|
if tags:
|
|
resp += format('; %s: %s', self.bold('Tags'), tags.group(1))
|
|
resp += format('; %u', url)
|
|
irc.reply(resp)
|
|
else:
|
|
irc.error('I was unable to properly parse the BTS page.')
|
|
bug = wrap(bug, [('id', 'bug')])
|
|
|
|
Class = Debian
|
|
|
|
|
|
# vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79:
|