### # 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