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)
+
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)