diff --git a/Phabricator/config.py b/Phabricator/config.py --- a/Phabricator/config.py +++ b/Phabricator/config.py @@ -70,6 +70,20 @@ )) ) +conf.registerChannelValue( + Phabricator, 'announce', + registry.Boolean(False, _( + """Determines whether Phabricator's feed will be announced + on the channel.""" + )) +) + +conf.registerChannelValue( + Phabricator.announce, 'interval', + registry.PositiveInteger(5*60, _( + """The interval between two queries to Phabricator's feed API.""" + )) +) # vim:set shiftwidth=4 tabstop=4 expandtab textwidth=79: diff --git a/Phabricator/plugin.py b/Phabricator/plugin.py --- a/Phabricator/plugin.py +++ b/Phabricator/plugin.py @@ -28,15 +28,18 @@ ### -from collections import defaultdict import re import time +import threading +from collections import defaultdict, namedtuple import phabricator import supybot.utils as utils from supybot.commands import * +import supybot.world as world import supybot.plugins as plugins +import supybot.ircmsgs as ircmsgs import supybot.ircutils as ircutils import supybot.callbacks as callbacks try: @@ -47,6 +50,7 @@ # without the i18n module _ = lambda x: x +feed_announce = namedtuple('feed_announce', 'fetch_time max_epoch') class Phabricator(callbacks.PluginRegexp): """Integration with the Phabricator development collaboration tools""" @@ -63,6 +67,7 @@ self.default_conduit = None self._conduits = {} + self._last_feed_announces = defaultdict(lambda: feed_announce(0, None)) self._phid_cache = defaultdict(lambda: (None, 0)) host = self.registryValue('phabricatorURI') token = self.registryValue('phabricatorConduitToken') @@ -370,6 +375,48 @@ to=recipient, ) + def __call__(self, irc, msg): + super().__call__(irc, msg) + threading.Thread(target=self._update_feeds).start() + + def _update_feeds(self): + """Goes through all channels, and trigger an update on the ones + which have an associated feed.""" + for irc in world.ircs: + for channel in irc.state.channels: + if self.registryValue('announce', channel): + self._update_feed_if_needed(irc, channel) + + def _update_feed_if_needed(self, irc, channel): + """Triggers an update if the channel's feed should be updated.""" + recipient_key = (irc.network, channel) + previous_announce = self._last_feed_announces[recipient_key] + current_time = time.time() + interval = self.registryValue('announce.interval', channel) + if previous_announce.fetch_time + interval < current_time: + max_epoch = self._update_feed(irc, channel, + previous_announce.max_epoch) + self._last_feed_announces[recipient_key] = feed_announce( + fetch_time=current_time, + max_epoch=max_epoch) + + def _update_feed(self, irc, channel, after_epoch): + """Send updates of a feed to a channel, and returns its max epoch.""" + conduit = self.conduit(channel) + stories = conduit.feed.query(view='text') + after_epoch = after_epoch or 0 + for story in stories.values(): + if (after_epoch > 0 and # Don't announce on the first run + story['epoch'] > after_epoch): + self._announce_story(irc, channel, story) + max_epoch = max(story['epoch'] for story in stories.values()) + return max(after_epoch, max_epoch) + + def _announce_story(self, irc, channel, story): + """Send a story to a channel.""" + irc.queueMsg(ircmsgs.privmsg(channel, story['text'])) + + Class = Phabricator diff --git a/Phabricator/test.py b/Phabricator/test.py --- a/Phabricator/test.py +++ b/Phabricator/test.py @@ -30,9 +30,104 @@ from supybot.test import * +ENTRY1 = { + 'authorPHID': 'PHID-USER-fozivtfr457sc7smrhtv', + 'chronologicalKey': '6607393807553125523', + 'class': 'PhabricatorApplicationTransactionFeedStory', + 'epoch': 1538403753, + 'objectPHID': 'PHID-TASK-lwuvnwjjnenqsyan73om', + 'text': 'ardumont added a comment to T611: ' + 'support for external definitions ' + 'in the svn/subversion loader.'} -class PhabricatorTestCase(PluginTestCase): +ENTRY2 = { + 'authorPHID': 'PHID-USER-fozivtfr457sc7smrhtv', + 'chronologicalKey': '6607424649221995233', + 'class': 'PhabricatorApplicationTransactionFeedStory', + 'epoch': 1538410933, + 'objectPHID': 'PHID-TASK-lwuvnwjjnenqsyan73om', + 'text': 'ardumont added a comment to T611: ' + 'support for external definitions ' + 'in the svn/subversion loader.'} + +ENTRY3 = { + 'authorPHID': 'PHID-USER-jyszzzys2aaakr2q2ijx', + 'chronologicalKey': '6607410428317095759', + 'class': 'PhabricatorApplicationTransactionFeedStory', + 'epoch': 1538407622, + 'objectPHID': 'PHID-DREV-jaamseb4cyq2glp3ekmr', + 'text': 'Herald added a reviewer for D454: ' + 'Provide Sphinx targets in ' + 'docs/images/ (see D453).: ' + 'Reviewers.'} + +FEED1 = { + 'PHID-STRY-z3obqexdnpd3dfvpwxez': ENTRY1} +FEED2 = { + 'PHID-STRY-z3obqexdnpd3dfvpwxez': ENTRY1, + 'PHID-STRY-zuafozdksyhxqixmqej3': ENTRY3} +FEED3 = { + 'PHID-STRY-z3obqexdnpd3dfvpwxez': ENTRY1, + 'PHID-STRY-zuafozdksyhxqixmqej3': ENTRY2, + 'PHID-STRY-zuicf6fhi4esag22cmw2': ENTRY3} + + +class PhabricatorTestCase(ChannelPluginTestCase): plugins = ('Phabricator',) + config = { + 'supybot.plugins.Phabricator.announce.interval': 1, + } + + def testAnnounce(self): + nb_mock_calls = 0 + + class MockConduit: + class feed: + @classmethod + def query(cls, *, view): + nonlocal nb_mock_calls + nb_mock_calls += 1 + self.assertEqual(view, 'text') + return current_feed + def mock_get_conduit(*args): + return MockConduit + self.irc.getCallback('Phabricator').conduit_for_host_token = \ + mock_get_conduit + + # Check there are no announce the first time. + current_feed = FEED1 + self.assertNotError('config channel plugins.Phabricator.announce True') + m = self.getMsg(' ', timeout=0.1) + self.assertEqual(nb_mock_calls, 1, m) + self.assertIs(m, None) + time.sleep(1.1) + m = self.getMsg(' ', timeout=0.1) + self.assertEqual(nb_mock_calls, 2, m) + self.assertIs(m, None) + + # A new story, that should be announced only after the time interval. + current_feed = FEED2 + m = self.getMsg(' ', timeout=0.1) + self.assertEqual(nb_mock_calls, 2, m) + self.assertIs(m, None) + time.sleep(1.1) + m = self.getMsg(' ', timeout=0.1) + self.assertEqual(nb_mock_calls, 3, m) + self.assertIsNot(m, None) + self.assertIn('Herald added a reviewer for D454:', m.args[1], m) + + # Another new story. + current_feed = FEED3 + m = self.getMsg(' ', timeout=0.1) + self.assertEqual(nb_mock_calls, 3, m) + self.assertIs(m, None) + time.sleep(1.1) + m = self.getMsg(' ', timeout=0.1) + self.assertEqual(nb_mock_calls, 4, m) + self.assertIsNot(m, None) + self.assertIn('ardumont added a comment', m.args[1], m) + + # vim:set shiftwidth=4 tabstop=4 expandtab textwidth=79: