Merge "Add subscribe command for automatic notifications of topics"

This commit is contained in:
Zuul 2019-04-30 21:49:16 +00:00 committed by Gerrit Code Review
commit 4fbaf41725
4 changed files with 114 additions and 5 deletions

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 * ``seen NICK`` - asks the bot where the user with the given IRC nick
was last seen (if anywhere). The nick is case-insensitive. 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 ``#``, The above commands also work in the channel when prefixed with ``#``,
for example ``#in the pub``. You can use the ``#`` prefix 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 private messages to the bot too, in case you don't want to memorise
different syntax for these presence-tracking commands depending on different syntax for these commands depending on whether you are
whether you are messaging the bot privately or in a channel. messaging the bot privately or in a channel.
Track moderators commands Track moderators commands

View File

@ -113,6 +113,15 @@
<a href="https://opendev.org/openstack/ptgbot/src/branch/master/README.rst">(more help)</a> <a href="https://opendev.org/openstack/ptgbot/src/branch/master/README.rst">(more help)</a>
</div> </div>
</div> </div>
<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="https://opendev.org/openstack/ptgbot/src/branch/master/README.rst">(more help)</a>
</div>
</div>
<div class="panel panel-default"> <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="panel-heading"><h3 class="panel-title">Looking for someone, or want to be easy to find?</h3></div>
<div class="bot-help"> <div class="bot-help">

View File

@ -21,6 +21,7 @@ from ib3.connection import SSL
import irc.bot import irc.bot
import json import json
import logging.config import logging.config
import re
import os import os
import time import time
import textwrap import textwrap
@ -107,9 +108,13 @@ class PTGBot(SASL, SSL, irc.bot.SingleServerIRCBot):
self.check_out(nick, nick, words) self.check_out(nick, nick, words)
elif cmd == 'seen': elif cmd == 'seen':
self.last_seen(nick, nick, words) 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: else:
self.send_priv_or_pub(nick, None, self.send_priv_or_pub(
"Recognised commands: in, out, seen") nick, None, "Recognised commands: in, out, seen, subscribe")
# Checks location against known tracks. If prefixed with # then # Checks location against known tracks. If prefixed with # then
# insists it must match a known track. If not #-prefixed but # 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['nick'], last_check_in['location'],
last_check_in['out'])) 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): def on_pubmsg(self, c, e):
if not self.identify_msg_cap: if not self.identify_msg_cap:
self.log.debug("Ignoring message because identify-msg " 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:]) self.last_seen(chan, nick, words[1:])
return 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 if (self.data.is_voice_required() and not
(self.channels[chan].is_voiced(nick) or (self.channels[chan].is_voiced(nick) or
self.channels[chan].is_oper(nick))): self.channels[chan].is_oper(nick))):
@ -254,8 +300,10 @@ class PTGBot(SASL, SSL, irc.bot.SingleServerIRCBot):
params = str.join(' ', words[2:]) params = str.join(' ', words[2:])
if adverb == 'now': if adverb == 'now':
self.data.add_now(track, params) self.data.add_now(track, params)
self.notify(track, adverb, params)
elif adverb == 'next': elif adverb == 'next':
self.data.add_next(track, params) self.data.add_next(track, params)
self.notify(track, adverb, params)
elif adverb == 'clean': elif adverb == 'clean':
self.data.clean_tracks([track]) self.data.clean_tracks([track])
elif adverb == 'color': elif adverb == 'color':
@ -333,6 +381,24 @@ class PTGBot(SASL, SSL, irc.bot.SingleServerIRCBot):
self.send(chan, "%s: unknown command '%s'" % (nick, command)) self.send(chan, "%s: unknown command '%s'" % (nick, command))
return 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): def send_priv_or_pub(self, target, nick, msg):
if target.startswith('#') and nick is not None: if target.startswith('#') and nick is not None:
self.send(target, "%s: %s" % (nick, msg)) self.send(target, "%s: %s" % (nick, msg))

View File

@ -36,7 +36,8 @@ class PTGDataBase():
'links': OrderedDict(), 'links': OrderedDict(),
# Keys for last_check_in are lower-cased nicks; # Keys for last_check_in are lower-cased nicks;
# values are in the same format as BASE_CHECK_IN # values are in the same format as BASE_CHECK_IN
'last_check_in': OrderedDict()} 'last_check_in': OrderedDict(),
'subscriptions': OrderedDict()}
BASE_CHECK_IN = { BASE_CHECK_IN = {
'nick': None, # original case for use in output 'nick': None, # original case for use in output
@ -117,6 +118,9 @@ class PTGDataBase():
self.data['location'][track] = location self.data['location'][track] = location
self.save() self.save()
def get_location(self, track):
return self.data['location'].get(track)
def add_next(self, track, session): def add_next(self, track, session):
if track not in self.data['next']: if track not in self.data['next']:
self.data['next'][track] = [] self.data['next'][track] = []
@ -234,6 +238,22 @@ class PTGDataBase():
self.save() self.save()
return self.data['last_check_in'][nick]['location'] 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): def save(self):
timestamp = datetime.datetime.now() timestamp = datetime.datetime.now()
self.data['timestamp'] = self.serialise_timestamp(timestamp) self.data['timestamp'] = self.serialise_timestamp(timestamp)