From 98f8821fda0488685d6e36d1b70510444487abec Mon Sep 17 00:00:00 2001 From: JustAnotherArchivist Date: Wed, 13 May 2020 15:00:54 +0000 Subject: [PATCH] Add option to truncate overlong messages instead of splitting them --- config.example.toml | 2 ++ http2irc.py | 32 +++++++++++++++++++++++--------- 2 files changed, 25 insertions(+), 9 deletions(-) diff --git a/config.example.toml b/config.example.toml index 6bfa3a1..9d6c483 100644 --- a/config.example.toml +++ b/config.example.toml @@ -26,3 +26,5 @@ #module = # moduleargs are additional arguments to be passed into the module's process function after the request object. Example use: Gitea webhook secret key #moduleargs = [] + # overlongmode determines what happens to messages that are too long to be sent to the channel. The value may be 'split' (split into multiple messages on spaces or codepoints) or 'truncate' (truncate everything exceeding the limit). + #overlongmode = 'split' diff --git a/http2irc.py b/http2irc.py index 60c0a5c..16e02e3 100644 --- a/http2irc.py +++ b/http2irc.py @@ -121,7 +121,7 @@ class Config(dict): raise InvalidConfig(f'Invalid map key {key!r}') if not isinstance(map_, collections.abc.Mapping): raise InvalidConfig(f'Invalid map for {key!r}') - if any(x not in ('webpath', 'ircchannel', 'auth', 'module', 'moduleargs') for x in map_): + if any(x not in ('webpath', 'ircchannel', 'auth', 'module', 'moduleargs', 'overlongmode') for x in map_): raise InvalidConfig(f'Unknown key(s) found in map {key!r}') if 'webpath' not in map_: @@ -158,6 +158,11 @@ class Config(dict): raise InvalidConfig(f'Invalid module args for {key!r}: not an array') if 'module' not in map_: raise InvalidConfig(f'Module args cannot be specified without a module for {key!r}') + if 'overlongmode' in map_: + if not isinstance(map_['overlongmode'], str): + raise InvalidConfig(f'Invalid map {key!r} overlongmode: not a string') + if map_['overlongmode'] not in ('split', 'truncate'): + raise InvalidConfig(f'Invalid map {key!r} overlongmode: unsupported value') # Default values finalObj = {'logging': {'level': 'INFO', 'format': '{asctime} {levelname} {name} {message}'}, 'irc': {'host': 'irc.hackint.org', 'port': 6697, 'ssl': 'yes', 'nick': 'h2ibot', 'real': 'I am an http2irc bot.', 'certfile': None, 'certkeyfile': None}, 'web': {'host': '127.0.0.1', 'port': 8080}, 'maps': {}} @@ -172,6 +177,8 @@ class Config(dict): map_['module'] = None if 'moduleargs' not in map_: map_['moduleargs'] = [] + if 'overlongmode' not in map_: + map_['overlongmode'] = 'split' # Load modules modulePaths = {} # path: str -> (extraargs: int, key: str) @@ -386,7 +393,7 @@ class IRCClientProtocol(asyncio.Protocol): async def send_messages(self): while self.connected: self.logger.debug(f'Trying to get a message') - channel, message = await self._get_message() + channel, message, overlongmode = await self._get_message() self.logger.debug(f'Got message: {message!r}') if message is None: break @@ -394,13 +401,17 @@ class IRCClientProtocol(asyncio.Protocol): messageB = message.encode('utf-8') 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. + # Message too long, need to split or truncate. First try to split on spaces, then on codepoints. Ideally, would use graphemes between, but that's too complicated. + self.logger.debug(f'Message too long, overlongmode = {overlongmode}') prefix = b'PRIVMSG ' + channelB + b' :' 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 + if overlongmode == 'truncate': + maxMessageLength -= 3 # Make room for an ellipsis at the end messages = [] while message: + if overlongmode == 'truncate' and messages: + break # Only need the first message on truncation if len(messageB) <= maxMessageLength: messages.append(message) break @@ -425,8 +436,11 @@ class IRCClientProtocol(asyncio.Protocol): messages.append(message[:cutoffIndex]) message = message[cutoffIndex:] messageB = message.encode('utf-8') - for msg in reversed(messages): - self.messageQueue.putleft_nowait((channel, msg)) + if overlongmode == 'split': + for msg in reversed(messages): + self.messageQueue.putleft_nowait((channel, msg, overlongmode)) + elif overlongmode == 'truncate': + self.messageQueue.putleft_nowait((channel, messages[0] + '…', overlongmode)) else: self.logger.info(f'Sending {message!r} to {channel!r}') self.unconfirmedMessages.append((channel, message)) @@ -633,7 +647,7 @@ class WebServer: self._configChanged = asyncio.Event() def update_config(self, config): - self._paths = {map_['webpath']: (map_['ircchannel'], f'Basic {base64.b64encode(map_["auth"].encode("utf-8")).decode("utf-8")}' if map_['auth'] else False, map_['module'], map_['moduleargs']) for map_ in config['maps'].values()} + self._paths = {map_['webpath']: (map_['ircchannel'], f'Basic {base64.b64encode(map_["auth"].encode("utf-8")).decode("utf-8")}' if map_['auth'] else False, map_['module'], map_['moduleargs'], map_['overlongmode']) for map_ in config['maps'].values()} needRebind = self.config['web'] != config['web'] self.config = config if needRebind: @@ -654,7 +668,7 @@ class WebServer: async def post(self, request): self.logger.info(f'Received request {id(request)} from {request.remote!r} for {request.path!r} with body {(await request.read())!r}') try: - channel, auth, module, moduleargs = self._paths[request.path] + channel, auth, module, moduleargs, overlongmode = self._paths[request.path] except KeyError: self.logger.info(f'Bad request {id(request)}: no path {request.path!r}') raise aiohttp.web.HTTPNotFound() @@ -679,7 +693,7 @@ class WebServer: self.logger.debug(f'Processing request {id(request)} using default processor') message = await self._default_process(request) self.logger.info(f'Accepted request {id(request)}, putting message {message!r} for {channel} into message queue') - self.messageQueue.put_nowait((channel, message)) + self.messageQueue.put_nowait((channel, message, overlongmode)) raise aiohttp.web.HTTPOk() async def _default_process(self, request):