示例#1
0
    def __init__(self, irc):
        super().__init__(irc)
        self.irc = irc
        self.topic_lock = threading.Lock()

        self.dbus_service = BTSDBusService(self._email_callback)

        self.mainloop = None
        mainloop = GObject.MainLoop()
        if not mainloop.is_running():
            mainloop_thread = threading.Thread(target=mainloop.run)
            mainloop_thread.start()
            self.mainloop = mainloop

        self.dbus_bus = SystemBus()
        self.dbus_bus.publish(self.dbus_service.interface_name,
                              self.dbus_service)
        self.dbus_service.start()

        self.requests_session = requests.Session()
        self.requests_session.verify = True

        self.queued_topics = {}
        self.last_n_messages = []

        self.stable_rc_bugs = StableRCBugs(self.requests_session)
        self.testing_rc_bugs = TestingRCBugs(self.requests_session)
        self.new_queue = NewQueue(self.requests_session)
        self.dinstall = Dinstall(self.requests_session)
        self.rm_queue = RmQueue(self.requests_session)
        self.apt_archive = AptArchive(
            self.registryValue('apt_configuration_directory'),
            self.registryValue('apt_cache_directory'))
        self.data_sources = (self.stable_rc_bugs, self.testing_rc_bugs,
                             self.new_queue, self.dinstall, self.rm_queue,
                             self.apt_archive)

        # Schedule datasource updates
        def wrapper(source):
            def implementation():
                try:
                    source.update()
                except Exception as e:
                    log.exception('Failed to update {}: {}'.format(
                        source.NAME, e))
                self._topic_callback()

            return implementation

        for source in self.data_sources:
            schedule.addPeriodicEvent(wrapper(source),
                                      source.INTERVAL,
                                      source.NAME,
                                      now=False)
            schedule.addEvent(wrapper(source), time.time() + 1)
示例#2
0
class TestDatasourceTestingNewQueue(unittest.TestCase):
    def setUp(self):
        fixture = os.path.join(os.path.dirname(os.path.abspath(__file__)),
                               'fixtures', 'new_queue.txt')
        with io.open(fixture, encoding='utf-8') as f:
            data = f.read()

        self.mocker = requests_mock.Mocker()
        self.mocker.start()
        self.mocker.register_uri('GET', NewQueue.URL, text=data)

        session = requests.Session()
        self.datasource = NewQueue(session)

    def tearDown(self):
        self.mocker.stop()

    def is_new(self, package, version):
        self.datasource.update()
        return self.datasource.is_new(package, version)

    def testURL(self):
        """
        Check we have a sane URL.
        """
        self.assertTrue(len(self.datasource.URL) > 5)
        self.assertTrue(self.datasource.URL.startswith('http'))

    def testInterval(self):
        """
        Check we have a sane update interval.
        """
        self.assertTrue(self.datasource.INTERVAL > 60)

    def testTop(self):
        self.assertTrue(self.is_new('ezmlm-idx', '6.0.1-1'))

    def testBottom(self):
        self.assertTrue(self.is_new('libxml-sax-expatxs-perl', '1.31-1'))

    def testMultipleVersions(self):
        self.assertTrue(self.is_new('libgcal', '0.8.1-1'))
        self.assertTrue(self.is_new('libgcal', '0.8.1-2'))

    def testInvalidVersion(self):
        self.assertFalse(self.is_new('rcpp', '0.5.2.invalid'))

    def testNotInQueue(self):
        self.assertFalse(self.is_new('package-not-in-new-queue', 'version-foo'))

    def testByhand(self):
        self.assertFalse(self.is_new('loadlin', '1.6c.really1.6c.nobin-2'))

    def testExperimental(self):
        self.assertTrue(self.is_new('ooo-build', '3.0.0.9+r14588-1'))
示例#3
0
    def setUp(self):
        fixture = os.path.join(os.path.dirname(os.path.abspath(__file__)),
                               'fixtures', 'new_queue.txt')
        with io.open(fixture, encoding='utf-8') as f:
            data = f.read()

        self.mocker = requests_mock.Mocker()
        self.mocker.start()
        self.mocker.register_uri('GET', NewQueue.URL, text=data)

        session = requests.Session()
        self.datasource = NewQueue(session)
