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 3dd7787..9ddad5f 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") # Checks location against known tracks. If prefixed with # then # insists it must match a known track. If not #-prefixed but @@ -205,6 +210,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 " @@ -228,6 +267,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))): @@ -254,8 +300,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': @@ -333,6 +381,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)