"""Monitor and playback IRC stuff.""" import logging from datetime import datetime import irc.client from ircbot.lib import Plugin, most_specific_message logger = logging.getLogger(__name__) class History(Plugin): """Watch the history of IRC channels and try to track what users may have missed.""" what_missed_regex = r'(?i)(what did I miss\?|did I miss anything\?)$' def __init__(self, bot, connection, event): """Initialize some tracking stuff.""" super(History, self).__init__(bot, connection, event) self.channel_history = {} self.channel_participants = {} self.channel_leave_points = {} def start(self): """Set up the handlers.""" logger.debug("%s starting up", __name__) self.connection.add_global_handler('pubmsg', self.handle_chatter, 50) self.connection.add_global_handler('join', self.handle_join, 50) self.connection.add_global_handler('part', self.handle_part, 50) self.connection.add_global_handler('quit', self.handle_quit, 50) self.connection.reactor.add_global_regex_handler(['pubmsg', 'privmsg'], self.what_missed_regex, self.handle_what_missed, 60) super(History, self).start() def stop(self): """Tear down handlers.""" logger.debug("%s shutting down", __name__) self.connection.remove_global_handler('pubmsg', self.handle_chatter) self.connection.remove_global_handler('join', self.handle_join) self.connection.remove_global_handler('part', self.handle_part) self.connection.remove_global_handler('quit', self.handle_quit) self.connection.reactor.remove_global_regex_handler(['pubmsg', 'privmsg'], self.handle_what_missed) super(History, self).stop() def handle_chatter(self, connection, event): """Track IRC chatter.""" what = event.arguments[0] where = event.target who = irc.client.NickMask(event.source).nick when = datetime.now() logger.debug("tracking message for %s: (%s,%s)", where, who, what) history = self.channel_history.setdefault(where, []) history.append((where, when.isoformat(), who, what)) logger.debug("history for %s: %s", where, history) # for when we maybe don't see a join, if they talked in the channel, add them to it self._add_channel_participant(where, who) def handle_join(self, connection, event): """Track who is entitled to see channel history.""" where = event.target who = irc.client.NickMask(event.source).nick logger.debug("%s joined %s", who, where) self._add_channel_participant(where, who) def handle_part(self, connection, event): """Note when people leave IRC channels.""" where = event.target who = irc.client.NickMask(event.source).nick logger.debug("%s left %s", who, where) # if they parted the channel, they must have been in it, so note their point in history self._add_channel_leave_point(where, who) self._remove_channel_participant(where, who) def handle_quit(self, connection, event): """Note when people leave IRC.""" who = irc.client.NickMask(event.source).nick logger.debug("%s disconnected", who) # find all channels the quitter was in, save their leave points for channel in self.channel_participants.keys(): self._add_channel_leave_point(channel, who) self._remove_channel_participant(channel, who) def handle_what_missed(self, connection, event, match): """Tell the user what they missed.""" who = irc.client.NickMask(event.source).nick if event.in_privmsg or event.addressed: logger.debug("<%s> %s is asking for an update", who, most_specific_message(event)) if event.in_privmsg: total_history = [] channel_count = 0 for channel in self.channel_leave_points.keys(): logger.debug("checking history slice for %s", channel) total_history += self._missed_slice(channel, who) self._delete_channel_leave_point(channel, who) logger.debug("total history so far: %s", total_history) channel_count += 1 logger.debug("final missed history: %s", total_history) self._send_history(who, total_history) self.bot.reply(event, f"{len(total_history)} line(s) over {channel_count} channel(s)") return 'NO MORE' else: where = event.target history = self._missed_slice(where, who) self._delete_channel_leave_point(where, who) self._send_history(who, history) privmsged_str = " (PRIVMSGed)" if history else "" self.bot.reply(event, f"{len(history)} line(s){privmsged_str}") return 'NO MORE' def _send_history(self, who, history): """Reply to who with missed history.""" for line in history: self.bot.privmsg(who, f"{line[0]}: [{line[1]}] <{line[2]}> {line[3]}") def _add_channel_leave_point(self, where, who): """Note that the given who left the channel at the current history point.""" leave_points = self.channel_leave_points.setdefault(where, {}) leave_points[who] = len(self.channel_history.setdefault(where, [])) - 1 logger.debug("leave points for %s: %s", where, leave_points) def _delete_channel_leave_point(self, where, who): """Remove tracking for user's history point.""" leave_points = self.channel_leave_points.setdefault(where, {}) leave_points.pop(who, None) logger.debug("leave points for %s: %s", where, leave_points) def _add_channel_participant(self, where, who): """Add a who to the list of people who are/were in a channel.""" participants = self.channel_participants.setdefault(where, set()) participants.add(who) logger.debug("participants for %s: %s", where, participants) def _missed_slice(self, where, who): """Get the lines in where since who last left.""" leave_points = self.channel_leave_points.setdefault(where, {}) if leave_points.get(who) is not None: leave_point = leave_points.get(who) + 1 history = self.channel_history.setdefault(where, []) missed_history = history[leave_point:] logger.debug("in %s, %s missed: %s", where, who, missed_history) return missed_history return [] def _remove_channel_participant(self, where, who): """Remove the specified who from the where channel's participants list.""" participants = self.channel_participants.setdefault(where, set()) participants.discard(who) logger.debug("participants for %s: %s", where, participants) plugin = History