From 4ec43050383d3f34a7a2b1ec25d6875ae957ca06 Mon Sep 17 00:00:00 2001 From: Dennis Kaarsemaker Date: Wed, 24 Jan 2007 00:42:45 +0100 Subject: [PATCH] Made bugtracker plugin more usable Some copyright notices --- Bantracker/__init__.py | 4 +- Bantracker/bans.cgi | 2 +- Bantracker/config.py | 2 +- Bantracker/lp_auth.py | 2 +- Bantracker/plugin.py | 18 ++++-- Bugtracker/README.txt | 39 ++++++++++++- Bugtracker/__init__.py | 4 +- Bugtracker/config.py | 15 ++++- Bugtracker/plugin.py | 121 +++++++++++++++++++++++++---------------- TODO | 5 -- commoncgi.py | 16 +++++- 11 files changed, 160 insertions(+), 68 deletions(-) delete mode 100644 TODO diff --git a/Bantracker/__init__.py b/Bantracker/__init__.py index 972ac9b..d1a2ecd 100644 --- a/Bantracker/__init__.py +++ b/Bantracker/__init__.py @@ -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' diff --git a/Bantracker/bans.cgi b/Bantracker/bans.cgi index bf94ed8..17a7511 100755 --- a/Bantracker/bans.cgi +++ b/Bantracker/bans.cgi @@ -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 diff --git a/Bantracker/config.py b/Bantracker/config.py index 292ed24..d54cf6e 100644 --- a/Bantracker/config.py +++ b/Bantracker/config.py @@ -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 diff --git a/Bantracker/lp_auth.py b/Bantracker/lp_auth.py index 563107c..a3702ed 100755 --- a/Bantracker/lp_auth.py +++ b/Bantracker/lp_auth.py @@ -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 diff --git a/Bantracker/plugin.py b/Bantracker/plugin.py index eb62fe7..1f3b566 100644 --- a/Bantracker/plugin.py +++ b/Bantracker/plugin.py @@ -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 diff --git a/Bugtracker/README.txt b/Bugtracker/README.txt index 79563aa..a0a8da5 100644 --- a/Bugtracker/README.txt +++ b/Bugtracker/README.txt @@ -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 :) diff --git a/Bugtracker/__init__.py b/Bugtracker/__init__.py index 3de4c99..6c04428 100644 --- a/Bugtracker/__init__.py +++ b/Bugtracker/__init__.py @@ -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/' diff --git a/Bugtracker/config.py b/Bugtracker/config.py index 81ca345..c9ef892 100644 --- a/Bugtracker/config.py +++ b/Bugtracker/config.py @@ -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""")) + diff --git a/Bugtracker/plugin.py b/Bugtracker/plugin.py index d8d5b71..4fd0ffa 100644 --- a/Bugtracker/plugin.py +++ b/Bugtracker/plugin.py @@ -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): + """ + + Rename the bugtracker associated with to + """ + 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(([a-z]+)?\s+bugs?|[a-z]+))\s+#?(?P\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 '

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.*?
\s+(?P\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 = {} diff --git a/TODO b/TODO deleted file mode 100644 index 0e9e646..0000000 --- a/TODO +++ /dev/null @@ -1,5 +0,0 @@ -Bantracker: - - Documentation - -Bugtracker: - - Make sure to use reply.whenAddressedBy diff --git a/commoncgi.py b/commoncgi.py index afcbebc..63ce0dc 100644 --- a/commoncgi.py +++ b/commoncgi.py @@ -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('&','&').replace('<','<').replace('>','>').replace('"','"') - +