From e8f88b2ee19d2bc7eb712515bacf904f626b67ba Mon Sep 17 00:00:00 2001 From: Adam Spiers Date: Mon, 29 Apr 2019 22:50:44 -0600 Subject: [PATCH] Add subscribe command for automatic notifications of topics Add a new 'subscribe' command which allows people to subscribe for automatic notifications via direct message of topics which match the subscription regex they provide. With no regex argument it shows the user's current subscription (if any). Also add a new 'unsubscribe' command for clearing the regex. Example use cases: 1. I know that nova is planning to discuss $TOPIC some time tomorrow but they don't know exactly when, and I want to spend most of the day in another room whilst ensuring I don't miss that particular discussion on $TOPIC => "/msg ptgbot subscribe $TOPIC" will give me notifications when the PTL types "#nova next $TOPIC" and "#nova now $TOPIC". 2. I'm interested in *all* discussion on Python 3. I don't know which projects are planning to discuss it, let alone when, but that doesn't matter, because I can type "/msg ptgbot subscribe python ?3" and get notified of all Python 3 discussions. As with the presence tracking commands, these commands can be used in public channels by preceding them with a '#' character. Change-Id: I3f51acc318ecf31d435768640cef6c46d8ca136c --- README.rst | 18 +++++++++++-- html/ptg.html | 9 +++++++ ptgbot/bot.py | 70 +++++++++++++++++++++++++++++++++++++++++++++++++-- ptgbot/db.py | 22 +++++++++++++++- 4 files changed, 114 insertions(+), 5 deletions(-) diff --git a/README.rst b/README.rst index 6bb210f..9bd988e 100644 --- a/README.rst +++ b/README.rst @@ -37,11 +37,25 @@ Anyone can privately message the bot with the following commands: * ``seen NICK`` - asks the bot where the user with the given IRC nick was last seen (if anywhere). The nick is case-insensitive. +* ``subscribe REGEXP`` - subscribes for a direct message notification + from the bot whenever a topic with a substring matching ``REGEXP`` + is set via the ``now`` or ``next`` commands (see below). The exact + string the (case-insensitive) regular expression will be matched + against is of the form ``#track now topic`` (i.e. the same as the + full commands issued by track moderators). So for example + ``subscribe #nova.*test|python *3`` would match any testing topics + in the nova track, and any Python 3 topics in any track. + +* ``subscribe`` - shows your current subscription regular expression + (if any) + +* ``unsubscribe`` - cancels your current subscription (if any) + The above commands also work in the channel when prefixed with ``#``, for example ``#in the pub``. You can use the ``#`` prefix with private messages to the bot too, in case you don't want to memorise -different syntax for these presence-tracking commands depending on -whether you are messaging the bot privately or in a channel. +different syntax for these commands depending on whether you are +messaging the bot privately or in a channel. Track moderators commands diff --git a/html/ptg.html b/html/ptg.html index 5775c9c..170bc60 100644 --- a/html/ptg.html +++ b/html/ptg.html @@ -113,6 +113,15 @@ (more help) +
+

Worried about missing discussions on your favourite topic?

+
+ Message the bot with subscribe REGEXP to get a + notification message when any topic matching that REGEXP is being + discussed or up next. + (more help) +
+

Looking for someone, or want to be easy to find?