class TestDatasourceTestingNewQueue(unittest.TestCase):

    def setUp(self):
        self.fixture = os.path.join(os.path.dirname(os.path.abspath(__file__)), \
            'fixtures', 'new_queue.txt')

        self.datasource = NewQueue()

    def is_new(self, package, version):
        fileobj = open(self.fixture)
        self.datasource.update(fileobj)
        return self.datasource.is_new(package, version)

    def testURL(self):
        """
        Check we have a sane URL.
        """
        self.assert_(len(self.datasource.URL) > 5)
        self.assert_(self.datasource.URL.startswith('http'))

    def testInterval(self):
        """
        Check we have a sane update interval.
        """
        self.assert_(self.datasource.INTERVAL > 60)

    def testTop(self):
        self.assert_(self.is_new('ezmlm-idx', '6.0.1-1'))

    def testBottom(self):
        self.assert_(self.is_new('libxml-sax-expatxs-perl', '1.31-1'))

    def testMultipleVersions(self):
        self.assert_(self.is_new('libgcal', '0.8.1-1'))
        self.assert_(self.is_new('libgcal', '0.8.1-2'))

    def testInvalidVersion(self):
        self.failIf(self.is_new('rcpp', '0.5.2.invalid'))

    def testNotInQueue(self):
        self.failIf(self.is_new('package-not-in-new-queue', 'version-foo'))

    def testByhand(self):
        self.failIf(self.is_new('loadlin', '1.6c.really1.6c.nobin-2'))

    def testExperimental(self):
        self.assert_(self.is_new('ooo-build', '3.0.0.9+r14588-1'))
示例#5
0
    def __init__(self, irc):
        super(DebianDevelChanges, self).__init__(irc)
        self.irc = irc
        self.topic_lock = threading.Lock()

        fr = FifoReader()
        fifo_loc = '/var/run/debian-devel-changes-bot/fifo'
        fr.start(self._email_callback, fifo_loc)

        self.requests_session = requests.Session()
        self.requests_session.verify = True

        self.queued_topics = {}
        self.last_n_messages = []

        self.stable_rc_bugs = StableRCBugs(self.requests_session)
        self.testing_rc_bugs = TestingRCBugs(self.requests_session)
        self.new_queue = NewQueue(self.requests_session)
        self.data_sources = (self.stable_rc_bugs, self.testing_rc_bugs,
                             self.new_queue)

        # Schedule datasource updates
        for klass, interval, name in get_datasources():
            try:
                schedule.removePeriodicEvent(name)
            except KeyError:
                pass

            def wrapper(klass=klass):
                klass().update()
                self._topic_callback()

            schedule.addPeriodicEvent(wrapper, interval, name, now=False)
            schedule.addEvent(wrapper, time.time() + 1)

        def wrapper(source):
            def implementation():
                source.update()
                self._topic_callback()
            return implementation

        for source in self.data_sources:
            schedule.addPeriodicEvent(wrapper(source), source.INTERVAL,
                                      source.NAME, now=False)
            schedule.addEvent(wrapper(source), time.time() + 1)
