diff --git a/history/__init__.py b/history/__init__.py new file mode 100644 index 0000000..fddb6f8 --- /dev/null +++ b/history/__init__.py @@ -0,0 +1 @@ +"""Track IRC channel history and report on it for users.""" diff --git a/history/ircplugin.py b/history/ircplugin.py new file mode 100644 index 0000000..4a8b6cb --- /dev/null +++ b/history/ircplugin.py @@ -0,0 +1,142 @@ +"""Monitor and playback IRC stuff.""" +import logging + +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'what did I miss\?$' + + 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) + + self.channel_history = {} + self.channel_participants = {} + self.channel_leave_points = {} + + 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 + + logger.debug("tracking message for %s: (%s,%s)", where, who, what) + history = self.channel_history.setdefault(where, []) + history.append((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 = [] + for channel in self.channel_leave_points.keys(): + logger.debug("checking history slice for %s", channel) + total_history += self._missed_slice(channel, who) + logger.debug("total history so far: %s", total_history) + logger.debug("final missed history: %s", total_history) + self._send_history(who, total_history) + return 'NO MORE' + else: + where = event.target + history = self._missed_slice(where, who) + self._send_history(who, history) + 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]}") + + 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 _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.remove(who) + logger.debug("participants for %s: %s", where, participants) + + +plugin = History