ubuntu-bots/Bugtracker/plugin.py

1049 lines
46 KiB
Python
Raw Normal View History

# -*- Encoding: utf-8 -*-
2006-06-26 17:57:20 +00:00
###
# Copyright (c) 2005-2007 Dennis Kaarsemaker
# Copyright (c) 2008-2010 Terence Simpson
2006-06-26 17:57:20 +00:00
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of version 2 of the GNU General Public License as
# published by the Free Software Foundation.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
###
from supybot.commands import *
import supybot.utils as utils
import supybot.ircmsgs as ircmsgs
2006-06-26 17:57:20 +00:00
import supybot.ircutils as ircutils
import supybot.callbacks as callbacks
import supybot.conf as conf
import supybot.registry as registry
import supybot.schedule as schedule
import supybot.log as supylog
2006-06-26 17:57:20 +00:00
#import imaplib
import re, os, time, commands
2006-06-26 17:57:20 +00:00
import xml.dom.minidom as minidom
from htmlentitydefs import entitydefs as entities
import email.FeedParser
import SOAPpy
2006-06-26 17:57:20 +00:00
# All the words below will be censored when reporting bug information
bad_words = set(["fuck","fuk","fucking","fuking","fukin","fuckin","fucked","fuked","fucker","shit","cunt","bastard","nazi","nigger","nigga","cock","bitches","bitch"])
def makeClean(s):
words = s.split()
for word in words:
if word.lower() in bad_words:
words[words.index(word)] = "<censored>"
return " ".join(words)
2006-06-26 17:57:20 +00:00
def registerBugtracker(name, url='', description='', trackertype=''):
conf.supybot.plugins.Bugtracker.bugtrackers().add(name)
group = conf.registerGroup(conf.supybot.plugins.Bugtracker.bugtrackers, name)
URL = conf.registerGlobalValue(group, 'url', registry.String(url, ''))
DESC = conf.registerGlobalValue(group, 'description', registry.String(description, ''))
TRACKERTYPE = conf.registerGlobalValue(group, 'trackertype', registry.String(trackertype, ''))
if url:
URL.setValue(url)
if description:
DESC.setValue(description)
if trackertype:
if defined_bugtrackers.has_key(trackertype.lower()):
TRACKERTYPE.setValue(trackertype.lower())
else:
raise BugtrackerError("Unknown trackertype: %s" % trackertype)
entre = re.compile('&(\S*?);')
def _getnodetxt(node):
L = []
for childnode in node.childNodes:
if childnode.nodeType == childnode.TEXT_NODE:
L.append(childnode.data)
val = ''.join(L)
if node.hasAttribute('encoding'):
encoding = node.getAttribute('encoding')
if encoding == 'base64':
try:
val = val.decode('base64')
except:
val = 'Cannot convert bug data from base64.'
while entre.search(val):
entity = entre.search(val).group(1)
if entity in entities:
val = entre.sub(entities[entity], val)
else:
val = entre.sub('?', val)
return val
class BugtrackerError(Exception):
"""A bugtracker error"""
pass
class BugNotFoundError(Exception):
"""Pity, bug isn't there"""
pass
cvere = re.compile(r'<th.*?Description.*?<td.*?>(.*?)\s*</td>', re.I | re.DOTALL)
2006-06-26 17:57:20 +00:00
class Bugtracker(callbacks.PluginRegexp):
"""Show a link to a bug report with a brief description"""
threaded = True
callBefore = ['URL']
regexps = ['turlSnarfer', 'bugSnarfer', 'oopsSnarfer', 'cveSnarfer']
2006-06-26 17:57:20 +00:00
def __init__(self, irc):
callbacks.PluginRegexp.__init__(self, irc)
self.db = ircutils.IrcDict()
# self.events = []
2006-06-26 17:57:20 +00:00
for name in self.registryValue('bugtrackers'):
registerBugtracker(name)
group = self.registryValue('bugtrackers.%s' % name.replace('.','\\.'), value=False)
if group.trackertype() in defined_bugtrackers.keys():
self.db[name] = defined_bugtrackers[group.trackertype()](name, group.url(), group.description())
else:
self.log.warning("Bugtracker: Unknown trackertype: %s (%s)" % (group.trackertype(), name))
2006-06-26 17:57:20 +00:00
self.shorthand = utils.abbrev(self.db.keys())
2009-10-15 20:21:50 +00:00
self.shown = {}
# # Schedule bug reporting
# #TODO: Remove everything below this line
# if self.registryValue('imap_server') and self.registryValue('reportercache'):
# try:
# schedule.removeEvent(self.name() + '.bugreporter')
# except:
# pass
# schedule.addPeriodicEvent(lambda: self.reportnewbugs(irc), 60, name=self.name() + '.bugreporter')
# self.events += [self.name() + '.bugreporter']
# self.log.info('Bugtracker: Adding scheduled event "%s.bugreporter"' % self.name())
def die(self): #TODO: Remove me
pass
# try:
# for event in self.events:
# self.log.info('Bugtracker: Removing scheduled event "%s"' % event)
# schedule.removeEvent(event)
# schedule.removeEvent(self.name())
# except:
# pass
2006-12-27 14:33:59 +00:00
def is_ok(self, channel, tracker, bug):
'''Flood/repeat protection'''
now = time.time()
for k in self.shown.keys():
2006-12-27 14:33:59 +00:00
if self.shown[k] < now - self.registryValue('repeatdelay', channel):
self.shown.pop(k)
if (channel, tracker, bug) not in self.shown:
self.shown[(channel, tracker, bug)] = now
return True
return False
def is_new(self, tracker, tag, id): #Depricated
pass
# bugreporter_base = self.registryValue('reportercache')
# if not os.path.exists(os.path.join(bugreporter_base,tag,tracker.name,str(int(id/1000)),str(id))):
# try:
# os.makedirs(os.path.join(bugreporter_base,tag,tracker.name,str(int(id/1000))))
# except:
# pass
# fd = open(os.path.join(bugreporter_base,tag,tracker.name,str(int(id/1000)),str(id)),'w')
# fd.close()
# return True
# return False
def reportnewbugs(self,irc): #Depricated
pass
# # Compile list of bugs
# self.log.info("Bugtracker: Checking for new bugs")
# bugs = {}
# if self.registryValue('imap_ssl'):
# sc = imaplib.IMAP4_SSL(self.registryValue('imap_server'))
# else:
# sc = imaplib.IMAP4(self.registryValue('imap_server'))
# sc.login(self.registryValue('imap_user'), self.registryValue('imap_password'))
# sc.select('INBOX')
# new_mail = sc.search(None, '(UNSEEN)')[1][0].split()[:20]
#
# # Read all new mail
# for m in new_mail:
# msg = sc.fetch(m, 'RFC822')[1][0][1]
# fp = email.FeedParser.FeedParser()
# sc.store(m, '+FLAGS', "(\Deleted)") # Mark message deleted so we don't have to process it again
# fp.feed(msg)
# bug = fp.close()
# tag = None
#
# if 'X-Launchpad-Bug' not in bug.keys():
# self.log.info('Bugtracker: Ignoring e-mail with no detectable bug (Not from Launchpad)')
# continue
# else:
# tag = bug['X-Launchpad-Bug']
# if 'distribution=' not in tag and 'product=' not in tag:
# self.log.info('Bugtracker: Ignoring e-mail with no detectable bug (no distro/product)')
# continue
# else:
# tag = tag.split(';')[0].strip().replace("product=",'').replace("distribution=","")
#
# if not tag:
# self.log.info('Bugtracker: Ignoring e-mail with no detectible bug (bad tag)')
#
# tag = tag[tag.find('+')+1:tag.find('@')]
# if tag not in bugs:
# bugs[tag] = {}
#
# # Determine bugtracker type (currently only Launchpad is supported anyway)
# if bug['X-Launchpad-Bug']:
# tracker = self.db['launchpad']
# id = int(bug['Reply-To'].split()[1])
# subj = bug['Subject'];
# if '[NEW]' not in subj: #Not a new bug
# continue
# if self.is_new(tracker, tag, id):
# component = bug['X-Launchpad-Bug']
# if 'component' in component:
# component = component[component.find('component=')+10:]
# component = component[:component.find(';')].replace('None','')
# else:
# component = ''
# try:
# if component:
# bugs[tag][id] = self.get_bug('',tracker, id, False)[0].replace('"','(%s) "' % component, 1)
# else:
# bugs[tag][id] = self.get_bug('',tracker, id, False)[0]
# if '[apport]' in bugs[tag][id]:
# bugs[tag].pop(id)
# except:
# self.log.info("Bugtracker: Unable to get new bug %d" % id)
# pass
# else:
# self.log.info('Bugtracker: Ignoring e-mail with no detectable bug')
#
# reported_bugs = 0
#
# for c in irc.state.channels:
# tags = self.registryValue('bugReporter', channel=c)
# if not tags:
# continue
# for tag in tags.split(','):
# if not tag or tag not in bugs.keys():
# continue
# for b in sorted(bugs[tag].keys()):
# irc.queueMsg(ircmsgs.privmsg(c,'New bug: #%s' % bugs[tag][b][bugs[tag][b].find('bug ')+4:]))
# reported_bugs = reported_bugs+1
2006-06-26 17:57:20 +00:00
def add(self, irc, msg, args, name, trackertype, url, description):
"""<name> <type> <url> [<description>]
Add a bugtracker <url> to the list of defined bugtrackers. <type> is the
type of the tracker (currently only Launchpad, Debbugs, Bugzilla,
Issuezilla, Mantis and Trac are known). <name> is the name that will be used to
2006-06-26 17:57:20 +00:00
reference the bugzilla in all commands. Unambiguous abbreviations of
<name> will be accepted also. <description> is the common name for the
bugzilla and will be listed with the bugzilla query; if not given, it
defaults to <name>.
"""
name = name.lower()
if not description:
description = name
if url[-1] == '/':
url = url[:-1]
trackertype = trackertype.lower()
if trackertype in defined_bugtrackers:
self.db[name] = defined_bugtrackers[trackertype](name,url,description)
else:
irc.error("Bugtrackers of type '%s' are not understood" % trackertype)
return
registerBugtracker(name, url, description, trackertype)
self.shorthand = utils.abbrev(self.db.keys())
irc.replySuccess()
add = wrap(add, [('checkCapability', 'admin'), 'something', 'something', 'url', additional('text')])
2006-06-26 17:57:20 +00:00
def remove(self, irc, msg, args, name):
"""<abbreviation>
Remove the bugtracker associated with <abbreviation> from the list of
defined bugtrackers.
"""
try:
name = self.shorthand[name.lower()]
del self.db[name]
self.registryValue('bugtrackers').remove(name)
self.shorthand = utils.abbrev(self.db.keys())
irc.replySuccess()
except KeyError:
s = self.registryValue('replyNoBugtracker', ircutils.isChannel(msg.args[0]) and msg.args[0] or None)
2006-06-26 17:57:20 +00:00
irc.error(s % name)
remove = wrap(remove, [('checkCapability', 'admin'), 'text'])
2006-06-26 17:57:20 +00:00
def rename(self, irc, msg, args, oldname, newname, newdesc):
"""<oldname> <newname>
2008-05-05 16:44:14 +00:00
Rename the bugtracker associated with <oldname> to <newname>.
"""
try:
name = self.shorthand[oldname.lower()]
group = self.registryValue('bugtrackers.%s' % name.replace('.','\\.'), value=False)
d = group.description()
if newdesc:
d = newdesc
self.db[newname] = defined_bugtrackers[group.trackertype()](name,group.url(),d)
registerBugtracker(newname, group.url(), d, group.trackertype())
del self.db[name]
self.registryValue('bugtrackers').remove(name)
self.shorthand = utils.abbrev(self.db.keys())
irc.replySuccess()
except KeyError:
s = self.registryValue('replyNoBugtracker', ircutils.isChannel(msg.args[0]) and msg.args[0] or None)
irc.error(s % name)
rename = wrap(rename, [('checkCapability', 'admin'), 'something','something', additional('text')])
2006-06-26 17:57:20 +00:00
def list(self, irc, msg, args, name):
"""[abbreviation]
List defined bugtrackers. If [abbreviation] is specified, list the
information for that bugtracker.
"""
if name:
name = name.lower()
try:
name = self.shorthand[name]
(url, description, type) = (self.db[name].url, self.db[name].description,
self.db[name].__class__.__name__)
irc.reply('%s: %s, %s [%s]' % (name, description, url, type))
except KeyError:
s = self.registryValue('replyNoBugtracker', ircutils.isChannel(msg.args[0]) and msg.args[0] or None)
2006-06-26 17:57:20 +00:00
irc.error(s % name)
else:
if self.db:
L = self.db.keys()
L.sort()
irc.reply(utils.str.commaAndify(L))
else:
irc.reply('I have no defined bugtrackers.')
list = wrap(list, [additional('text')])
def bugSnarfer(self, irc, msg, match):
r"""\b(?P<bt>(([a-z0-9]+)?\s+bugs?|[a-z0-9]+):?)\s+#?(?P<bug>\d+(?!\d*[\-\.]\d+)((,|\s*(and|en|et|und|ir))\s*#?\d+(?!\d*[\-\.]\d+))*)"""
channel = ircutils.isChannel(msg.args[0]) and msg.args[0] or None
if not self.registryValue('bugSnarfer', channel):
2006-06-26 17:57:20 +00:00
return
2007-02-04 22:10:10 +00:00
nbugs = msg.tagged('nbugs')
if not nbugs: nbugs = 0
if nbugs >= 5:
return
2006-06-26 17:57:20 +00:00
# Don't double on commands
s = str(msg).split(':')[2]
if s and s[0] in str(conf.supybot.reply.whenAddressedBy.chars):
2006-06-26 17:57:20 +00:00
return
2006-06-26 17:57:20 +00:00
sure_bug = match.group('bt').endswith('bug') or match.group('bt').endswith('bug')
2006-06-26 17:57:20 +00:00
# Get tracker name
bugids = match.group('bug')
reps = ((' ',''),('#',''),('and',','),('en',','),('et',','),('und',','),('ir',','))
2006-06-26 17:57:20 +00:00
for r in reps:
bugids = bugids.replace(r[0],r[1])
2007-02-04 22:10:10 +00:00
bugids = bugids.split(',')[:5-nbugs]
# Begin HACK
# strings like "ubuntu 1004" and "ubuntu 1010" are false triggers for us
# filter out bug number that are 4 numbers, start with '1' and end in '04' or '10
# (let's fix this for 2020 ;)
if match.group('bt').lower() == 'ubuntu':
bugids = filter(lambda bugnum: not (len(bugnum) == 4 and bugnum[0] == '1' and bugnum[2:] in ('04', '10')), bugids)
# End HACK
if not sure_bug:
bugids = [x for x in bugids if int(x) > 100]
bugids = list(set(bugids)) ## remove dups
2007-02-04 22:10:10 +00:00
msg.tag('nbugs', nbugs + len(bugids))
2006-06-26 17:57:20 +00:00
bt = map(lambda x: x.lower(), match.group('bt').split())
# Strip off trailing ':' from the tracker name. Allows for (LP: #nnnnnn)
if bt and bt[0].endswith(':'):
bt[0] = bt[:-1]
2006-06-26 17:57:20 +00:00
name = ''
if len(bt) == 1 and not (bt[0] in ['bug','bugs']):
try:
name = bt[0]
2006-06-26 17:57:20 +00:00
tracker = self.db[name]
except:
return
elif len(bt) == 2:
try:
name = bt[0]
2006-06-26 17:57:20 +00:00
tracker = self.db[name]
except:
name = ''
pass
if not name:
snarfTarget = self.registryValue('snarfTarget', channel)
2006-06-26 17:57:20 +00:00
if not snarfTarget:
self.log.warning("Bugtracker: no snarfTarget for Bugtracker")
2006-06-26 17:57:20 +00:00
return
try:
name = self.shorthand[snarfTarget.lower()]
except:
s = self.registryValue('replyNoBugtracker', name)
irc.error(s % name)
try:
tracker = self.db[name]
except KeyError:
s = self.registryValue('replyNoBugtracker', name)
irc.error(s % name)
else:
for bugid in bugids:
bugid = int(bugid)
try:
report = self.get_bug(channel,tracker,bugid,self.registryValue('showassignee', channel))
except BugNotFoundError:
2007-02-04 22:10:10 +00:00
if self.registryValue('replyWhenNotFound'):
irc.error("%s bug %d could not be found" % (tracker.description, bugid))
2006-06-26 17:57:20 +00:00
except BugtrackerError, e:
2006-10-19 14:39:11 +00:00
if 'private' in str(e):
irc.reply("Bug %d on http://launchpad.net/bugs/%d is private" % (bugid, bugid))
return
2006-06-26 17:57:20 +00:00
if not sure_bug and bugid < 30:
return
irc.error(str(e))
else:
2006-12-27 14:33:59 +00:00
for r in report:
irc.reply(makeClean(r), prefixNick=False)
2006-06-26 17:57:20 +00:00
def turlSnarfer(self, irc, msg, match):
r"(?P<tracker>https?://\S*?)/(?:Bugs/0*|str.php\?L|show_bug.cgi\?id=|bugreport.cgi\?bug=|(?:bugs|\+bug)/|ticket/|tracker/|\S*aid=|bug=)?(?P<bug>\d+)(?P<sfurl>&group_id=\d+&at_id=\d+)?"
channel = ircutils.isChannel(msg.args[0]) and msg.args[0] or None
if not self.registryValue('bugSnarfer', channel):
2006-06-26 17:57:20 +00:00
return
2007-02-04 22:10:10 +00:00
nbugs = msg.tagged('nbugs')
if not nbugs: nbugs = 0
if nbugs >= 5:
return
msg.tag('nbugs', nbugs+1)
2006-06-26 17:57:20 +00:00
try:
tracker = self.get_tracker(match.group(0),match.group('sfurl'))
if not tracker:
return
report = self.get_bug(channel, tracker, int(match.group('bug')), self.registryValue('showassignee', channel), do_url = False)
2006-06-26 17:57:20 +00:00
except BugtrackerError, e:
irc.error(str(e))
except BugNotFoundError, e:
irc.error("%s bug %s not found" % (tracker, match.group('bug')))
2006-06-26 17:57:20 +00:00
else:
2006-12-27 14:33:59 +00:00
for r in report:
irc.reply(makeClean(r), prefixNick=False)
2006-06-26 17:57:20 +00:00
# Only useful for launchpad developers
def oopsSnarfer(self, irc, msg, match):
r"(?:https?://pad.lv/){0}?OOPS-(?P<oopsid>\d*[\dA-Z]{3,})"
channel = ircutils.isChannel(msg.args[0]) and msg.args[0] or None
if not self.registryValue('bugSnarfer', channel) or not self.registryValue('oopsSnarfer', channel):
return
2006-06-26 17:57:20 +00:00
oopsid = match.group(1)
if oopsid.lower() == "tools":
return
if not self.is_ok(channel, 'lpoops', oopsid):
return
irc.reply('https://lp-oops.canonical.com/oops.py/?oopsid=' + oopsid, prefixNick=False)
2006-06-26 17:57:20 +00:00
def cveSnarfer(self, irc, msg, match):
r"(cve[- ]\d{4}[- ]\d{4})"
channel = ircutils.isChannel(msg.args[0]) and msg.args[0] or None
if not self.registryValue('bugSnarfer', channel) or not self.registryValue('cveSnarfer', channel):
return
cve = match.group(1).replace(' ','-').upper()
if not self.is_ok(channel, 'cve', cve):
return
url = 'http://cve.mitre.org/cgi-bin/cvename.cgi?name=%s' % cve
cvedata = utils.web.getUrl(url)
m = cvere.search(cvedata)
if m:
cve = m.group(1).replace('\n', ' ')
if len(cve) > 380:
cve = cve[:380] + '...'
irc.reply("%s (%s)" % (cve,url), prefixNick=False)
#TODO: as we will depend on launchpadlib, we should consider using lazr.uri.URI to do URL parsing
def get_tracker(self, snarfurl, sfdata):
2007-02-10 21:47:18 +00:00
snarfurl = snarfurl.replace('sf.net','sourceforge.net')
2006-10-19 14:39:11 +00:00
snarfhost = snarfurl.replace('http://','').replace('https://','')
# Begin HACK
# launchpad.net has many URLs that can confuse us
# make sure 'bug' in in the URL, somewhere
if 'launchpad' in snarfhost:
if not 'bug' in snarfhost: # Not a bug URL
return None
if snarfhost.startswith('pad.lv'): # Launchpad URL shortening
2011-03-29 19:40:53 +00:00
snarfhost = snarfhost[:snarfhost.rfind('/')]
snarfhost = '/'.join( (_ for _ in snarfhost.split('/') if _) )
if '/' in snarfhost: # it's not a bug URL
return None
2011-03-29 19:40:53 +00:00
return self.db.get('launchpad', None)
# End HACK
# At this point, we are only interested in the host part of the URL
2006-10-19 14:39:11 +00:00
if '/' in snarfurl:
snarfhost = snarfhost[:snarfhost.index('/')]
if 'sourceforge.net' in snarfurl: # See below
return None
2006-06-26 17:57:20 +00:00
for t in self.db.keys():
tracker = self.db.get(t, None)
if not tracker:
self.log.error("No tracker for key %r" % t)
continue
2006-06-26 17:57:20 +00:00
url = tracker.url.replace('http://','').replace('https://','')
2006-06-26 17:57:20 +00:00
if 'sourceforge.net' in url:
# sourceforge.net has no API or structured bug exporting, HTML
# scraping is not good enough. Especially as SF keep changing it
continue
2006-06-26 17:57:20 +00:00
if '/' in url:
url = url[:url.index('/')]
2006-10-19 14:39:11 +00:00
if url in snarfhost:
2006-06-26 17:57:20 +00:00
return tracker
if snarfhost == 'pad.lv': # Launchpad URL shortening
return self.db.get('lp', None)
2006-06-26 17:57:20 +00:00
# No tracker found, bummer. Let's try and add one
if 'show_bug.cgi' in snarfurl:
tracker = Bugzilla().get_tracker(snarfurl)
if tracker:
self.db[tracker.name] = tracker
self.shorthand = utils.abbrev(self.db.keys())
return tracker
return None
def get_bug(self, channel, tracker, id, do_assignee, do_url = True):
2006-12-27 14:33:59 +00:00
reports = []
if not self.is_ok(channel, tracker, id):
return []
2006-12-27 14:33:59 +00:00
for r in tracker.get_bug(id):
showext = self.registryValue('extended', channel)
extinfo = ''
if len(r) == 8:
(bid, product, title, severity, status, assignee, url, extinfo) = r
else:
(bid, product, title, severity, status, assignee, url) = r
2006-12-27 14:33:59 +00:00
severity = severity[0].upper() + severity[1:].lower()
status = status[0].upper() + status[1:].lower()
if not do_url:
url = ''
if product:
if showext:
reports.append("%s bug %s in %s \"%s\" %s [%s,%s] %s" % (tracker.description, bid, product,
title, extinfo, severity, status, url))
else:
reports.append("%s bug %s in %s \"%s\" [%s,%s] %s" % (tracker.description, bid, product,
title, severity, status, url))
2006-12-27 14:33:59 +00:00
else:
if showext:
2010-05-19 13:56:46 +00:00
reports.append("%s bug %s \"%s\" %s [%s,%s] %s" % (tracker.description, bid, title, extinfo, severity, status, url))
else:
reports.append("%s bug %s \"%s\" [%s,%s] %s" % (tracker.description, bid, title, severity, status, url))
2006-12-27 14:33:59 +00:00
if do_assignee and assignee:
reports[-1] = reports[-1] + (" - Assigned to %s" % assignee)
return reports
2006-06-26 17:57:20 +00:00
# Define all bugtrackers
class IBugtracker:
def __init__(self, name=None, url=None, description=None):
self.name = name
self.url = url
self.description = description
self.log = supylog # Convenience log wrapper
2006-06-26 17:57:20 +00:00
def get_bug(self, id):
raise BugTrackerError("Bugtracker class does not implement get_bug")
def get_tracker(self, url):
raise BugTrackerError("Bugtracker class does not implement get_tracker")
def __str__(self):
return '%s(%s)' % (self.__class__.__name__, self.url)
def __hash__(self):
return hash(self.url)
def __cmp__(self, other): # used implicitly in Bugtracker.is_ok()
return cmp(hash(self), hash(other))
2006-06-26 17:57:20 +00:00
class Bugzilla(IBugtracker):
def get_tracker(self, url):
url = url.replace('show_bug','xml')
try:
bugxml = utils.web.getUrl(url)
tree = minidom.parseString(bugxml)
url = str(tree.getElementsByTagName('bugzilla')[0].attributes['urlbase'].childNodes[0].data)
if url[-1] == '/':
url = url[:-1]
name = url[url.find('//') + 2:]
if '/' in name:
name = name[:name.find('/')]
desc = name
registerBugtracker(name, url, desc, 'bugzilla')
tracker = Bugzilla(name, url, desc)
return tracker
except:
return None
def get_bug(self, id):
url = "%s/xml.cgi?id=%d" % (self.url,id)
try:
bugxml = utils.web.getUrl(url)
zilladom = minidom.parseString(bugxml)
except Exception, e:
s = 'Could not parse XML returned by %s: %s (%s)' % (self.description, e, url)
2006-06-26 17:57:20 +00:00
raise BugtrackerError, s
bug_n = zilladom.getElementsByTagName('bug')[0]
if bug_n.hasAttribute('error'):
errtxt = bug_n.getAttribute('error')
if errtxt == 'NotFound':
raise BugNotFoundError
2006-06-26 17:57:20 +00:00
s = 'Error getting %s bug #%s: %s' % (self.description, id, errtxt)
raise BugtrackerError, s
try:
title = _getnodetxt(bug_n.getElementsByTagName('short_desc')[0])
status = _getnodetxt(bug_n.getElementsByTagName('bug_status')[0])
try:
status += ": " + _getnodetxt(bug_n.getElementsByTagName('resolution')[0])
except:
pass
component = _getnodetxt(bug_n.getElementsByTagName('component')[0])
severity = _getnodetxt(bug_n.getElementsByTagName('bug_severity')[0])
assignee = '(unavailable)'
try:
assignee = _getnodetxt(bug_n.getElementsByTagName('assigned_to')[0])
except:
pass
2006-06-26 17:57:20 +00:00
except Exception, e:
s = 'Could not parse XML returned by %s bugzilla: %s (%s)' % (self.description, e, url)
2006-06-26 17:57:20 +00:00
raise BugtrackerError, s
2006-12-27 14:33:59 +00:00
return [(id, component, title, severity, status, assignee, "%s/show_bug.cgi?id=%d" % (self.url, id))]
2006-06-26 17:57:20 +00:00
class Issuezilla(IBugtracker):
def get_bug(self, id):
url = "%s/xml.cgi?id=%d" % (self.url,id)
try:
bugxml = utils.web.getUrl(url)
zilladom = minidom.parseString(bugxml)
except Exception, e:
s = 'Could not parse XML returned by %s: %s (%s)' % (self.description, e, url)
2006-06-26 17:57:20 +00:00
raise BugtrackerError, s
bug_n = zilladom.getElementsByTagName('issue')[0]
if not (bug_n.getAttribute('status_code') == '200'):
if bug_n.getAttribute('status_message') == 'NotFound':
raise BugNotFoundError
2006-06-26 17:57:20 +00:00
s = 'Error getting %s bug #%s: %s' % (self.description, id, bug_n.getAttribute('status_message'))
raise BugtrackerError, s
try:
title = _getnodetxt(bug_n.getElementsByTagName('short_desc')[0])
status = _getnodetxt(bug_n.getElementsByTagName('issue_status')[0])
try:
status += ": " + _getnodetxt(bug_n.getElementsByTagName('resolution')[0])
except:
pass
component = _getnodetxt(bug_n.getElementsByTagName('component')[0])
severity = _getnodetxt(bug_n.getElementsByTagName('issue_type')[0])
2006-12-27 14:33:59 +00:00
assignee = _getnodetxt(bug_n.getElementsByTagName('assigned_to')[0])
2006-06-26 17:57:20 +00:00
except Exception, e:
s = 'Could not parse XML returned by %s bugzilla: %s (%s)' % (self.description, e, url)
2006-06-26 17:57:20 +00:00
raise BugtrackerError, s
2006-12-27 14:33:59 +00:00
return [(id, component, title, severity, status, assignee, "%s/show_bug.cgi?id=%d" % (self.url, id))]
2006-06-26 17:57:20 +00:00
class Launchpad(IBugtracker):
statuses = ["Unknown", "Invalid", "Opinion", "Won't Fix", "Fix Released", "Fix Committed", "New", "Incomplete", "Confirmed", "Triaged", "In Progress"]
severities = ["Unknown", "Undecided", "Wishlist", "Low", "Medium", "High", "Critical"]
def __init__(self, *args, **kwargs):
IBugtracker.__init__(self, *args, **kwargs)
self.lp = None
# A word to the wise:
# The Launchpad API is much better than the /+text interface we currently use,
# it's faster and easier to get the information we need.
# The current /+text interface is not really maintained by Launchpad and most,
# or all, of the Launchpad developers hate it. For this reason, we are dropping
# support for /+text in the future in favour of launchpadlib.
# Terence Simpson (tsimpson) 2010-04-20
try: # Attempt to use launchpadlib, python bindings for the Launchpad API
from launchpadlib.launchpad import Launchpad
cachedir = os.path.join(conf.supybot.directories.data.tmp(), 'lpcache')
if hasattr(Launchpad, 'login_anonymously'):
self.lp = Launchpad.login_anonymously("Ubuntu Bots - Bugtracker", 'production', cachedir)
else: #NOTE: Most people should have a launchpadlib new enough for .login_anonymously
self.lp = Launchpad.login("Ubuntu Bots - Bugtracker", '', '', 'production', cahedir)
except ImportError:
# Ask for launchpadlib to be installed
supylog.warning("Please install python-launchpadlib, the old interface is deprecated")
except Exception: # Something unexpected happened
self.lp = None
supylog.exception("Unknown exception while accessing the Launchpad API")
def _parse(self, task): #Depricated
2006-06-26 17:57:20 +00:00
parser = email.FeedParser.FeedParser()
parser.feed(task)
return parser.close()
def get_bug(self, id): #TODO: Remove this method and rename 'get_new_bug' to 'get_bug'
if self.lp:
return self.get_bug_new(id)
return self.get_bug_old(id)
@classmethod
def _sort(cls, task1, task2):
task1_status = task1.status
task1_importance = task1.importance
task2_status = task2.status
task2_importance = task2.importance
if task1_status not in cls.statuses:
supylog.error("%r is an unknown status for Launchapd, update %s.statuses" % (task1_status, getattr(cls, '__name__', 'Launchpad')))
if task2_status not in cls.statuses:
supylog.error("%r is an unknown status for Launchapd, update %s.statuses" % (task1_status, getattr(cls, '__name__', 'Launchpad')))
return -1
return 1
if task1_importance not in cls.severities:
supylog.error("%r is an unknown status for Launchapd, update %s.severities" % (task1_importance, getattr(cls, '__name__', 'Launchpad')))
if task2_importance not in cls.severities:
supylog.error("%r is an unknown status for Launchapd, update %s.severities" % (task1_importance, getattr(cls, '__name__', 'Launchpad')))
return -1
return 1
if task1_status != task2_status:
if cls.statuses.index(task1_status) < cls.statuses.index(task2_status):
return -1
return 1
if task1_importance != task2_importance:
if cls.severities.index(task1_importance) < cls.severities.index(task2_importance):
return -1
return 1
return 0
@classmethod
def _old_sort(cls, task1, task2): #Depricated
2006-06-26 17:57:20 +00:00
# Status sort:
try:
if task1['status'] not in cls.statuses and task2['status'] in cls.statuses: return -1
if task1['status'] in cls.statuses and task2['status'] not in cls.statuses: return 1
if task1['importance'] not in cls.severities and task2['importance'] in cls.severities: return -1
if task1['importance'] in cls.severities and task2['importance'] not in cls.severities: return 1
2006-06-26 17:57:20 +00:00
if not (task1['status'] == task2['status']):
if cls.statuses.index(task1['status']) < cls.statuses.index(task2['status']):
2006-06-26 17:57:20 +00:00
return -1
return 1
if not (task1['importance'] == task2['importance']):
if cls.severities.index(task1['importance']) < cls.severities.index(task2['importance']):
2006-06-26 17:57:20 +00:00
return -1
return 1
except: # Launchpad changed again?
return 0
return 0
def get_bug_new(self, id): #TODO: Rename this method to 'get_bug'
try:
bugdata = self.lp.bugs[id]
if bugdata.private:
raise BugtrackerError, "This bug is private"
dup = bugdata.duplicate_of
affected = bugdata.users_affected_count_with_dupes
heat = bugdata.heat
tasks = bugdata.bug_tasks
if tasks.total_size != 1:
tasks = list(tasks)
tasks.sort(self._sort)
taskdata = tasks[-1]
else:
taskdata = tasks[0]
assignee = taskdata.assignee
t = taskdata.bug_target_display_name #task name
if assignee: # "Diaplay Name (Launchpad ID)"
assignee = u"%s (%s)" % (assignee.display_name, assignee.name)
else:
assignee = ''
except Exception, e:
#NOTE: The LP API will raise a lazr.restfulclient.errors.HTTPError with private bugs, so bugdata.private is useless..., do the checking here instead
if e.__class__.__name__ == 'HTTPError': # messy, but meh
raise BugtrackerError, e.content # should be 'Bug nnnnn is private'
elif isinstance(e, KeyError):
raise BugNotFoundError
supylog.exception("Error gathering bug data for %s bug %d" % (self.description, id))
s = 'Could not parse data returned by %s: %s (%s/bugs/%d)' % (self.description, e, self.url, id)
raise BugtrackerError, s
extinfo = "(affected: %d, heat: %d)" % (affected, heat)
if dup:
dupbug = self.get_bug(dup.id)
return [(id, t, bugdata.title + (' (dup-of: %d)' % dup.id), taskdata.importance,
taskdata.status, assignee, "%s/bugs/%s" % (self.url, id))] + dupbug
return [(id, t, bugdata.title, taskdata.importance, taskdata.status,
assignee, "%s/bugs/%s" % (self.url, id), extinfo)]
def get_bug_old(self, id): #Depricated
if id == 1:
raise BugtrackerError, "https://bugs.launchpad.net/ubuntu/+bug/1 (Not reporting large bug)"
2006-06-26 17:57:20 +00:00
try:
bugdata = utils.web.getUrl("%s/bugs/%d/+text" % (self.url,id))
2006-06-26 17:57:20 +00:00
except Exception, e:
if '404' in str(e):
raise BugNotFoundError
s = 'Could not parse data returned by %s: %s (%s/bugs/%d)' % (self.description, e, self.url, id)
2006-06-26 17:57:20 +00:00
raise BugtrackerError, s
summary = {}
# Trap private bugs
if "<!-- 4a. didn't try to log in last time: -->" in bugdata:
raise BugtrackerError, "This bug is private"
try:
# Split bug data into separate pieces (bug data, task data)
data = bugdata.split('\n\n')
bugdata = data[0]
taskdata = data[1:]
parser = email.FeedParser.FeedParser()
parser.feed(bugdata)
bugdata = parser.close()
taskdata = map(self._parse, taskdata)
taskdata.sort(self._old_sort)
2006-06-26 17:57:20 +00:00
taskdata = taskdata[-1]
except Exception, e:
s = 'Could not parse data returned by %s: %s (%s/bugs/%d)' % (self.description, e, self.url, id)
2006-06-26 17:57:20 +00:00
raise BugtrackerError, s
2006-12-27 14:33:59 +00:00
# Try and find duplicates
2006-06-26 17:57:20 +00:00
t = taskdata['task']
if '(' in t:
t = t[:t.rfind('(') -1]
2006-12-27 14:33:59 +00:00
if bugdata['duplicate-of']:
dupbug = self.get_bug(int(bugdata['duplicate-of']))
return [(id, t, bugdata['title'] + (' (dup-of: %d)' % dupbug[0][0]), taskdata['importance'],
taskdata['status'], taskdata['assignee'], "%s/bugs/%s" % (self.url, id))] + dupbug
2006-12-27 14:33:59 +00:00
return [(id, t, bugdata['title'], taskdata['importance'],
taskdata['status'], taskdata['assignee'], "%s/bugs/%s" % (self.url, id))]
2006-06-26 17:57:20 +00:00
# <rant>
# Debbugs sucks donkeyballs
# * HTML pages are inconsistent
# * Parsing mboxes gets incorrect with cloning perversions (eg with bug 330000)
# * No sane way of accessing bug reports in a machine readable way (bts2ldap
# has no search on bugid)
# * The damn thing allow incomplete bugs, eg bugs without severity set. WTF?!?
2006-06-26 17:57:20 +00:00
#
# Fortunately bugs.donarmstrong.com has a SOAP interface which we can use.
2006-06-26 17:57:20 +00:00
# </rant>
class Debbugs(IBugtracker):
def __init__(self, *args, **kwargs):
IBugtracker.__init__(self, *args, **kwargs)
2008-01-19 17:57:32 +00:00
self.soap_proxy = SOAPpy.SOAPProxy("bugs.debian.org/cgi-bin/soap.cgi", "Debbugs/SOAP/Status")
self.soap_proxy.soapaction = "Debbugs/SOAP/Status#get_status"
2006-06-26 17:57:20 +00:00
def get_bug(self, id):
bug_url = "http://bugs.debian.org/cgi-bin/bugreport.cgi?bug=%d" % id
2006-06-26 17:57:20 +00:00
try:
raw = self.soap_proxy.get_status(id)
2006-06-26 17:57:20 +00:00
except Exception, e:
s = 'Could not parse data returned by %s: %s' % (self.description, e)
raise BugtrackerError, s
if not raw:
raise BugNotFoundError
raw = raw['item']['value']
2006-06-26 17:57:20 +00:00
try:
if len(raw['fixed_versions']):
status = 'Fixed'
else:
status = 'Open'
return [(id, raw['package'], raw['subject'], raw['severity'], status, '', "%s/%s" % (self.url, id))]
2006-06-26 17:57:20 +00:00
except Exception, e:
s = 'Could not parse data returned by %s bugtracker: %s (%s)' % (self.description, e, bug_url)
2006-06-26 17:57:20 +00:00
raise BugtrackerError, s
class Mantis(IBugtracker):
def __init__(self, *args, **kwargs):
IBugtracker.__init__(self, *args, **kwargs)
self.soap_proxy = SOAPpy.SOAPProxy(self.url + "/api/soap/mantisconnect.php", "http://futureware.biz/mantisconnect")
self.soap_proxy.soapaction = "http://futureware.biz/mantisconnect#mc_issue_get"
def get_bug(self, id):
url = self.url + "/view.php?id=%i" % id
try:
raw = self.soap_proxy.mc_issue_get('', "", id)
except Exception, e:
s = 'Could not parse data returned by %s: %s (%s)' % (self.description, e, url)
raise BugtrackerError, s
if not raw:
raise BugNotFoundError
try:
return [(id, raw['project']['name'], raw['summary'], raw['priority']['name'], raw['resolution']['name'], '', url)]
except Exception, e:
s = 'Could not parse data returned by %s bugtracker: %s (%s)' % (self.description, e, url)
raise BugtrackerError, s
# For trac based trackers we get the tab-separated-values format.
# The other option is a comma-separated-values format, but if the description
# has commas, things get tricky.
# This should be more robust than the screen-scraping done previously.
2006-06-26 17:57:20 +00:00
class Trac(IBugtracker):
def get_bug(self, id): # This is still a little rough, but it works :)
bug_url = "%s/%d" % (self.url, id)
2006-06-26 17:57:20 +00:00
try:
raw = utils.web.getUrl("%s?format=tab" % bug_url)
2006-06-26 17:57:20 +00:00
except Exception, e:
if 'HTTP Error 500' in str(e):
raise BugNotFoundError
s = 'Could not parse data returned by %s: %s' % (self.description, e, bug_url)
2006-06-26 17:57:20 +00:00
raise BugtrackerError, s
raw = raw.replace("\r\n", '\n')
(headers, rest) = raw.split('\n', 1)
headers = headers.strip().split('\t')
rest = rest.strip().split('\t')
title = status = package = severity = assignee = "Unknown"
if "summary" in headers:
title = rest[headers.index("summary")]
if "status" in headers:
status = rest[headers.index("status")]
if "component" in headers:
package = rest[headers.index("component")]
if "severity" in headers:
severity = rest[headers.index("severity")]
if "owner" in headers:
assingee = rest[headers.index("owner")]
if severity == "Unknown" and "priority" in headers:
severity = rest[headers.index("priority")]
return [(id, package, title, severity, status, assignee, bug_url)]
class WikiForms(IBugtracker):
def get_bug(self, id):
def strip_tags(s):
while '<' in s and '>' in s:
s = str(s[:s.find('<')]) + str(s[s.find('>')+1:])
return s
url = "%s/%05d" % (self.url, id)
try:
bugdata = utils.web.getUrl(url)
except Exception, e:
if 'HTTP Error 404' in str(e):
raise BugNotFoundError
s = 'Could not parse data returned by %s: %s (%s)' % (self.description, e, url)
raise BugtrackerError, s
for l in bugdata.split("\n"):
l2 = l.lower()
if '<dt>importance</dt>' in l2:
severity = 'Importance ' + strip_tags(l[l.find('<dd>')+4:])
if '<dt>summary</dt>' in l2:
title = strip_tags(l[l.find('<dd>')+4:])
if '<dt>status</dt>' in l2:
status = strip_tags(l[l.find('<dd>')+4:])
if '<dt>category</dt>' in l2:
package = strip_tags(l[l.find('<dd>')+4:])
2006-12-27 14:33:59 +00:00
return [(id, package, title, severity, status, '', "%s/%05d" % (self.url, id))]
2006-06-26 17:57:20 +00:00
class Str(IBugtracker):
def get_bug(self, id):
def strip_tags(s):
while '<' in s and '>' in s:
s = str(s[:s.find('<')]) + str(s[s.find('>')+1:])
return s
url = "%s?L%d" % (self.url, id)
try:
bugdata = utils.web.getUrl(url)
except Exception, e:
s = 'Could not parse data returned by %s: %s (%s)' % (self.description, e, url)
raise BugtrackerError, s
for l in bugdata.split("\n"):
l2 = l.lower()
if 'nowrap>priority:</th>' in l2:
severity = 'Priority ' + l[l.find(' - ')+3:min(l.find(','),l.find('</td>'))]
if '>application:</th>' in l2:
package = l[l.find('<td>')+4:l.find('</td>')]
if 'nowrap>status:</th>' in l2:
status = l[l.find(' - ')+3:l.find('</td>')]
if 'nowrap>summary:</th>' in l2:
title = l[l.find('<td>')+4:l.find('</td>')]
if 'nowrap>assigned to:</th>' in l2:
assignee = strip_tags(l[l.find('<td>')+4:l.find('</td>')])
if assignee == 'Unassigned':
assignee = 'nobody'
2007-02-04 17:35:40 +00:00
return [(id, package, title, severity, status, assignee, "%s?L%d" % (self.url, id))]
2006-06-26 17:57:20 +00:00
sfre = re.compile(r"""
.*?
<h2>\[.*?\]\s*(?P<title>.*?)</h2>
.*?
2007-02-04 22:10:10 +00:00
assigned.*?<br>\s+(?P<assignee>\S+)
2006-12-27 14:33:59 +00:00
.*?
2007-02-04 22:10:10 +00:00
priority.*?(?P<priority>\d+)
2006-06-26 17:57:20 +00:00
.*?
2007-02-04 22:10:10 +00:00
status.*?<br>\s+(?P<status>\S+)
2006-06-26 17:57:20 +00:00
.*?
2007-02-04 22:10:10 +00:00
resolution.*?<br>\s+(?P<resolution>\S+)
2006-06-26 17:57:20 +00:00
.*?
""", re.VERBOSE | re.DOTALL | re.I)
#NOTE: Until sf.net has a way to export formatted bug data, this will remain broken and unmaintained
2006-06-26 17:57:20 +00:00
class Sourceforge(IBugtracker):
_sf_url = 'http://sf.net/support/tracker.php?aid=%d'
def get_bug(self, id):
url = self._sf_url % id
try:
bugdata = utils.web.getUrl(url)
except Exception, e:
s = 'Could not parse data returned by %s: %s (%s)' % (self.description, e, url)
2006-06-26 17:57:20 +00:00
raise BugtrackerError, s
try:
reo = sfre.search(bugdata)
status = reo.group('status')
resolution = reo.group('resolution')
if not (resolution.lower() == 'none'):
status += ' ' + resolution
2006-12-27 14:33:59 +00:00
return [(id, None, reo.group('title'), "Pri: %s" % reo.group('priority'), status, reo.group('assignee'),self._sf_url % id)]
2006-06-26 17:57:20 +00:00
except:
raise BugNotFoundError
2006-06-26 17:57:20 +00:00
# Introspection is quite cool
defined_bugtrackers = {}
v = vars()
for k in v.keys():
if type(v[k]) == type(IBugtracker) and issubclass(v[k], IBugtracker) and not (v[k] == IBugtracker):
defined_bugtrackers[k.lower()] = v[k]
registerBugtracker('mozilla', 'http://bugzilla.mozilla.org', 'Mozilla', 'bugzilla')
registerBugtracker('ubuntu', 'https://launchpad.net', 'Ubuntu', 'launchpad')
2006-06-26 17:57:20 +00:00
registerBugtracker('gnome', 'http://bugzilla.gnome.org', 'Gnome', 'bugzilla')
registerBugtracker('gnome2', 'http://bugs.gnome.org', 'Gnome', 'bugzilla')
registerBugtracker('kde', 'http://bugs.kde.org', 'KDE', 'bugzilla')
registerBugtracker('ximian', 'http://bugzilla.ximian.com', 'Ximian', 'bugzilla')
registerBugtracker('freedesktop', 'http://bugzilla.freedesktop.org', 'Freedesktop', 'bugzilla')
registerBugtracker('freedesktop2', 'http://bugs.freedesktop.org', 'Freedesktop', 'bugzilla')
registerBugtracker('openoffice', 'http://openoffice.org/issues', 'OpenOffice.org', 'issuezilla')
registerBugtracker('launchpad', 'https://launchpad.net', 'Launchpad', 'launchpad')
registerBugtracker('lp', 'https://launchpad.net', 'Launchpad', 'launchpad')
registerBugtracker('malone', 'https://launchpad.net', 'Launchpad', 'launchpad')
2006-06-26 17:57:20 +00:00
registerBugtracker('debian', 'http://bugs.debian.org', 'Debian', 'debbugs')
2007-02-04 22:10:10 +00:00
registerBugtracker('trac', 'http://trac.edgewall.org/ticket', 'Trac', 'trac')
2006-06-26 17:57:20 +00:00
registerBugtracker('django', 'http://code.djangoproject.com/ticket', 'Django', 'trac')
registerBugtracker('cups', 'http://www.cups.org/str.php', 'CUPS', 'str')
registerBugtracker('gnewsense', 'http://bugs.gnewsense.org/Bugs', 'gNewSense', 'wikiforms')
2006-06-26 17:57:20 +00:00
registerBugtracker('supybot', 'http://sourceforge.net/tracker/?group_id=58965&atid=489447', 'Supybot', 'sourceforge')
registerBugtracker('mantis', "http://www.mantisbt.org/bugs", "Mantis", 'mantis')
registerBugtracker('ubottu', 'https://launchpad.net', 'Ubottu', 'launchpad')
# Don't delete this one
2006-06-26 17:57:20 +00:00
registerBugtracker('sourceforge', 'http://sourceforge.net/tracker/', 'Sourceforge', 'sourceforge')
Class = Bugtracker