@@ -288,6 +288,7 @@ class IRCClientProtocol(asyncio.Protocol):
self.pongReceivedEvent = asyncio.Event()
self.pongReceivedEvent = asyncio.Event()
self.sasl = bool(self.config['irc']['certfile'] and self.config['irc']['certkeyfile'])
self.sasl = bool(self.config['irc']['certfile'] and self.config['irc']['certkeyfile'])
self.authenticated = False
self.authenticated = False
self.usermask = None
@staticmethod
@staticmethod
def nick_command(nick: str):
def nick_command(nick: str):
@@ -298,6 +299,11 @@ class IRCClientProtocol(asyncio.Protocol):
nickb = nick.encode('utf-8')
nickb = nick.encode('utf-8')
return b'USER ' + nickb + b' ' + nickb + b' ' + nickb + b' :' + real.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):
def connection_made(self, transport):
self.logger.info('IRC connected')
self.logger.info('IRC connected')
self.transport = transport
self.transport = transport
@@ -386,11 +392,12 @@ class IRCClientProtocol(asyncio.Protocol):
break
break
channelB = channel.encode('utf-8')
channelB = channel.encode('utf-8')
messageB = message.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')
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.
# 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' :'
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
maxMessageLength = 510 - prefixLength # maximum length of the message part within each line
messages = []
messages = []
while message:
while message:
@@ -463,7 +470,8 @@ class IRCClientProtocol(asyncio.Protocol):
def message_received(self, message):
def message_received(self, message):
self.logger.debug(f'Message received: {message!r}')
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)
# Prefixed message, extract command + parameters (the prefix cannot contain a space)
message = message.split(b' ', 1)[1]
message = message.split(b' ', 1)[1]
@@ -482,6 +490,13 @@ class IRCClientProtocol(asyncio.Protocol):
self.transport.close()
self.transport.close()
elif message == b'AUTHENTICATE +':
elif message == b'AUTHENTICATE +':
self.send(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
elif message.startswith(b'903 '): # SASL auth successful
self.authenticated = True
self.authenticated = True
self.send(b'CAP END')
self.send(b'CAP END')
@@ -527,6 +542,28 @@ class IRCClientProtocol(asyncio.Protocol):
asyncio.create_task(self.send_messages())
asyncio.create_task(self.send_messages())
asyncio.create_task(self.confirm_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):
def connection_lost(self, exc):
self.logger.info('IRC connection lost')
self.logger.info('IRC connection lost')
self.connected = False
self.connected = False