Made bugtracker plugin more usable

Some copyright notices
This commit is contained in:
Dennis Kaarsemaker 2007-01-24 00:42:45 +01:00
parent 3ca25ef2d1
commit 4ec4305038
11 changed files with 160 additions and 68 deletions

View File

@ -2,7 +2,7 @@
This plugin can store all kick/ban/remove/mute actions
"""
###
# Copyright (c) 2005,2006 Dennis Kaarsemaker
# Copyright (c) 2005-2007 Dennis Kaarsemaker
#
# 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
@ -18,7 +18,7 @@ This plugin can store all kick/ban/remove/mute actions
import supybot
import supybot.world as world
__version__ = "0.2"
__version__ = "0.3"
__author__ = supybot.Author("Dennis Kaarsemaker","Seveas","dennis@kaarsemaker.net")
__contributors__ = {}
__url__ = 'https://bots.ubuntulinux.nl'

View File

@ -1,6 +1,6 @@
#!/usr/bin/python
###
# Copyright (c) 2005,2006 Dennis Kaarsemaker
# Copyright (c) 2005-2007 Dennis Kaarsemaker
#
# 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

View File

@ -1,5 +1,5 @@
###
# Copyright (c) 2006 Dennis Kaarsemaker
# Copyright (c) 2006,2007 Dennis Kaarsemaker
#
# 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

View File

@ -1,6 +1,6 @@
#!/usr/bin/python
#
# Copyright (c) 2005,2006 Dennis Kaarsemaker
# Copyright (c) 2005-2007 Dennis Kaarsemaker
#
# 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

View File

@ -1,5 +1,5 @@
###
# Copyright (c) 2006 Dennis Kaarsemaker
# Copyright (c) 2006,2007 Dennis Kaarsemaker
#
# 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
@ -73,6 +73,12 @@ class Bantracker(callbacks.Plugin):
else:
self.db = None
def die(self):
self.db.close()
def reset(self):
self.db.close()
def __call__(self, irc, msg):
try:
super(self.__class__, self).__call__(irc, msg)
@ -88,7 +94,7 @@ class Bantracker(callbacks.Plugin):
cur.execute(query, parms)
data = None
if expect_result: data = cur.fetchall()
if expect_id: data = con.insert_id()
if expect_id: data = self.db.insert_id()
self.db.commit()
return data
@ -121,17 +127,17 @@ class Bantracker(callbacks.Plugin):
if not self.registryValue('enabled', channel):
return
n = now()
id = db_run("INSERT INTO bans (channel, mask, operator, time, log) values(%s, %s, %s, %s, %s)",
id = self.db_run("INSERT INTO bans (channel, mask, operator, time, log) values(%s, %s, %s, %s, %s)",
(channel, target, nick, n, '\n'.join(self.logs[channel])), expect_id=True)
if kickmsg and id and not (kickmsg == nick):
db_run("INSERT INTO comments (ban_id, who, comment, time) values(%s,%s,%s,%s)", (id, nick, kickmsg, n))
self.db_run("INSERT INTO comments (ban_id, who, comment, time) values(%s,%s,%s,%s)", (id, nick, kickmsg, n))
def doUnban(self, irc, channel, nick, mask):
if not self.registryValue('enabled', channel):
return
data = db_run("SELECT MAX(id) FROM bans where channel=%s and mask=%s", (channel, mask), expect_result=True)
data = self.db_run("SELECT MAX(id) FROM bans where channel=%s and mask=%s", (channel, mask), expect_result=True)
if len(data) and not (data[0][0] == None):
db_run("UPDATE bans SET removal=%s , removal_op=%s WHERE id=%s", (now(), nick, int(data[0][0])))
self.db_run("UPDATE bans SET removal=%s , removal_op=%s WHERE id=%s", (now(), nick, int(data[0][0])))
def doPrivmsg(self, irc, msg):
(recipients, text) = msg.args

View File

@ -1,4 +1,4 @@
Copyright (c) 2005-2006, Dennis Kaarsemaker
Copyright (c) 2005-2007, Dennis Kaarsemaker
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
@ -34,3 +34,40 @@ bug #123
supybot bug 123
bug 123, 4, 5
bug 1, 3 and 89
To rename a bugtracker:
@bugtracker rename old-name new-name
To change details of a bugtracker, just add it again and it will overwrite the
existing tracker.
The bug snarfing (responding to bug numbers/urls) will only work in channels
where supybot.plugins.bugtracker.bugsnarfer is True.
Automatic reporting of new bugs is also possible for Malone (the launchpad
bugtracker). Enabling this is not a trivial process. First step is to set the
supybot.plugins.bugtracker.reportercache variable to a dir for this purpose. You
also need a mail account that supports the + hack (mail for foo+bar@baz.com is
automatically delivered to foo@baz.com while the Delivered-To: header is set to
foo+bar@baz.com) which is accessible via IMAP. I know this is a rather strong
requirement, but that's the way it works now. Patches to make it work in other
situations are appreciated.
Anyway, once that is all set up you're almost there. Let's assume the
mailaddress is bugreporter@yourdomain.com. Now pick a tag for your bugreports,
e.g. ubuntu (you can set a different tag per channel) and create a launchpad
account with address bugreporter+ubuntu@yourdomain.com. Activate that account
and make sure it gets bugmail for the product(s) you want to monitor.
Now set the supybot.plugins.bugtracker.bugreporter in the channels where bugs
are to be reported to the value of the tag for bugs to be reported there and
watch bugs flowing in.
To prevent old bugs from showing up when they change or a comment is being
added, you can manually fill the cache. Just touch files in the reporters cache
with the following name:
tag_here/malone/NN/MMMM where NN is int(bugid/1000) and MMMM is the bugid.
If your products already have many bugreports, consider doing some
screenscraping with the malone searchpages and sed/awk :)

View File

@ -1,5 +1,5 @@
###
# Copyright (c) 2005,2006 Dennis Kaarsemaker
# Copyright (c) 2005-2007 Dennis Kaarsemaker
#
# 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
@ -19,7 +19,7 @@ This plugin will display bug information when requested.
import supybot
import supybot.world as world
__version__ = "2.0"
__version__ = "2.5"
__author__ = supybot.Author("Dennis Kaarsemaker","Seveas","dennis@kaarsemaker.net")
__contributors__ = {}
__url__ = 'http://bots.ubuntulinux.nl/'

View File

@ -1,5 +1,5 @@
###
# Copyright (c) 2005,2006 Dennis Kaarsemaker
# Copyright (c) 2005-2007 Dennis Kaarsemaker
#
# 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
@ -20,7 +20,6 @@ def configure(advanced):
from supybot.questions import expect, anything, something, yn
conf.registerPlugin('Bugtracker', True)
Bugtracker = conf.registerPlugin('Bugtracker')
conf.registerChannelValue(conf.supybot.plugins.Bugtracker, 'bugSnarfer',
@ -50,3 +49,15 @@ conf.registerChannelValue(conf.supybot.plugins.Bugtracker, 'repeatdelay',
registry.Integer(60, """Number of seconds to wait between repeated bug calls"""))
conf.registerChannelValue(conf.supybot.plugins.Bugtracker, 'showassignee',
registry.Boolean(False, """Whether to show th assignee in bug reports"""))
conf.registerGlobalValue(conf.supybot.plugins.Bugtracker, 'reportercache',
registry.String('', """Name of the basedir for the bugreporter cache"""))
conf.registerGlobalValue(conf.supybot.plugins.Bugtracker, 'imap_server',
registry.String('', """IMAP server for bugmail account""",private=True))
conf.registerGlobalValue(conf.supybot.plugins.Bugtracker, 'imap_user',
registry.String('', """IMAP user for bugmail account""", private=True))
conf.registerGlobalValue(conf.supybot.plugins.Bugtracker, 'imap_password',
registry.String('', """IMAP password for bugmail account""", private=True))
conf.registerGlobalValue(conf.supybot.plugins.Bugtracker, 'imap_ssl',
registry.Boolean(False, """Use SSL for imap connections"""))

View File

@ -1,5 +1,5 @@
###
# Copyright (c) 2005,2006 Dennis Kaarsemaker
# Copyright (c) 2005-2007 Dennis Kaarsemaker
#
# 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
@ -12,9 +12,8 @@
#
###
import supybot.utils as utils
from supybot.commands import *
import supybot.plugins as plugins
import supybot.utils as utils
import supybot.ircmsgs as ircmsgs
import supybot.ircutils as ircutils
import supybot.callbacks as callbacks
@ -27,11 +26,6 @@ import xml.dom.minidom as minidom
from htmlentitydefs import entitydefs as entities
import email.FeedParser
bugreporter_base = '/home/dennis/ubugtu/data/bugmail'
imap_server = 'localhost'
imap_user = commands.getoutput('cat /home/dennis/ubugtu/imap_user')
imap_password = commands.getoutput('cat /home/dennis/ubugtu/imap_password')
def registerBugtracker(name, url='', description='', trackertype=''):
conf.supybot.plugins.Bugtracker.bugtrackers().add(name)
group = conf.registerGroup(conf.supybot.plugins.Bugtracker.bugtrackers, name)
@ -95,13 +89,15 @@ class Bugtracker(callbacks.PluginRegexp):
else:
raise BugtrackerError("Unknown trackertype: %s" % group.trackertype())
self.shorthand = utils.abbrev(self.db.keys())
try:
schedule.removeEvent(self.name())
except:
pass
schedule.addPeriodicEvent(lambda: self.reportnewbugs(irc), 60, name=self.name())
# Schedule bug reporting
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.shown = {}
self.nomailtime = 0
def die(self):
try:
@ -119,49 +115,68 @@ class Bugtracker(callbacks.PluginRegexp):
return True
return False
def is_new(self, tracker, tag, id):
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):
# Compile list of bugs
tracker = self.db['malone']
self.log.info("Checking for new bugs")
bugs = {}
sc = imaplib.IMAP4_SSL(imap_server)
sc.login(imap_user, imap_password)
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()
fp.feed(msg)
bug = fp.close()
# Determine bug number, component and tag
try:
id = int(bug['Reply-To'].split()[1])
except:
continue
tag = bug['Delivered-To']
if '+' not in tag:
self.log.info('Ignoring e-mail with no detectable bug')
continue
tag = tag[tag.find('+')+1:tag.find('@')]
component = bug['X-Launchpad-Bug']
if 'component' in component:
component = component[component.find('component=')+10:]
component = component[:component.find(';')].replace('None','')
else:
component = ''
if tag not in bugs:
bugs[tag] = {}
if id not in bugs[tag]:
try:
os.makedirs(os.path.join(bugreporter_base,tag,str(int(id/1000))))
except:
pass
if id > 58184 and not os.path.exists(os.path.join(bugreporter_base,tag,str(int(id/1000)),str(id))):
fd2 = open(os.path.join(bugreporter_base,tag,str(int(id/1000)),str(id)),'w')
fd2.close()
# Determine bugtracker type (currently only Malone is supported anyway)
if bug['X-Launchpad-Bug']:
tracker = self.db['malone']
id = int(bug['Reply-To'].split()[1])
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]
except:
self.log.info("Unable to get new bug %d" % id)
pass
else:
self.log.info('Ignoring e-mail with no detectable bug')
for c in irc.state.channels:
tag = self.registryValue('bugReporter', channel=c)
if not tag:
@ -215,6 +230,25 @@ class Bugtracker(callbacks.PluginRegexp):
irc.error(s % name)
remove = wrap(remove, ['text'])
def rename(self, irc, msg, args, oldname, newname):
"""<oldname> <newname>
Rename the bugtracker associated with <oldname> to <newname>
"""
try:
name = self.shorthand[oldname.lower()]
group = self.registryValue('bugtrackers.%s' % name.replace('.','\\.'), value=False)
self.db[newname] = defined_bugtrackers[trackertype](name,group.url(),group.description())
registerBugtracker(newname, group.url(), group.description(), 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', msg.args[0])
irc.error(s % name)
rename = wrap(rename, ['something','something'])
def list(self, irc, msg, args, name):
"""[abbreviation]
@ -244,16 +278,13 @@ class Bugtracker(callbacks.PluginRegexp):
r"""\b(?P<bt>(([a-z]+)?\s+bugs?|[a-z]+))\s+#?(?P<bug>\d+(?!\d*\.\d+)((,|\s*(and|en|et|und|ir))\s*#?\d+(?!\d*\.\d+))*)"""
if msg.args[0][0] == '#' and not self.registryValue('bugSnarfer', msg.args[0]):
return
# Don't double on commands
s = str(msg).split(':')[2]
if s[0] in str(conf.supybot.reply.whenAddressedBy.chars):
return
sure_bug = match.group('bt').endswith('bug') or match.group('bt').endswith('bug')
# FIXME dig into supybot docs/code
#if conf.supybot.reply.whenAddressedBy.strings:
# for p in conf.supybot.reply.whenAddressedBy.strings:
# if s.startswith(str(p)):
# return
# Get tracker name
bugids = match.group('bug')
reps = ((' ',''),('#',''),('and',','),('en',','),('et',','),('und',','),('ir',','))
@ -491,8 +522,7 @@ class Malone(IBugtracker):
bugdata = utils.web.getUrl("%s/%d/+text" % (self.url.replace('malone','bugs'),id))
except Exception, e:
if '404' in str(e):
s = 'Error getting %s bug #%s: Bug does not exist' % (self.description, id)
raise BugtrackerError, s
raise BugNotFoundError
s = 'Could not parse data returned by %s: %s' % (self.description, e)
raise BugtrackerError, s
summary = {}
@ -586,7 +616,7 @@ class Debbugs(IBugtracker):
s = 'Could not parse data returned by %s: %s' % (self.description, e)
raise BugtrackerError, s
if '<p>There is no record of Bug' in bugdata:
raise BugtrackerError, "%s bug %d does not exist" % (self.description, id)
raise BugNotFoundError
try:
data = {'package': 'unknown','title': 'unknown','severity':'unknown','status':'Open'}
for m in bugdata.split("\n\n\nFrom"):
@ -659,7 +689,6 @@ sfre = re.compile(r"""
Resolution.*?<br>\s+(?P<resolution>\S+)
.*?
""", re.VERBOSE | re.DOTALL | re.I)
class Sourceforge(IBugtracker):
_sf_url = 'http://sf.net/support/tracker.php?aid=%d'
def get_bug(self, id):
@ -677,7 +706,7 @@ class Sourceforge(IBugtracker):
status += ' ' + resolution
return [(id, None, reo.group('title'), "Pri: %s" % reo.group('priority'), status, reo.group('assignee'),self._sf_url % id)]
except:
raise BugtrackerError, "Bug not found"
raise BugNotFoundError
# Introspection is quite cool
defined_bugtrackers = {}

5
TODO
View File

@ -1,5 +0,0 @@
Bantracker:
- Documentation
Bugtracker:
- Make sure to use reply.whenAddressedBy

View File

@ -1,3 +1,17 @@
###
# Copyright (c) 2006-2007 Dennis Kaarsemaker
#
# 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.
#
###
import cgi, cgitb, re, sys, math, os, md5, sqlite, random, time, datetime, pytz, Cookie, StringIO
import cPickle as pickle
cgitb.enable()
@ -42,4 +56,4 @@ def send_page(template):
def q(txt):
return txt.replace('&','&amp;').replace('<','&lt;').replace('>','&gt;').replace('"','&quot;')