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

View File

@ -113,6 +113,15 @@
<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-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-heading"><h3 class="panel-title">Looking for someone, or want to be easy to find?</h3></div>
<div class="bot-help">

View File

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

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()}
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)