class DebianDevelChanges(supybot.callbacks.Plugin):
    threaded = True

    def __init__(self, irc):
        super().__init__(irc)
        self.irc = irc
        self.topic_lock = threading.Lock()

        self.mainloop = None
        self.mainloop_thread = None
        mainloop = GObject.MainLoop()
        if not mainloop.is_running():
            log.info('Starting Glib main loop')
            mainloop_thread = threading.Thread(target=mainloop.run,
                                               name='Glib maing loop')
            mainloop_thread.start()
            self.mainloop_thread = mainloop_thread
            self.mainloop = mainloop

        log.info('Starting D-Bus service')
        self.dbus_service = BTSDBusService(self._email_callback)
        self.dbus_bus = SystemBus()
        self.dbus_bus.publish(self.dbus_service.interface_name,
                              self.dbus_service)
        self.dbus_service.start()

        self.requests_session = requests.Session()
        self.requests_session.verify = True

        self.queued_topics = {}
        self.last_n_messages = []

        # data sources
        self.stable_rc_bugs = StableRCBugs(self.requests_session)
        self.testing_rc_bugs = TestingRCBugs(self.requests_session)
        self.new_queue = NewQueue(self.requests_session)
        self.dinstall = Dinstall(self.requests_session)
        self.rm_queue = RmQueue(self.requests_session)
        self.apt_archive = AptArchive(
            self.registryValue('apt_configuration_directory'),
            self.registryValue('apt_cache_directory'))
        self.data_sources = (self.stable_rc_bugs, self.testing_rc_bugs,
                             self.new_queue, self.dinstall, self.rm_queue,
                             self.apt_archive)

        # Schedule datasource updates
        def wrapper(source):
            def implementation():
                try:
                    source.update()
                except Exception as e:
                    log.exception('Failed to update {}: {}'.format(
                        source.NAME, e))
                self._topic_callback()

            return implementation

        for source in self.data_sources:
            # schedule periodic events
            schedule.addPeriodicEvent(wrapper(source),
                                      source.INTERVAL,
                                      source.NAME,
                                      now=False)
            # and run them now once
            schedule.addEvent(wrapper(source), time.time() + 1)

    def die(self):
        log.info('Stopping D-Bus service')
        self.dbus_service.stop()
        if self.mainloop is not None:
            log.info('Stopping Glib main loop')
            self.mainloop.quit()
            self.mainloop_thread.join(timeout=1.0)
            if self.mainloop_thread.is_alive():
                log.warn('Glib main loop thread is still alive.')

            self.mainloop = None
            self.mainloop_thread = None

        for source in self.data_sources:
            try:
                schedule.removePeriodicEvent(source.NAME)
            except KeyError:
                pass

        super().die()

    def _email_callback(self, fileobj):
        try:
            emailmsg = parse_mail(fileobj)
            msg = get_message(emailmsg, new_queue=self.new_queue)

            if not msg:
                return

            txt = colourise(msg.for_irc())

            # Simple flood/duplicate detection
            if txt in self.last_n_messages:
                return
            self.last_n_messages.insert(0, txt)
            self.last_n_messages = self.last_n_messages[:20]

            maintainer_info = None
            if hasattr(msg, 'maintainer'):
                maintainer_info = (split_address(msg.maintainer), )
            else:
                maintainer_info = []
                for package in msg.package.split(','):
                    package = package.strip()
                    try:
                        maintainer_info.append(
                            self.apt_archive.get_maintainer(package))
                    except NewDataSource.DataError as e:
                        log.info("Failed to query maintainer for {}.".format(
                            package))

            for channel in self.irc.state.channels:
                package_regex = self.registryValue(
                    'package_regex',
                    channel,
                ) or 'a^'  # match nothing by default

                package_match = re.search(package_regex, msg.package)

                maintainer_match = False
                maintainer_regex = self.registryValue('maintainer_regex',
                                                      channel)
                if maintainer_regex and maintainer_info is not None and len(
                        maintainer_info) >= 0:
                    for mi in maintainer_info:
                        maintainer_match = re.search(maintainer_regex,
                                                     mi['email'])
                        if maintainer_match:
                            break

                if not package_match and not maintainer_match:
                    continue

                distribution_regex = self.registryValue(
                    'distribution_regex',
                    channel,
                )

                if distribution_regex:
                    if not hasattr(msg, 'distribution'):
                        # If this channel has a distribution regex, don't
                        # bother continuing unless the message actually has a
                        # distribution. This filters security messages, etc.
                        continue

                    if not re.search(distribution_regex, msg.distribution):
                        # Distribution doesn't match regex; don't send this
                        # message.
                        continue

                send_privmsg = self.registryValue('send_privmsg', channel)
                # Send NOTICE per default and if 'send_privmsg' is set for the
                # channel, send PRIVMSG instead.
                if send_privmsg:
                    ircmsg = supybot.ircmsgs.privmsg(channel, txt)
                else:
                    ircmsg = supybot.ircmsgs.notice(channel, txt)

                self.irc.queueMsg(ircmsg)

        except Exception as e:
            log.exception('Uncaught exception: %s ' % e)

    def _topic_callback(self):
        sections = {
            self.testing_rc_bugs.get_number_bugs: 'RC bug count',
            self.stable_rc_bugs.get_number_bugs: 'Stable RC bug count',
            self.new_queue.get_size: 'NEW queue',
            self.rm_queue.get_size: 'RM queue',
            self.dinstall.get_status: 'dinstall'
        }

        channels = set()
        with self.topic_lock:
            values = {}
            for callback, prefix in sections.items():
                values[prefix] = callback()

            for channel in self.irc.state.channels:
                new_topic = topic = self.irc.state.getTopic(channel)

                for callback, prefix in sections.items():
                    if values[prefix]:
                        new_topic = rewrite_topic(new_topic, prefix,
                                                  values[prefix])

                if topic != new_topic:
                    self.queued_topics[channel] = new_topic

                    if channel not in channels:
                        log.info("Queueing change of topic in #%s to '%s'" %
                                 (channel, new_topic))
                        channels.add(channel)

        for channel in channels:
            event_name = '{}_topic'.format(channel)
            try:
                schedule.removeEvent(event_name)
            except KeyError:
                pass

            def update_topic(channel=channel):
                self._update_topic(channel)

            schedule.addEvent(update_topic, time.time() + 60, event_name)

    def _update_topic(self, channel):
        with self.topic_lock:
            try:
                new_topic = self.queued_topics[channel]
                log.info("Changing topic in #%s to '%s'" %
                         (channel, new_topic))
                self.irc.queueMsg(supybot.ircmsgs.topic(channel, new_topic))
            except KeyError:
                pass

    def rc(self, irc, msg, args):
        """Link to UDD RC bug overview."""
        num_bugs = self.testing_rc_bugs.get_number_bugs()
        if type(num_bugs) is int:
            irc.reply(
                "There are %d release-critical bugs in the testing distribution. "
                "See https://udd.debian.org/bugs.cgi?release=buster&notmain=ign&merged=ign&rc=1"
                % num_bugs)
        else:
            irc.reply("No data at this time.")

    rc = wrap(rc)
    bugs = wrap(rc)

    def update(self, irc, msg, args):
        """Trigger an update."""
        if not ircdb.checkCapability(msg.prefix, 'owner'):
            irc.reply("You are not authorised to run this command.")
            return

        for source in self.data_sources:
            source.update()
            irc.reply("Updated %s." % source.NAME)
        self._topic_callback()

    update = wrap(update)

    def madison(self, irc, msg, args, package):
        """List packages."""
        try:
            lines = madison(package)
            if not lines:
                irc.reply(
                    'Did not get a response -- is "%s" a valid package?' %
                    package)
                return

            field_styles = ('package', 'version', 'distribution', 'section')
            for line in lines:
                out = []
                fields = line.strip().split('|', len(field_styles))
                for style, data in zip(field_styles, fields):
                    out.append('[%s]%s' % (style, data))
                irc.reply(colourise('[reset]|'.join(out)), prefixNick=False)
        except Exception as e:
            irc.reply("Error: %s" % e.message)

    madison = wrap(madison, ['text'])

    def get_pool_url(self, package):
        if package.startswith('lib'):
            return (package[:4], package)
        else:
            return (package[:1], package)

    def _maintainer(self, irc, msg, args, items):
        """Get maintainer for package."""
        for package in items:
            info = self.apt_archive.get_maintainer(package)
            if info:
                display_name = format_email_address(
                    "%s <%s>" % (info['name'], info['email']), max_domain=18)

                login = info['email']
                if login.endswith('@debian.org'):
                    login = login.replace('@debian.org', '')

                msg = "[desc]Maintainer for[reset] [package]%s[reset] [desc]is[reset] [by]%s[reset]: " % (
                    package, display_name)
                msg += "[url]https://qa.debian.org/developer.php?login=%s[/url]" % login
            else:
                msg = 'Unknown source package "%s"' % package

            irc.reply(colourise(msg), prefixNick=False)

    maintainer = wrap(_maintainer, [many('anything')])
    maint = wrap(_maintainer, [many('anything')])
    who_maintains = wrap(_maintainer, [many('anything')])

    def _qa(self, irc, msg, args, items):
        """Get link to QA page."""
        for package in items:
            url = "https://packages.qa.debian.org/%s/%s.html" % self.get_pool_url(
                package)
            msg = "[desc]QA page for[reset] [package]%s[reset]: [url]%s[/url]" % (
                package, url)
            irc.reply(colourise(msg), prefixNick=False)

    qa = wrap(_qa, [many('anything')])
    overview = wrap(_qa, [many('anything')])
    package = wrap(_qa, [many('anything')])
    pkg = wrap(_qa, [many('anything')])
    srcpkg = wrap(_qa, [many('anything')])

    def _changelog(self, irc, msg, args, items):
        """Get link to changelog."""
        for package in items:
            url = "https://packages.debian.org/changelogs/pool/main/%s/%s/current/changelog" % self.get_pool_url(
                package)
            msg = "[desc]debian/changelog for[reset] [package]%s[reset]: [url]%s[/url]" % (
                package, url)
            irc.reply(colourise(msg), prefixNick=False)

    changelog = wrap(_changelog, [many('anything')])
    changes = wrap(_changelog, [many('anything')])

    def _copyright(self, irc, msg, args, items):
        """Link to copyright files."""
        for package in items:
            url = "https://packages.debian.org/changelogs/pool/main/%s/%s/current/copyright" % self.get_pool_url(
                package)
            msg = "[desc]debian/copyright for[reset] [package]%s[reset]: [url]%s[/url]" % (
                package, url)
            irc.reply(colourise(msg), prefixNick=False)

    copyright = wrap(_copyright, [many('anything')])

    def _buggraph(self, irc, msg, args, items):
        """Link to bug graph."""
        for package in items:
            msg = "[desc]Bug graph for[reset] [package]%s[reset]: [url]https://qa.debian.org/data/bts/graphs/%s/%s.png[/url]" % \
                (package, package[0], package)
            irc.reply(colourise(msg), prefixNick=False)

    buggraph = wrap(_buggraph, [many('anything')])
    bug_graph = wrap(_buggraph, [many('anything')])

    def _buildd(self, irc, msg, args, items):
        """Link to buildd page."""
        for package in items:
            msg = "[desc]buildd status for[reset] [package]%s[reset]: [url]https://buildd.debian.org/pkg.cgi?pkg=%s[/url]" % \
                (package, package)
            irc.reply(colourise(msg), prefixNick=False)

    buildd = wrap(_buildd, [many('anything')])

    def _popcon(self, irc, msg, args, package):
        """Get popcon data."""
        try:
            msg = popcon(package, self.requests_session)
            if msg:
                irc.reply(colourise(msg.for_irc()), prefixNick=False)
        except Exception as e:
            irc.reply("Error: unable to obtain popcon data for %s" % package)

    popcon = wrap(_popcon, ['text'])

    def _testing(self, irc, msg, args, items):
        """Check testing migration status."""
        for package in items:
            msg = "[desc]Testing migration status for[reset] [package]%s[reset]: [url]https://qa.debian.org/excuses.php?package=%s[/url]" % \
                (package, package)
            irc.reply(colourise(msg), prefixNick=False)

    testing = wrap(_testing, [many('anything')])
    migration = wrap(_testing, [many('anything')])

    def _dehs(self, irc, msg, args, items):
        """Link to DEHS."""
        for package in items:
            msg = "[desc]Debian External Health Status for[reset] [package]%s[reset]: [url]https://dehs.alioth.debian.org/report.php?package=%s[/url]" % \
                (package, urllib.parse.quote(package))
            irc.reply(colourise(msg), prefixNick=False)

    dehs = wrap(_dehs, [many('anything')])
    health = wrap(_dehs, [many('anything')])

    def _new(self, irc, msg, args):
        """Link to NEW queue."""
        line = "[desc]NEW queue is[reset]: [url]%s[/url]. [desc]Current size is:[reset] %d" % \
            ("https://ftp-master.debian.org/new.html", self.new_queue.get_size())
        irc.reply(colourise(line))

    new = wrap(_new)
    new_queue = wrap(_new)
    newqueue = wrap(_new)
    def setUp(self):
        self.fixture = os.path.join(os.path.dirname(os.path.abspath(__file__)), \
            'fixtures', 'new_queue.txt')

        self.datasource = NewQueue()
