Merge "Add presence tracking commands"

This commit is contained in:
Zuul 2019-04-28 20:03:21 +00:00 committed by Gerrit Code Review
commit 9cb30c28a9
4 changed files with 209 additions and 5 deletions

View File

@ -12,6 +12,37 @@ with several sections of information:
* The tracks pre-scheduled for the day
* The tracks which booked available slots in the additional rooms
The bot also allows people to voluntarily check into (and out of)
tracks or other arbitrary locations, if they want to be found more
easily by other people.
User commands
=============
Anyone can privately message the bot with the following commands:
* ``in #TRACKNAME`` - tells the bot you are currently in the track
named ``TRACKNAME``. This must be one of the tracks it knows about,
for example: ``in #nova``
* ``in LOCATION`` - tells the bot you are currently in a location
which doesn't correspond to any track. This can be any freeform
text, for example: ``in the pub``
* ``out`` - tells the bot you've checked out of your current location.
However others will still be able to see when and where you checked
out.
* ``seen NICK`` - asks the bot where the user with the given IRC nick
was last seen (if anywhere). The nick is case-insensitive.
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.
Track moderators commands
=========================

View File

@ -113,6 +113,16 @@
<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">
Use <code>#seen NICK</code> to see if a user has checked in to a particular location.
Use <code>#in LOCATION</code> to check in somewhere, and <code>#out</code> to check out.
<br />
Presence-tracking commands can also be sent privately to the bot.
<a href="https://opendev.org/openstack/ptgbot/src/branch/master/README.rst">(more help)</a>
</div>
</div>
<p class="text-muted">Content on this page is being driven by room operators through the <a href="https://opendev.org/openstack/ptgbot/src/branch/master/README.rst">openstackptg bot</a> on the <a href="http://eavesdrop.openstack.org/irclogs/%23openstack-ptg/">#openstack-ptg IRC channel</a>. It was last refreshed on {{timestamp}}.</p>
</script>

View File

