diff --git a/Bugreporter/README.rst b/Bugreporter/README.rst new file mode 100644 index 0000000..f9ef83c --- /dev/null +++ b/Bugreporter/README.rst @@ -0,0 +1,61 @@ +.. _plugin-Bugreporter: + +Documentation for the Bugreporter plugin for Supybot +==================================================== + +Purpose +------- +This plugin announces bug reports from Launchpad as they are filed. +Based on EeeBotu by Mike Rooney + +Usage +----- +Announce bug reports from Launchpad as they are filed. + +.. _conf-Bugreporter: + +Configuration +------------- + +.. _conf-supybot.plugins.Bugreporter.channels: + +supybot.plugins.Bugreporter.channels + This config variable defaults to "", is network-specific, and is not channel-specific. + + Channels to announce bug reports to. + +.. _conf-supybot.plugins.Bugreporter.count: + +supybot.plugins.Bugreporter.count + This config variable defaults to "5", is not network-specific, and is not channel-specific. + + Number of bug reports to fetch at each interval. + +.. _conf-supybot.plugins.Bugreporter.interval: + +supybot.plugins.Bugreporter.interval + This config variable defaults to "300", is not network-specific, and is not channel-specific. + + Interval in seconds to check for new bug reports. + +.. _conf-supybot.plugins.Bugreporter.projects: + +supybot.plugins.Bugreporter.projects + This config variable defaults to "ubuntu", is network-specific, and is channel-specific. + + Projects to announce bug reports on. + +.. _conf-supybot.plugins.Bugreporter.public: + +supybot.plugins.Bugreporter.public + This config variable defaults to "True", is not network-specific, and is not channel-specific. + + Determines whether this plugin is publicly visible. + +.. _conf-supybot.plugins.Bugreporter.useNotices: + +supybot.plugins.Bugreporter.useNotices + This config variable defaults to "False", is network-specific, and is channel-specific. + + Use notices instead of normal messages. + diff --git a/Bugreporter/__init__.py b/Bugreporter/__init__.py new file mode 100644 index 0000000..1be861a --- /dev/null +++ b/Bugreporter/__init__.py @@ -0,0 +1,64 @@ +### +# Copyright (c) 2021, Krytarik Raido +# 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. +### + +""" +This plugin announces bug reports from Launchpad as they are filed. +Based on EeeBotu by Mike Rooney +""" + +import supybot +from supybot import world + +# Use this for the version of this plugin. +__version__ = "1.0.0" + +# XXX Replace this with an appropriate author or supybot.Author instance. +__author__ = supybot.Author('Krytarik Raido', 'krytarik', 'krytarik@gmail.com') + +# This is a dictionary mapping supybot.Author instances to lists of +# contributions. +__contributors__ = {} + +# This is a url where the most recent plugin package can be downloaded. +__url__ = 'https://launchpad.net/ubuntu-bots' + +from . import config +from . import plugin +from importlib import reload +# In case we're being reloaded. +reload(config) +reload(plugin) +# Add more reloads here if you add third-party modules and want them to be +# reloaded when this plugin is reloaded. Don't forget to import them as well! + +if world.testing: + from . import test + +Class = plugin.Class +configure = config.configure diff --git a/Bugreporter/config.py b/Bugreporter/config.py new file mode 100644 index 0000000..e653c33 --- /dev/null +++ b/Bugreporter/config.py @@ -0,0 +1,65 @@ +### +# Copyright (c) 2021, Krytarik Raido +# 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. +### + +from supybot import conf, registry + +try: + from supybot.i18n import PluginInternationalization + _ = PluginInternationalization('Bugreporter') +except: + # Placeholder that allows to run the plugin on a bot + # without the i18n module + _ = lambda x: x + + +def configure(advanced): + # This will be called by supybot to configure this module. advanced is + # a bool that specifies whether the user identified themself as an advanced + # user or not. You should effect your configuration by manipulating the + # registry as appropriate. + from supybot.questions import expect, anything, something, yn + conf.registerPlugin('Bugreporter', True) + + +Bugreporter = conf.registerPlugin('Bugreporter') + +conf.registerGlobalValue(Bugreporter, 'interval', + registry.NonNegativeInteger(300, _("""Interval in seconds to check for new bug reports."""))) + +conf.registerGlobalValue(Bugreporter, 'count', + registry.NonNegativeInteger(5, _("""Number of bug reports to fetch at each interval."""))) + +conf.registerNetworkValue(Bugreporter, 'channels', + registry.SpaceSeparatedListOfStrings([], _("""Channels to announce bug reports to."""))) + +conf.registerChannelValue(Bugreporter, 'projects', + registry.SpaceSeparatedListOfStrings(['ubuntu'], _("""Projects to announce bug reports on."""))) + +conf.registerChannelValue(Bugreporter, 'useNotices', + registry.Boolean(False, _("""Use notices instead of normal messages."""))) diff --git a/Bugreporter/plugin.py b/Bugreporter/plugin.py new file mode 100644 index 0000000..aa432d0 --- /dev/null +++ b/Bugreporter/plugin.py @@ -0,0 +1,151 @@ +### +# Copyright (c) 2021, Krytarik Raido +# 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. +### + +from supybot import utils, callbacks, registry +from supybot import world, ircmsgs, schedule +import supybot.log as supylog +import requests, feedparser, time +from bs4 import BeautifulSoup + +try: + from supybot.i18n import PluginInternationalization + _ = PluginInternationalization('Bugreporter') +except ImportError: + # Placeholder that allows to run the plugin on a bot + # without the i18n module + _ = lambda x: x + + +class Bugreporter(callbacks.Plugin): + """Announce bug reports from Launchpad as they are filed.""" + threaded = True + + def __init__(self, irc): + self.__parent = super(Bugreporter, self) + self.__parent.__init__(irc) + self.event = 'Bugreporter' + self.bugsAnnounced = {} + self.bugsCache = {} + self.lastChecked = {} + self.lastModified = {} + schedule.addPeriodicEvent(self.trackBugs, self.registryValue('interval'), self.event) + + def die(self): + schedule.removePeriodicEvent(self.event) + + def getLatestBugs(self, channel, network, now): + """Get the latest bug reports on the specified projects.""" + bugs = {} + for project in self.registryValue('projects', channel, network): + if project in self.bugsCache and project in self.lastChecked \ + and self.lastChecked[project] > now - self.registryValue('interval'): + bugsp = self.bugsCache[project] + else: + bugsp = {} + self.lastChecked[project] = now + feedUrl = "http://feeds.launchpad.net/%s/latest-bugs.atom" % project + feedHeaders = requests.head(feedUrl).headers + + if project in self.lastModified and \ + feedHeaders['Last-Modified'] == self.lastModified[project]: + if channel not in self.bugsAnnounced and project in self.bugsCache: + bugsp = self.bugsCache[project] + else: + self.lastModified[project] = feedHeaders['Last-Modified'] + feed = feedparser.parse(feedUrl) + + for i in range(self.registryValue('count')): + entry = feed.entries[i] + title = entry.title + numEnd = title.find(']') + bugNo = title[1:numEnd] + bugTitle = title[numEnd+2:].strip() + bugTitle = bugTitle.replace('&','&').replace('<','<').replace('>','>').replace('"','"') + html = entry.content[0].value + + try: + soup = BeautifulSoup(html, 'lxml').div.table.findAll('tr')[1].findAll('td')[1:4] + bugPackage, bugStat, bugImp = [tag.contents[0] for tag in soup] + except: + supylog.warning('Bugreporter: Failed to soup on bug #%s.' % bugNo) + bugPackage = bugStat = bugImp = '???' + + bugsp[bugNo] = (bugNo, bugPackage, bugTitle, bugImp, bugStat) + + self.bugsCache[project] = bugsp + bugs.update(bugsp) + return bugs + + + def trackBugs(self): + """Check for new bug reports every interval and announce them.""" + now = time.time() + for irc in world.ircs: + network = irc.network + for channel in self.registryValue('channels', None, network): + if channel not in irc.state.channels: + continue + + if channel not in self.bugsAnnounced: + bugs = self.getLatestBugs(channel, network, now) + self.bugsAnnounced[channel] = sorted(list(bugs.keys()), reverse=True) + + try: + bugs = self.getLatestBugs(channel, network, now) + except Exception: + supylog.warning('Bugreporter: Failed to fetch bug reports from Launchpad, ' + + 'will retry next interval.') + continue + + newBugs = [bugs[x] for x in sorted(list(bugs.keys())) if x not in self.bugsAnnounced[channel]] + for (bugNo, bugPackage, bugTitle, bugImp, bugStat) in newBugs: + report = 'New bug #%s in %s: "%s" [%s, %s] https://launchpad.net/bugs/%s' % (bugNo, bugPackage, bugTitle.replace('"',"'"), bugImp, bugStat, bugNo) + + message_max = 450 - len(channel) + if len(report) > message_max: + report_parts = report.split('"') + report_start = report_parts[0] + report_end = report_parts[-1] + report_title = '"'.join(report_parts[1:-1]) + title_max = message_max - len(report_start) - len(report_end) - 5 + report_title_cut = report_title[:title_max].rsplit(None, 1)[0] + '...' + report = '%s"%s"%s' % (report_start, report_title_cut, report_end) + + if not self.registryValue('useNotices', channel, network): + irc.queueMsg(ircmsgs.privmsg(channel, report)) + else: + irc.queueMsg(ircmsgs.notice(channel, report)) + self.bugsAnnounced[channel].insert(0, bugNo) + + for i in range(len(self.registryValue('projects', channel, network)) + * self.registryValue('count'), len(self.bugsAnnounced[channel])): + self.bugsAnnounced[channel].pop() + + +Class = Bugreporter diff --git a/Bugreporter/test.py b/Bugreporter/test.py new file mode 100644 index 0000000..9e56cb2 --- /dev/null +++ b/Bugreporter/test.py @@ -0,0 +1,33 @@ +### +# Copyright (c) 2021, Krytarik Raido +# 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. +### + +from supybot.test import * + +class BugreporterTestCase(PluginTestCase): + plugins = ('Bugreporter',)