diff --git a/ptgbot/bot.py b/ptgbot/bot.py index 171d014..58aca39 100644 --- a/ptgbot/bot.py +++ b/ptgbot/bot.py @@ -21,6 +21,7 @@ from ib3.connection import SSL import irc.bot import json import logging.config +import re import os import time import textwrap @@ -107,9 +108,13 @@ class PTGBot(SASL, SSL, irc.bot.SingleServerIRCBot): self.check_out(nick, nick, words) elif cmd == 'seen': self.last_seen(nick, nick, words) + elif cmd == 'subscribe': + self.subscribe(nick, nick, msg.lstrip('#' + cmd).strip()) + elif cmd == 'unsubscribe': + self.unsubscribe(nick, nick) else: - self.send_priv_or_pub(nick, None, - "Recognised commands: in, out, seen") + self.send_priv_or_pub( + nick, None, "Recognised commands: in, out, seen, subscribe") def check_in(self, reply_to, nick, words): if len(words) == 0: @@ -190,6 +195,40 @@ class PTGBot(SASL, SSL, irc.bot.SingleServerIRCBot): (last_check_in['nick'], last_check_in['location'], last_check_in['out'])) + def subscribe(self, reply_to, nick, new_re): + existing_re = self.data.get_subscription(nick) + if new_re == "": + if existing_re is None: + self.send_priv_or_pub( + reply_to, nick, + "You don't have a subscription regex set yet" + ) + else: + self.send_priv_or_pub( + reply_to, nick, + "Your current subscription regex is: " + existing_re) + else: + self.data.set_subscription(nick, new_re) + self.send_priv_or_pub( + reply_to, nick, + "Subscription set to " + new_re + + (" (was %s)" % existing_re if existing_re else "") + ) + + def unsubscribe(self, reply_to, nick): + existing_re = self.data.get_subscription(nick) + if existing_re is None: + self.send_priv_or_pub( + reply_to, nick, + "You don't have a subscription regex set yet" + ) + else: + self.data.set_subscription(nick, None) + self.send_priv_or_pub( + reply_to, nick, + "Cancelled subscription %s" % existing_re + ) + def on_pubmsg(self, c, e): if not self.identify_msg_cap: self.log.debug("Ignoring message because identify-msg " @@ -213,6 +252,13 @@ class PTGBot(SASL, SSL, irc.bot.SingleServerIRCBot): self.last_seen(chan, nick, words[1:]) return + elif cmd == '#subscribe': + self.subscribe(chan, nick, msg.lstrip('#' + cmd).strip()) + return + elif cmd == '#unsubscribe': + self.unsubscribe(chan, nick) + return + if (self.data.is_voice_required() and not (self.channels[chan].is_voiced(nick) or self.channels[chan].is_oper(nick))): @@ -239,8 +285,10 @@ class PTGBot(SASL, SSL, irc.bot.SingleServerIRCBot): params = str.join(' ', words[2:]) if adverb == 'now': self.data.add_now(track, params) + self.notify(track, adverb, params) elif adverb == 'next': self.data.add_next(track, params) + self.notify(track, adverb, params) elif adverb == 'clean': self.data.clean_tracks([track]) elif adverb == 'color': @@ -318,6 +366,24 @@ class PTGBot(SASL, SSL, irc.bot.SingleServerIRCBot): self.send(chan, "%s: unknown command '%s'" % (nick, command)) return + def notify(self, track, adverb, params): + location = self.data.get_location(track) + track = '#' + track + trackloc = track + if location is not None: + trackloc = "%s (%s)" % (track, location) + + for nick, regexp in self.data.get_subscriptions().items(): + event_text = " ".join([track, adverb, params]) + if re.search(regexp, event_text, re.IGNORECASE): + message = "%s in %s: %s" % (adverb, trackloc, params) + # Note: there is no guarantee that nick will be online + # at this point. However if not, the bot will receive + # a 401 :No such nick/channel message which it will + # ignore due to the lack of a nosuchnick handler. + # Fortunately this is the behaviour we want. + self.send(nick, message) + def send_priv_or_pub(self, target, nick, msg): if target.startswith('#') and nick is not None: self.send(target, "%s: %s" % (nick, msg)) diff --git a/ptgbot/db.py b/ptgbot/db.py index 599500c..f481b65 100644 --- a/ptgbot/db.py +++ b/ptgbot/db.py @@ -36,7 +36,8 @@ class PTGDataBase(): 'links': OrderedDict(), # Keys for last_check_in are lower-cased nicks; # values are in the same format as BASE_CHECK_IN - 'last_check_in': OrderedDict()} + 'last_check_in': OrderedDict(), + 'subscriptions': OrderedDict()} BASE_CHECK_IN = { 'nick': None, # original case for use in output @@ -117,6 +118,9 @@ class PTGDataBase(): self.data['location'][track] = location self.save() + def get_location(self, track): + return self.data['location'].get(track) + def add_next(self, track, session): if track not in self.data['next']: self.data['next'][track] = [] @@ -234,6 +238,22 @@ class PTGDataBase(): self.save() return self.data['last_check_in'][nick]['location'] + def get_subscription(self, nick): + if 'subscriptions' not in self.data: + return None + return self.data['subscriptions'].get(nick) + + def get_subscriptions(self): + if 'subscriptions' not in self.data: + return {} + return self.data['subscriptions'] + + def set_subscription(self, nick, regexp): + if 'subscriptions' not in self.data: + self.data['subscriptions'] = OrderedDict() + self.data['subscriptions'][nick] = regexp + self.save() + def save(self): timestamp = datetime.datetime.now() self.data['timestamp'] = self.serialise_timestamp(timestamp)