示例#8
0
    def __init__(self, irc):
        super().__init__(irc)
        self.irc = irc
        self.topic_lock = threading.Lock()

        self.mainloop = None
        self.mainloop_thread = None
        mainloop = GObject.MainLoop()
        if not mainloop.is_running():
            log.info('Starting Glib main loop')
            mainloop_thread = threading.Thread(target=mainloop.run, name='Glib maing loop')
            mainloop_thread.start()
            self.mainloop_thread = mainloop_thread
            self.mainloop = mainloop

        self.requests_session = requests.Session()
        self.requests_session.verify = True

        self.queued_topics = {}
        self.last_n_messages = []

        # data sources
        pseudo_packages.pp = PseudoPackages(self.requests_session)
        self.pseudo_packages = pseudo_packages.pp
        self.stable_rc_bugs = StableRCBugs(self.requests_session)
        self.testing_rc_bugs = TestingRCBugs(self.requests_session)
        self.new_queue = NewQueue(self.requests_session)
        self.dinstall = Dinstall(self.requests_session)
        self.rm_queue = RmQueue(self.requests_session)
        self.apt_archive = AptArchive(
            self.registryValue('apt_configuration_directory'),
            self.registryValue('apt_cache_directory'))
        self.data_sources = (
            self.pseudo_packages,
            self.stable_rc_bugs,
            self.testing_rc_bugs,
            self.new_queue,
            self.dinstall,
            self.rm_queue,
            self.apt_archive
        )

        # Schedule datasource updates
        def wrapper(source):
            def implementation():
                try:
                    source.update()
                except Exception as e:
                    log.exception('Failed to update {}: {}'.format(source.NAME, e))
                self._topic_callback()
            return implementation

        for source in self.data_sources:
            # schedule periodic events
            schedule.addPeriodicEvent(wrapper(source), source.INTERVAL,
                                      source.NAME, now=False)
            # and run them now once
            schedule.addEvent(wrapper(source), time.time() + 1)

        log.info('Starting D-Bus service')
        self.dbus_service = BTSDBusService(self._email_callback)
        self.dbus_bus = SystemBus()
        self.dbus_bus.publish(self.dbus_service.interface_name,
                              self.dbus_service)
        self.dbus_service.start()
