Browse Source

Merge "Add subscribe command for automatic notifications of topics"

Zuul 2 years ago
committed by Gerrit Code Review
4 changed files with 114 additions and 5 deletions
  1. +16
  2. +9
  3. +68
  4. +21

+ 16
- 2
README.rst View File

@ -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

+ 9
- 0
html/ptg.html View File

@ -113,6 +113,15 @@
<a href="">(more help)</a>
<div class="panel panel-default">
<div class="panel-heading"><h3 class="panel-title">Worried about missing discussions on your favourite topic?</h3></div>
<div class="bot-help">
Message the bot with <code>subscribe REGEXP</code> to get a
notification message when any topic matching that REGEXP is being
discussed or up next.
<a href="">(more help)</a>
<div class="panel panel-default">
<div class="panel-heading"><h3 class="panel-title">Looking for someone, or want to be easy to find?</h3></div>
<div class="bot-help">

+ 68
- 2
ptgbot/ View File

@ -21,6 +21,7 @@ from ib3.connection import SSL
import json
import logging.config
import re
import os
import time
import textwrap
@ -107,9 +108,13 @@ class PTGBot(SASL, SSL,
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)
self.send_priv_or_pub(nick, None,
"Recognised commands: in, out, seen")
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,
(last_check_in['nick'], last_check_in['location'],
def subscribe(self, reply_to, nick, new_re):
existing_re =
if new_re == "":
if existing_re is None:
reply_to, nick,
"You don't have a subscription regex set yet"
reply_to, nick,
"Your current subscription regex is: " + existing_re)
else:, new_re)
reply_to, nick,
"Subscription set to " + new_re +
(" (was %s)" % existing_re if existing_re else "")
def unsubscribe(self, reply_to, nick):
existing_re =
if existing_re is None:
reply_to, nick,
"You don't have a subscription regex set yet"
else:, None)
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,
self.last_seen(chan, nick, words[1:])
elif cmd == '#subscribe':
self.subscribe(chan, nick, msg.lstrip('#' + cmd).strip())
elif cmd == '#unsubscribe':
self.unsubscribe(chan, nick)
if ( and not
(self.channels[chan].is_voiced(nick) or
@ -254,8 +300,10 @@ class PTGBot(SASL, SSL,
params = str.join(' ', words[2:])
if adverb == 'now':, params)
self.notify(track, adverb, params)
elif adverb == 'next':, params)
self.notify(track, adverb, params)
elif adverb == 'clean':[track])
elif adverb == 'color':
@ -333,6 +381,24 @@ class PTGBot(SASL, SSL,
self.send(chan, "%s: unknown command '%s'" % (nick, command))
def notify(self, track, adverb, params):
location =
track = '#' + track
trackloc = track
if location is not None:
trackloc = "%s (%s)" % (track, location)
for nick, regexp in
event_text = " ".join([track, adverb, params])
if, 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))

+ 21
- 1
ptgbot/ View File

@ -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()}
'nick': None, # original case for use in output
@ -117,6 +118,9 @@ class PTGDataBase():['location'][track] = location
def get_location(self, track):
def add_next(self, track, session):
if track not in['next']:['next'][track] = []
@ -234,6 +238,22 @@ class PTGDataBase():
def get_subscription(self, nick):
if 'subscriptions' not in
return None
def get_subscriptions(self):
if 'subscriptions' not in
return {}
def set_subscription(self, nick, regexp):
if 'subscriptions' not in['subscriptions'] = OrderedDict()['subscriptions'][nick] = regexp
def save(self):
timestamp =['timestamp'] = self.serialise_timestamp(timestamp)