diff --git a/irclog.py b/irclog.py index 8cdd43d..70094ff 100644 --- a/irclog.py +++ b/irclog.py @@ -203,6 +203,8 @@ class IRCClientProtocol(asyncio.Protocol): self.sasl = bool(self.config['irc']['certfile'] and self.config['irc']['certkeyfile']) self.authenticated = False 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 @staticmethod def nick_command(nick: str): @@ -236,8 +238,12 @@ class IRCClientProtocol(asyncio.Protocol): self.logger.info('IRC connected') self.transport = transport self.connected = True + caps = [b'userhost-in-names', b'away-notify', b'account-notify', b'extended-join'] if self.sasl: - self.send(b'CAP REQ :sasl') + caps.append(b'sasl') + for cap in caps: + self.capReqsPending.add(cap.decode('ascii')) + self.send(b'CAP REQ :' + cap) self.send(self.nick_command(self.config['irc']['nick'])) self.send(self.user_command(self.config['irc']['nick'], self.config['irc']['real'])) @@ -322,18 +328,29 @@ class IRCClientProtocol(asyncio.Protocol): if line.command == 'PING': self.send(irctokens.build('PONG', line.params).format().encode('utf-8')) - # SASL - elif line.command == 'CAP' and self.sasl: - if line.params[-2] == 'ACK' and 'sasl' in line.params[-1].split(' '): - self.send(b'AUTHENTICATE EXTERNAL') - else: - self.logger.error(f'Received unexpected CAP reply {message!r}, terminating connection') - self.transport.close() + # IRCv3 and SASL + elif line.command == 'CAP': + if line.params[1] == 'ACK': + for cap in line.params[2].split(' '): + self.logger.debug('CAP ACK: {cap}') + self.caps.add(cap) + if cap == 'sasl' and self.sasl: + self.send(b'AUTHENTICATE EXTERNAL') + else: + self.capReqsPending.remove(cap) + elif line.params[1] == 'NAK': + self.logger.warning(f'Failed to activate CAP(s): {line.params[2]}') + for cap in line.params[2].split(' '): + self.capReqsPending.remove(cap) + if len(self.capReqsPending) == 0: + self.send(b'CAP END') elif line.command == 'AUTHENTICATE' and line.params == ['+']: self.send(b'AUTHENTICATE +') elif line.command == '903': # SASL auth successful self.authenticated = True - self.send(b'CAP END') + self.capReqsPending.remove('sasl') + if len(self.capReqsPending) == 0: + self.send(b'CAP END') elif line.command in ('902', '904', '905', '906', '908'): self.logger.error('SASL error, terminating connection') self.transport.close() @@ -402,9 +419,10 @@ class IRCClientProtocol(asyncio.Protocol): if line.command == 'JOIN': # Although servers SHOULD NOT send multiple channels in one message per the modern IRC docs , let's do the safe thing... channels = [line.params[0]] if ',' not in line.params[0] else line.params[0].split(',') + account = f' ({line.params[-2]})' if 'extended-join' in self.caps and line.params[-2] != '*' else '' for channel in channels: # There can't be a mode set yet on the JOIN, so no need to use get_mode_nick (which would complicate the self-join). - yield 'JOIN', channel, f'{line.hostmask.nickname} joins {channel}' + yield 'JOIN', channel, f'{line.hostmask.nickname}{account} joins {channel}' elif line.command in ('PRIVMSG', 'NOTICE'): channel = line.params[0] if channel not in self.server.channels: @@ -415,7 +433,7 @@ class IRCClientProtocol(asyncio.Protocol): reason = f' [{line.params[1]}]' if len(line.params) == 2 else '' for channel in channels: yield 'PART', channel, f'{get_mode_nick(channel)} leaves {channel}' - elif line.command in ('QUIT', 'NICK'): + elif line.command in ('QUIT', 'NICK', 'ACCOUNT'): if line.hostmask.nickname == self.server.nickname: channels = self.channels elif sourceUser is not None: @@ -428,6 +446,8 @@ class IRCClientProtocol(asyncio.Protocol): elif line.command == 'NICK': newMode = self.get_mode_char(self.server.channels[self.server.casefold(channel)].users[self.server.casefold(line.hostmask.nickname)]) message = f'{get_mode_nick(channel)} is now known as {newMode}{line.params[0]}' + elif line.command == 'ACCOUNT': + message = f'{get_mode_nick(channel)} is now authenticated as {line.params[0]}' yield line.command, channel, message elif line.command == 'MODE' and line.params[0][0] in ('#', '&'): yield 'MODE', line.params[0], f'{get_mode_nick(line.params[0])} sets mode: {" ".join(line.params[1:])}'