Browse Source

Add option to truncate overlong messages instead of splitting them

master
JustAnotherArchivist 4 years ago
parent
commit
98f8821fda
2 changed files with 25 additions and 9 deletions
  1. +2
    -0
      config.example.toml
  2. +23
    -9
      http2irc.py

+ 2
- 0
config.example.toml View File

@@ -26,3 +26,5 @@
#module = #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 are additional arguments to be passed into the module's process function after the request object. Example use: Gitea webhook secret key
#moduleargs = [] #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'

+ 23
- 9
http2irc.py View File

@@ -121,7 +121,7 @@ class Config(dict):
raise InvalidConfig(f'Invalid map key {key!r}') raise InvalidConfig(f'Invalid map key {key!r}')
if not isinstance(map_, collections.abc.Mapping): if not isinstance(map_, collections.abc.Mapping):
raise InvalidConfig(f'Invalid map for {key!r}') 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}') raise InvalidConfig(f'Unknown key(s) found in map {key!r}')


if 'webpath' not in map_: if 'webpath' not in map_:
@@ -158,6 +158,11 @@ class Config(dict):
raise InvalidConfig(f'Invalid module args for {key!r}: not an array') raise InvalidConfig(f'Invalid module args for {key!r}: not an array')
if 'module' not in map_: if 'module' not in map_:
raise InvalidConfig(f'Module args cannot be specified without a module for {key!r}') 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 # 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': {}} 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 map_['module'] = None
if 'moduleargs' not in map_: if 'moduleargs' not in map_:
map_['moduleargs'] = [] map_['moduleargs'] = []
if 'overlongmode' not in map_:
map_['overlongmode'] = 'split'


# Load modules # Load modules
modulePaths = {} # path: str -> (extraargs: int, key: str) modulePaths = {} # path: str -> (extraargs: int, key: str)
@@ -386,7 +393,7 @@ class IRCClientProtocol(asyncio.Protocol):
async def send_messages(self): async def send_messages(self):
while self.connected: while self.connected:
self.logger.debug(f'Trying to get a message') 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}') self.logger.debug(f'Got message: {message!r}')
if message is None: if message is None:
break break
@@ -394,13 +401,17 @@ class IRCClientProtocol(asyncio.Protocol):
messageB = message.encode('utf-8') messageB = message.encode('utf-8')
usermaskPrefixLength = 1 + (len(self.usermask) if self.usermask else 100) + 1 usermaskPrefixLength = 1 + (len(self.usermask) if self.usermask else 100) + 1
if usermaskPrefixLength + len(b'PRIVMSG ' + channelB + b' :' + messageB) > 510: 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' :' prefix = b'PRIVMSG ' + channelB + b' :'
prefixLength = usermaskPrefixLength + len(prefix) # Need to account for the origin prefix included by the ircd when sending to others 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
if overlongmode == 'truncate':
maxMessageLength -= 3 # Make room for an ellipsis at the end
messages = [] messages = []
while message: while message:
if overlongmode == 'truncate' and messages:
break # Only need the first message on truncation
if len(messageB) <= maxMessageLength: if len(messageB) <= maxMessageLength:
messages.append(message) messages.append(message)
break break
@@ -425,8 +436,11 @@ class IRCClientProtocol(asyncio.Protocol):
messages.append(message[:cutoffIndex]) messages.append(message[:cutoffIndex])
message = message[cutoffIndex:] message = message[cutoffIndex:]
messageB = message.encode('utf-8') 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: else:
self.logger.info(f'Sending {message!r} to {channel!r}') self.logger.info(f'Sending {message!r} to {channel!r}')
self.unconfirmedMessages.append((channel, message)) self.unconfirmedMessages.append((channel, message))
@@ -633,7 +647,7 @@ class WebServer:
self._configChanged = asyncio.Event() self._configChanged = asyncio.Event()


def update_config(self, config): 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'] needRebind = self.config['web'] != config['web']
self.config = config self.config = config
if needRebind: if needRebind:
@@ -654,7 +668,7 @@ class WebServer:
async def post(self, request): 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}') self.logger.info(f'Received request {id(request)} from {request.remote!r} for {request.path!r} with body {(await request.read())!r}')
try: try:
channel, auth, module, moduleargs = self._paths[request.path]
channel, auth, module, moduleargs, overlongmode = self._paths[request.path]
except KeyError: except KeyError:
self.logger.info(f'Bad request {id(request)}: no path {request.path!r}') self.logger.info(f'Bad request {id(request)}: no path {request.path!r}')
raise aiohttp.web.HTTPNotFound() raise aiohttp.web.HTTPNotFound()
@@ -679,7 +693,7 @@ class WebServer:
self.logger.debug(f'Processing request {id(request)} using default processor') self.logger.debug(f'Processing request {id(request)} using default processor')
message = await self._default_process(request) message = await self._default_process(request)
self.logger.info(f'Accepted request {id(request)}, putting message {message!r} for {channel} into message queue') 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() raise aiohttp.web.HTTPOk()


async def _default_process(self, request): async def _default_process(self, request):


Loading…
Cancel
Save