diff --git a/http2irc.py b/http2irc.py index 14822eb..5a98c25 100644 --- a/http2irc.py +++ b/http2irc.py @@ -288,6 +288,7 @@ class IRCClientProtocol(asyncio.Protocol): self.pongReceivedEvent = asyncio.Event() self.sasl = bool(self.config['irc']['certfile'] and self.config['irc']['certkeyfile']) self.authenticated = False + self.usermask = None @staticmethod def nick_command(nick: str): @@ -298,6 +299,11 @@ class IRCClientProtocol(asyncio.Protocol): nickb = nick.encode('utf-8') return b'USER ' + nickb + b' ' + nickb + b' ' + nickb + b' :' + real.encode('utf-8') + def _maybe_set_usermask(self, usermask): + if b'@' in usermask and b'!' in usermask.split(b'@')[0] and all(x not in usermask for x in (b' ', b'*', b'#', b'&')): + self.usermask = usermask + self.logger.debug(f'Usermask is now {usermask!r}') + def connection_made(self, transport): self.logger.info('IRC connected') self.transport = transport @@ -386,11 +392,12 @@ class IRCClientProtocol(asyncio.Protocol): break channelB = channel.encode('utf-8') messageB = message.encode('utf-8') - if len(b'PRIVMSG ' + channelB + b' :' + messageB) > 510: + usermaskPrefixLength = 1 + (len(self.usermask) if self.usermask else 100) + 1 + if usermaskPrefixLength + len(b'PRIVMSG ' + channelB + b' :' + messageB) > 510: self.logger.debug(f'Splitting up into smaller messages') # Message too long, need to split. First try to split on spaces, then on codepoints. Ideally, would use graphemes between, but that's too complicated. prefix = b'PRIVMSG ' + channelB + b' :' - prefixLength = len(prefix) + prefixLength = usermaskPrefixLength + len(prefix) # Need to account for the origin prefix included by the ircd when sending to others maxMessageLength = 510 - prefixLength # maximum length of the message part within each line messages = [] while message: @@ -463,7 +470,8 @@ class IRCClientProtocol(asyncio.Protocol): def message_received(self, message): self.logger.debug(f'Message received: {message!r}') - if message.startswith(b':'): + rawMessage = message + if message.startswith(b':') and b' ' in message: # Prefixed message, extract command + parameters (the prefix cannot contain a space) message = message.split(b' ', 1)[1] @@ -482,6 +490,13 @@ class IRCClientProtocol(asyncio.Protocol): self.transport.close() elif message == b'AUTHENTICATE +': self.send(b'AUTHENTICATE +') + elif message.startswith(b'900 '): # "You are now logged in", includes the usermask + words = message.split(b' ') + if len(words) >= 3 and b'!' in words[2] and b'@' in words[2]: + if b'!~' not in words[2]: + # At least Charybdis seems to always return the user without a tilde, even if identd failed. Assume no identd and account for that extra tilde. + words[2] = words[2].replace(b'!', b'!~', 1) + self._maybe_set_usermask(words[2]) elif message.startswith(b'903 '): # SASL auth successful self.authenticated = True self.send(b'CAP END') @@ -527,6 +542,28 @@ class IRCClientProtocol(asyncio.Protocol): asyncio.create_task(self.send_messages()) asyncio.create_task(self.confirm_messages()) + # JOIN success + elif message.startswith(b'JOIN ') and not self.usermask: + # If this is my own join message, it should contain the usermask in the prefix + if rawMessage.startswith(b':' + self.config['irc']['nick'].encode('utf-8') + b'!') and b' ' in rawMessage: + usermask = rawMessage.split(b' ', 1)[0][1:] + self._maybe_set_usermask(usermask) + + # Services host change + elif message.startswith(b'396 '): + words = message.split(b' ') + if len(words) >= 3: + # Sanity check inspired by irssi src/irc/core/irc-servers.c + if not any(x in words[2] for x in (b'*', b'?', b'!', b'#', b'&', b' ')) and not any(words[2].startswith(x) for x in (b'@', b':', b'-')) and words[2][-1:] != b'-': + if b'@' in words[2]: # user@host + self._maybe_set_usermask(self.config['irc']['nick'].encode('utf-8') + b'!' + words[2]) + else: # host (get user from previous mask or settings) + if self.usermask: + user = self.usermask.split(b'@')[0].split(b'!')[1] + else: + user = b'~' + self.config['irc']['nick'].encode('utf-8') + self._maybe_set_usermask(self.config['irc']['nick'].encode('utf-8') + b'!' + user + b'@' + words[2]) + def connection_lost(self, exc): self.logger.info('IRC connection lost') self.connected = False