Add presence tracking commands
Add in/out/seen commands to let people voluntarily check in and out of tracks and other arbitrary locations, to make it easy for others to find them. Of course this is entirely optional. It's designed to cope gracefully with people forgetting to check out of locations they previously checked into. Change-Id: I0d88a540ad7a333841c208dd7f2a7247897eb238
This commit is contained in:
parent
4b30f5224e
commit
946d69b140
31
README.rst
31
README.rst
@ -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
|
||||
=========================
|
||||
|
@ -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>
|
||||
|
||||
|
126
ptgbot/bot.py
126
ptgbot/bot.py
@ -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)
|
||||
|
47
ptgbot/db.py
47
ptgbot/db.py
@ -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)
|
||||
|
Loading…
x
Reference in New Issue
Block a user