if 14 + len(map_['ircchannel']) > 510: # 14 = prefix 'PRIVMSG ' + suffix ' :' + at least one UTF-8 character; implicitly also covers the shorter JOIN/PART messages
raise InvalidConfig(f'Invalid map {key!r} IRC channel: too long')
if 'auth' in map_:
if map_['auth'] is not False and not isinstance(map_['auth'], str):
@@ -282,15 +289,56 @@ class IRCClientProtocol(asyncio.Protocol):
self.sasl = bool(self.config['irc']['certfile'] and self.config['irc']['certkeyfile'])
self.authenticated = False
@staticmethod
def nick_command(nick: str):
return b'NICK ' + nick.encode('utf-8')
@staticmethod
def user_command(nick: str, real: str):
nickb = nick.encode('utf-8')
return b'USER ' + nickb + b' ' + nickb + b' ' + nickb + b' :' + real.encode('utf-8')
'''Split a JOIN or PART into multiple messages as necessary'''
# command: b'JOIN' or b'PART'; channels: set[str]
channels = [x.encode('utf-8') for x in channels]
if len(command) + sum(1 + len(x) for x in channels) <= 510: # Total length = command + (separator + channel name for each channel, where the separator is a space for the first and then a comma)
# Everything fits into one command.
self.send(command + b' ' + b','.join(channels))
return
# List too long, need to split.
limit = 510 - len(command)
lengths = [1 + len(x) for x in channels] # separator + channel name
chanLengthAcceptable = [l <= limit for l in lengths]
if not all(chanLengthAcceptable):
# There are channel names that are too long to even fit into one message on their own; filter them out and warn about them.
# This should never happen since the config reader would already filter it out.
tooLongChannels = [x for x, a in zip(channels, chanLengthAcceptable) if not a]
channels = [x for x, a in zip(channels, chanLengthAcceptable) if a]
lengths = [l for l, a in zip(lengths, chanLengthAcceptable) if a]
for channel in tooLongChannels:
self.logger.warning(f'Cannot {command} {channel}: name too long')
runningLengths = list(itertools.accumulate(lengths)) # entry N = length of all entries up to and including channel N, including separators
offset = 0
while channels:
i = next((x[0] for x in enumerate(runningLengths) if x[1] - offset > limit), -1)
if i == -1: # Last batch
i = len(channels)
self.send(command + b' ' + b','.join(channels[:i]))
offset = runningLengths[i-1]
channels = channels[i:]
runningLengths = runningLengths[i:]
def update_channels(self, channels: set):
channelsToPart = self.channels - channels
@@ -299,10 +347,9 @@ class IRCClientProtocol(asyncio.Protocol):