示例#9
0
class DebianDevelChanges(supybot.callbacks.Plugin):
    threaded = True

    def __init__(self, irc):
        super().__init__(irc)
        self.irc = irc
        self.topic_lock = threading.Lock()

        self.mainloop = None
        self.mainloop_thread = None
        mainloop = GObject.MainLoop()
        if not mainloop.is_running():
            log.info('Starting Glib main loop')
            mainloop_thread = threading.Thread(target=mainloop.run, name='Glib maing loop')
            mainloop_thread.start()
            self.mainloop_thread = mainloop_thread
            self.mainloop = mainloop

        self.requests_session = requests.Session()
        self.requests_session.verify = True

        self.queued_topics = {}
        self.last_n_messages = []

        # data sources
        pseudo_packages.pp = PseudoPackages(self.requests_session)
        self.pseudo_packages = pseudo_packages.pp
        self.stable_rc_bugs = StableRCBugs(self.requests_session)
        self.testing_rc_bugs = TestingRCBugs(self.requests_session)
        self.new_queue = NewQueue(self.requests_session)
        self.dinstall = Dinstall(self.requests_session)
        self.rm_queue = RmQueue(self.requests_session)
        self.apt_archive = AptArchive(
            self.registryValue('apt_configuration_directory'),
            self.registryValue('apt_cache_directory'))
        self.data_sources = (
            self.pseudo_packages,
            self.stable_rc_bugs,
            self.testing_rc_bugs,
            self.new_queue,
            self.dinstall,
            self.rm_queue,
            self.apt_archive
        )

        # Schedule datasource updates
        def wrapper(source):
            def implementation():
                try:
                    source.update()
                except Exception as e:
                    log.exception('Failed to update {}: {}'.format(source.NAME, e))
                self._topic_callback()
            return implementation

        for source in self.data_sources:
            # schedule periodic events
            schedule.addPeriodicEvent(wrapper(source), source.INTERVAL,
                                      source.NAME, now=False)
            # and run them now once
            schedule.addEvent(wrapper(source), time.time() + 1)

        log.info('Starting D-Bus service')
        self.dbus_service = BTSDBusService(self._email_callback)
        self.dbus_bus = SystemBus()
        self.dbus_bus.publish(self.dbus_service.interface_name,
                              self.dbus_service)
        self.dbus_service.start()

    def die(self):
        log.info('Stopping D-Bus service')
        self.dbus_service.stop()
        if self.mainloop is not None:
            log.info('Stopping Glib main loop')
            self.mainloop.quit()
            self.mainloop_thread.join(timeout=1.0)
            if self.mainloop_thread.is_alive():
                log.warn('Glib main loop thread is still alive.')

            self.mainloop = None
            self.mainloop_thread = None

        for source in self.data_sources:
            try:
                schedule.removePeriodicEvent(source.NAME)
            except KeyError:
                pass

        super().die()

    def _email_callback(self, fileobj):
        try:
            emailmsg = parse_mail(fileobj)
            msg = get_message(emailmsg, new_queue=self.new_queue)

            if not msg:
                return

            txt = colourise(msg.for_irc())

            # Simple flood/duplicate detection
            if txt in self.last_n_messages:
                return
            self.last_n_messages.insert(0, txt)
            self.last_n_messages = self.last_n_messages[:20]

            maintainer_info = None
            if hasattr(msg, 'maintainer'):
                maintainer_info = (split_address(msg.maintainer), )
            else:
                maintainer_info = []
                for package in msg.package.split(','):
                    package = package.strip()
                    try:
                        maintainer_info.append(self.apt_archive.get_maintainer(package))
                    except NewDataSource.DataError as e:
                        log.info("Failed to query maintainer for {}.".format(package))

            for channel in self.irc.state.channels:
                package_regex = self.registryValue(
                    'package_regex',
                    channel,
                ) or 'a^'  # match nothing by default

                package_match = re.search(package_regex, msg.package)

                maintainer_match = False
                maintainer_regex = self.registryValue(
                    'maintainer_regex',
                    channel)
                if maintainer_regex and maintainer_info is not None and len(maintainer_info) >= 0:
                    for mi in maintainer_info:
                        maintainer_match = re.search(maintainer_regex, mi['email'])
                        if maintainer_match:
                            break

                if not package_match and not maintainer_match:
                    continue

                distribution_regex = self.registryValue(
                    'distribution_regex',
                    channel,
                )

                if distribution_regex:
                    if not hasattr(msg, 'distribution'):
                        # If this channel has a distribution regex, don't
                        # bother continuing unless the message actually has a
                        # distribution. This filters security messages, etc.
                        continue

                    if not re.search(distribution_regex, msg.distribution):
                        # Distribution doesn't match regex; don't send this
                        # message.
                        continue

                send_privmsg = self.registryValue('send_privmsg', channel)
                # Send NOTICE per default and if 'send_privmsg' is set for the
                # channel, send PRIVMSG instead.
                if send_privmsg:
                    ircmsg = supybot.ircmsgs.privmsg(channel, txt)
                else:
                    ircmsg = supybot.ircmsgs.notice(channel, txt)

                self.irc.queueMsg(ircmsg)

        except Exception as e:
            log.exception('Uncaught exception: %s ' % e)

    def _topic_callback(self):
        sections = {
            self.testing_rc_bugs.get_number_bugs: 'RC bug count',
            self.stable_rc_bugs.get_number_bugs: 'stable RC bug count',
            self.new_queue.get_size: 'NEW queue',
            self.new_queue.get_backports_size: 'backports NEW queue',
            self.rm_queue.get_size: 'RM queue',
            self.dinstall.get_status: 'dinstall'
        }

        channels = set()
        with self.topic_lock:
            values = {}
            for callback, prefix in sections.items():
                new_value = callback()
                if new_value is not None:
                    values[prefix] = new_value

            for channel in self.irc.state.channels:
                new_topic = topic = self.irc.state.getTopic(channel)

                for prefix, value in values.items():
                    new_topic = rewrite_topic(new_topic, prefix, value)

                if topic != new_topic:
                    self.queued_topics[channel] = new_topic

                    if channel not in channels:
                        log.info("Queueing change of topic in #%s to '%s'" %
                                 (channel, new_topic))
                        channels.add(channel)

        for channel in channels:
            event_name = '{}_topic'.format(channel)
            try:
                schedule.removeEvent(event_name)
            except KeyError:
                pass

            def update_topic(channel=channel):
                self._update_topic(channel)

            schedule.addEvent(update_topic, time.time() + 60, event_name)

    def _update_topic(self, channel):
        with self.topic_lock:
            try:
                new_topic = self.queued_topics[channel]
                log.info("Changing topic in #%s to '%s'" % (channel, new_topic))
                self.irc.queueMsg(supybot.ircmsgs.topic(channel, new_topic))
            except KeyError:
                pass

    def rc(self, irc, msg, args):
        """Link to UDD RC bug overview."""
        num_bugs = self.testing_rc_bugs.get_number_bugs()
        if type(num_bugs) is int:
            irc.reply(
              "There are %d release-critical bugs in the testing distribution. "
              "See https://udd.debian.org/bugs.cgi?release=buster&notmain=ign&merged=ign&rc=1" % num_bugs)
        else:
            irc.reply("No data at this time.")
    rc = wrap(rc)
    bugs = wrap(rc)

    def update(self, irc, msg, args):
        """Trigger an update."""
        if not ircdb.checkCapability(msg.prefix, 'owner'):
            irc.reply("You are not authorised to run this command.")
            return

        for source in self.data_sources:
            source.update()
            irc.reply("Updated %s." % source.NAME)
        self._topic_callback()
    update = wrap(update)

    def madison(self, irc, msg, args, package):
        """List packages."""
        try:
            lines = madison(package)
            if not lines:
                irc.reply('Did not get a response -- is "%s" a valid package?' % package)
                return

            field_styles = ('package', 'version', 'distribution', 'section')
            for line in lines:
                out = []
                fields = line.strip().split('|', len(field_styles))
                for style, data in zip(field_styles, fields):
                    out.append('[%s]%s' % (style, data))
                irc.reply(colourise('[reset]|'.join(out)), prefixNick=False)
        except Exception as e:
            irc.reply("Error: %s" % e.message)
    madison = wrap(madison, ['text'])

    def get_pool_url(self, package):
        if package.startswith('lib'):
            return (package[:4], package)
        else:
            return (package[:1], package)

    def _maintainer(self, irc, msg, args, items):
        """Get maintainer for package."""
        for package in items:
            info = self.apt_archive.get_maintainer(package)
            if info:
                display_name = format_email_address("%s <%s>" % (info['name'], info['email']), max_domain=18)

                login = info['email']
                if login.endswith('@debian.org'):
                    login = login.replace('@debian.org', '')

                msg = "[desc]Maintainer for[reset] [package]%s[reset] [desc]is[reset] [by]%s[reset]: " % (package, display_name)
                msg += "[url]https://qa.debian.org/developer.php?login=%s[/url]" % login
            else:
                msg = 'Unknown source package "%s"' % package

            irc.reply(colourise(msg), prefixNick=False)
    maintainer = wrap(_maintainer, [many('anything')])
    maint = wrap(_maintainer, [many('anything')])
    who_maintains = wrap(_maintainer, [many('anything')])

    def _qa(self, irc, msg, args, items):
        """Get link to QA page."""
        for package in items:
            url = "https://packages.qa.debian.org/%s/%s.html" % self.get_pool_url(package)
            msg = "[desc]QA page for[reset] [package]%s[reset]: [url]%s[/url]" % (package, url)
            irc.reply(colourise(msg), prefixNick=False)
    qa = wrap(_qa, [many('anything')])
    overview = wrap(_qa, [many('anything')])
    package = wrap(_qa, [many('anything')])
    pkg = wrap(_qa, [many('anything')])
    srcpkg = wrap(_qa, [many('anything')])

    def _changelog(self, irc, msg, args, items):
        """Get link to changelog."""
        for package in items:
            url = "https://packages.debian.org/changelogs/pool/main/%s/%s/current/changelog" % self.get_pool_url(package)
            msg = "[desc]debian/changelog for[reset] [package]%s[reset]: [url]%s[/url]" % (package, url)
            irc.reply(colourise(msg), prefixNick=False)
    changelog = wrap(_changelog, [many('anything')])
    changes = wrap(_changelog, [many('anything')])

    def _copyright(self, irc, msg, args, items):
        """Link to copyright files."""
        for package in items:
            url = "https://packages.debian.org/changelogs/pool/main/%s/%s/current/copyright" % self.get_pool_url(package)
            msg = "[desc]debian/copyright for[reset] [package]%s[reset]: [url]%s[/url]" % (package, url)
            irc.reply(colourise(msg), prefixNick=False)
    copyright = wrap(_copyright, [many('anything')])

    def _buggraph(self, irc, msg, args, items):
        """Link to bug graph."""
        for package in items:
            msg = "[desc]Bug graph for[reset] [package]%s[reset]: [url]https://qa.debian.org/data/bts/graphs/%s/%s.png[/url]" % \
                (package, package[0], package)
            irc.reply(colourise(msg), prefixNick=False)
    buggraph = wrap(_buggraph, [many('anything')])
    bug_graph = wrap(_buggraph, [many('anything')])

    def _buildd(self, irc, msg, args, items):
        """Link to buildd page."""
        for package in items:
            msg = "[desc]buildd status for[reset] [package]%s[reset]: [url]https://buildd.debian.org/pkg.cgi?pkg=%s[/url]" % \
                (package, package)
            irc.reply(colourise(msg), prefixNick=False)
    buildd = wrap(_buildd, [many('anything')])

    def _popcon(self, irc, msg, args, package):
        """Get popcon data."""
        try:
            msg = popcon(package, self.requests_session)
            if msg:
                irc.reply(colourise(msg.for_irc()), prefixNick=False)
        except Exception as e:
            irc.reply("Error: unable to obtain popcon data for %s" % package)
    popcon = wrap(_popcon, ['text'])

    def _testing(self, irc, msg, args, items):
        """Check testing migration status."""
        for package in items:
            msg = "[desc]Testing migration status for[reset] [package]%s[reset]: [url]https://qa.debian.org/excuses.php?package=%s[/url]" % \
                (package, package)
            irc.reply(colourise(msg), prefixNick=False)
    testing = wrap(_testing, [many('anything')])
    migration = wrap(_testing, [many('anything')])

    def _new(self, irc, msg, args):
        """Link to NEW queue."""
        line = "[desc]NEW queue is[reset]: [url]%s[/url]. [desc]Current size is:[reset] %d" % \
            ("https://ftp-master.debian.org/new.html", self.new_queue.get_size())
        irc.reply(colourise(line))
    new = wrap(_new)
    new_queue = wrap(_new)
    newqueue = wrap(_new)