From 609829bf5534dc8e3ddbfd6e8fd0a3c4ccc0224c Mon Sep 17 00:00:00 2001 From: JustAnotherArchivist Date: Wed, 13 May 2020 02:59:43 +0000 Subject: [PATCH] Track usermask and account for it in the message splitting Since the ircd will prefix each message with the origin usermask when broadcasting to the other users, it will have to split or truncate the message sent by the user. Charybdis and ratbox silently truncate it. --- http2irc.py | 43 ++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 40 insertions(+), 3 deletions(-) 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