diff --git a/README.rst b/README.rst index c75bb66..6bb210f 100644 --- a/README.rst +++ b/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 ========================= diff --git a/html/ptg.html b/html/ptg.html index 4bc93de..5775c9c 100644 --- a/html/ptg.html +++ b/html/ptg.html @@ -113,6 +113,16 @@ (more help) +
+

Looking for someone, or want to be easy to find?

+
+ Use #seen NICK to see if a user has checked in to a particular location. + Use #in LOCATION to check in somewhere, and #out to check out. +
+ Presence-tracking commands can also be sent privately to the bot. + (more help) +
+

Content on this page is being driven by room operators through the openstackptg bot on the #openstack-ptg IRC channel. It was last refreshed on {{timestamp}}.

diff --git a/ptgbot/bot.py b/ptgbot/bot.py index d324d55..171d014 100644 --- a/ptgbot/bot.py +++ b/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) diff --git a/ptgbot/db.py b/ptgbot/db.py index 34fc725..599500c 100644 --- a/ptgbot/db.py +++ b/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)