diff --git a/irclog.py b/irclog.py index 2ecc0e3..8b5f154 100644 --- a/irclog.py +++ b/irclog.py @@ -205,6 +205,9 @@ class IRCClientProtocol(asyncio.Protocol): self.server = ircstates.Server(self.config['irc']['host']) self.capReqsPending = set() # Capabilities requested from the server but not yet ACKd or NAKd self.caps = set() # Capabilities acknowledged by the server + self.whoxQueue = collections.deque() # Names of channels that were joined successfully but for which no WHO (WHOX) query was sent yet + self.whoxChannel = None # Name of channel for which a WHO query is currently running + self.whoxReply = [] # List of (nickname, account) tuples from the currently running WHO query @staticmethod def nick_command(nick: str): @@ -320,10 +323,12 @@ class IRCClientProtocol(asyncio.Protocol): self.logger.debug(f'Message received at {time_}: {message!r}') # Queue message for storage + # Note: WHOX is queued further down self.messageQueue.put_nowait((time_, b'< ' + message, None, None)) for command, channel, logMessage in self.render_message(line): self.messageQueue.put_nowait((time_, logMessage, command, channel)) + maybeTriggerWhox = False # PING/PONG if line.command == 'PING': self.send(irctokens.build('PONG', line.params).format().encode('utf-8')) @@ -396,11 +401,33 @@ class IRCClientProtocol(asyncio.Protocol): self.channels.remove(channel) break + # WHOX on successful JOIN if supported to fetch account information + elif line.command == 'JOIN' and self.server.isupport.whox and line.source and self.server.casefold(line.hostmask.nickname) == self.server.casefold(self.server.nickname): + self.whoxQueue.extend(line.params[0].split(',')) + maybeTriggerWhox = True + + # WHOX response + elif line.command == ircstates.numerics.RPL_WHOSPCRPL and line.params[1] == '042': + self.whoxReply.append((line.params[2], line.params[3] if line.params[3] != '0' else None)) + + # End of WHOX response + elif line.command == ircstates.numerics.RPL_ENDOFWHO: + self.messageQueue.put_nowait((time_, self.render_whox(), 'WHOX', self.whoxChannel)) + self.whoxChannel = None + self.whoxReply = [] + maybeTriggerWhox = True + # General fatal ERROR elif line.command == 'ERROR': self.logger.error(f'Server sent ERROR: {message!r}') self.transport.close() + # Send next WHOX if appropriate + if maybeTriggerWhox and self.whoxChannel is None and self.whoxQueue: + self.whoxChannel = self.whoxQueue.popleft() + self.whoxReply = [] + self.send(b'WHO ' + self.whoxChannel.encode('utf-8') + b' c%nat,042') + def get_mode_char(self, channelUser): if channelUser is None: return '' @@ -471,6 +498,13 @@ class IRCClientProtocol(asyncio.Protocol): users = self.server.channels[self.server.casefold(channel)].users yield 'NAMES', channel, f'Currently in {channel}: {", ".join(self.render_nick_with_mode(u, u.nickname) for u in users.values())}' + def render_whox(self): + users = [] + for nickname, account in self.whoxReply: + accountStr = f' ({account})' if account is not None else '' + users.append(f'{self.render_nick_with_mode(self.server.channels[self.server.casefold(self.whoxChannel)].users.get(self.server.casefold(nickname)), nickname)}{accountStr}') + return f'Currently in {self.whoxChannel}: {", ".join(users)}' + async def quit(self): # The server acknowledges a QUIT by sending an ERROR and closing the connection. The latter triggers connection_lost, so just wait for the closure event. self.logger.info('Quitting')