@ -87,6 +87,109 @@ class PTGBot(SASL, SSL, irc.bot.SingleServerIRCBot):
else:
self.send(channel, "There are no active tracks defined yet")
def on_privmsg(self, c, e):
if not self.identify_msg_cap:
self.log.debug("Ignoring message because identify-msg "
"cap not enabled")
return
nick = e.source.split('!')[0]
msg = e.arguments[0][1:]
words = msg.split()
cmd = words[0].lower()
words.pop(0)
if cmd.startswith('#'):
cmd = cmd[1:]
if cmd == 'in':
self.check_in(nick, nick, words)
elif cmd == 'out':
self.check_out(nick, nick, words)
elif cmd == 'seen':
self.last_seen(nick, nick, words)
else:
self.send_priv_or_pub(nick, None,
"Recognised commands: in, out, seen")
def check_in(self, reply_to, nick, words):
if len(words) == 0:
self.send_priv_or_pub(
reply_to, nick,
"The 'in' command should be followed by a location.")
return
location = " ".join(words)
if location.startswith('#'):
track = location[1:].lower()
tracks = self.data.list_tracks()
if track not in tracks:
self.send_priv_or_pub(
reply_to, nick, "Unrecognised track #%s" % track)
return
self.data.check_in(nick, location)
self.send_priv_or_pub(
reply_to, nick,
"OK, checked into %s - thanks for the update!" % location)
def check_out(self, reply_to, nick, words):
if len(words) > 0:
self.send_priv_or_pub(
reply_to, nick,
"The 'out' command does not accept any extra parameters.")
return
last_check_in = self.data.get_last_check_in(nick)
if last_check_in['location'] is None:
self.send_priv_or_pub(
reply_to, nick, "You weren't checked in anywhere yet!")
return
if last_check_in['out'] is not None:
self.send_priv_or_pub(
reply_to, nick,
"You already checked out of %s at %s!" %
(last_check_in['location'], last_check_in['out']))
return
location = self.data.check_out(nick)
self.send_priv_or_pub(
reply_to, nick,
"OK, checked out of %s - thanks for the update!" % location)
def last_seen(self, reply_to, nick, words):
if len(words) != 1:
self.send_priv_or_pub(
reply_to, nick,
"The 'seen' command needs a single nick argument.")
return
seen_nick = words[0]
if nick.lower() == seen_nick.lower():
self.send_priv_or_pub(
reply_to, nick,
"In case you hadn't noticed, you're right here.")
return
last_check_in = self.data.get_last_check_in(seen_nick)
if last_check_in['location'] is None:
self.send_priv_or_pub(
reply_to, nick,
"%s never checked in anywhere" % seen_nick)
elif last_check_in['out'] is None:
self.send_priv_or_pub(
reply_to, nick,
"%s was last seen in %s at %s" %
(last_check_in['nick'], last_check_in['location'],
last_check_in['in']))
else:
self.send_priv_or_pub(
reply_to, nick,
"%s checked out of %s at %s" %
(last_check_in['nick'], last_check_in['location'],
last_check_in['out']))
def on_pubmsg(self, c, e):
if not self.identify_msg_cap:
self.log.debug("Ignoring message because identify-msg "
@ -97,15 +200,26 @@ class PTGBot(SASL, SSL, irc.bot.SingleServerIRCBot):
chan = e.target
if msg.startswith('#'):
words = msg.split()
cmd = words[0].lower()
if cmd == '#in':
self.check_in(chan, nick, words[1:])
return
elif cmd == '#out':
self.check_out(chan, nick, words[1:])
return
elif cmd == '#seen':
self.last_seen(chan, nick, words[1:])
return
if (self.data.is_voice_required() and not
(self.channels[chan].is_voiced(nick) or
self.channels[chan].is_oper(nick))):
self.send(chan, "%s: Need voice to issue commands" % (nick,))
return
words = msg.split()
if words[0] == '#help':
if cmd == '#help':
self.usage(chan)
return
@ -204,6 +318,12 @@ class PTGBot(SASL, SSL, irc.bot.SingleServerIRCBot):
self.send(chan, "%s: unknown command '%s'" % (nick, command))
return
def send_priv_or_pub(self, target, nick, msg):
if target.startswith('#') and nick is not None:
self.send(target, "%s: %s" % (nick, msg))
else:
self.send(target, msg)
def send(self, channel, msg):
# 400 chars is an estimate of a safe line length (which can vary)
chunks = textwrap.wrap(msg, 400)

View File

@ -33,7 +33,15 @@ class PTGDataBase():
'schedule': OrderedDict(),
'voice': 0,
'motd': {'message': '', 'level': 'info'},
'links': OrderedDict()}
'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()}
BASE_CHECK_IN = {
'nick': None, # original case for use in output
'location': None, 'in': None, 'out': None
}
def __init__(self, config):
self.filename = config['db_filename']
@ -194,9 +202,44 @@ class PTGDataBase():
self.data['motd'] = {'level': '', 'message': ''}
self.save()
def _blank_check_in(self):
# No need for a copy here
return OrderedDict(self.BASE_CHECK_IN)
def get_last_check_in(self, nick):
if 'last_check_in' not in self.data:
return self._blank_check_in()
return self.data['last_check_in'].get(
nick.lower(), self._blank_check_in())
def check_in(self, nick, location):
if 'last_check_in' not in self.data:
self.data['last_check_in'] = OrderedDict()
self.data['last_check_in'][nick.lower()] = {
'nick': nick,
'location': location,
'in': self.serialise_timestamp(datetime.datetime.now()),
'out': None # no check-out yet
}
self.save()
# Returns location if successfully checked out, otherwise None
def check_out(self, nick):
if 'last_check_in' not in self.data:
self.data['last_check_in'] = OrderedDict()
if nick.lower() not in self.data['last_check_in']:
return None
self.data['last_check_in'][nick.lower()]['out'] = \
self.serialise_timestamp(datetime.datetime.now())
self.save()
return self.data['last_check_in'][nick]['location']
def save(self):
timestamp = datetime.datetime.now()
self.data['timestamp'] = '{:%Y-%m-%d %H:%M:%S}'.format(timestamp)
self.data['timestamp'] = self.serialise_timestamp(timestamp)
self.data['tracks'] = sorted(self.data['tracks'])
with open(self.filename, 'w') as fp:
json.dump(self.data, fp)
def serialise_timestamp(self, timestamp):
return '{:%Y-%m-%d %H:%M:%S}'.format(timestamp)