Browse Source

Add a way to pass additional arguments into the module

master
JustAnotherArchivist 4 years ago
parent
commit
d00c59e6bc
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

@@ -24,3 +24,5 @@
#auth = false #auth = false
# module is the path to a Python source file that handles the message transformation. It must contain a coroutine function 'process' that takes one argument, the aiohttp.web.Request object, and returns the message string to be sent to IRC, which must not contain any linebreaks (CR or LF). It may raise an aiohttp.web.HTTPException to stop processing; any other exception will also cause the processing to be stopped and a '400 Bad Request' response to be returned to the client. The empty default value (None) causes the default processor to be used, which expects a straight message in the request body (with an optional trailing CR+LF or LF) and performs no transformations other than encoding the message as UTF-8. # module is the path to a Python source file that handles the message transformation. It must contain a coroutine function 'process' that takes one argument, the aiohttp.web.Request object, and returns the message string to be sent to IRC, which must not contain any linebreaks (CR or LF). It may raise an aiohttp.web.HTTPException to stop processing; any other exception will also cause the processing to be stopped and a '400 Bad Request' response to be returned to the client. The empty default value (None) causes the default processor to be used, which expects a straight message in the request body (with an optional trailing CR+LF or LF) and performs no transformations other than encoding the message as UTF-8.
#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 = []

+ 23
- 9
http2irc.py View File

@@ -116,11 +116,16 @@ 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') for x in map_):
if any(x not in ('webpath', 'ircchannel', 'auth', 'module', 'moduleargs') 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}')
#TODO: Check values #TODO: Check values
if 'module' in map_ and not os.path.isfile(map_['module']): if 'module' in map_ and not os.path.isfile(map_['module']):
raise InvalidConfig(f'Module {map_["module"]!r} in map {key!r} is not a file') raise InvalidConfig(f'Module {map_["module"]!r} in map {key!r} is not a file')
if 'moduleargs' in map_:
if not isinstance(map_['moduleargs'], list):
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}')


# Default values # Default values
finalObj = {'logging': {'level': 'INFO', 'format': '{asctime} {levelname} {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} {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': {}}
@@ -135,11 +140,19 @@ class Config(dict):
map_['auth'] = False map_['auth'] = False
if 'module' not in map_: if 'module' not in map_:
map_['module'] = None map_['module'] = None
if 'moduleargs' not in map_:
map_['moduleargs'] = []


# Load modules # Load modules
modulePaths = {map_['module'] for map_ in obj['maps'].values() if 'module' in map_ and map_['module'] is not None}
modulePaths = {} # path: str -> (extraargs: int, key: str)
for key, map_ in obj['maps'].items():
if map_['module'] is not None:
if map_['module'] not in modulePaths:
modulePaths[map_['module']] = (len(map_['moduleargs']), key)
elif modulePaths[map_['module']][0] != len(map_['moduleargs']):
raise InvalidConfig(f'Module {map_["module"]!r} process function extra argument inconsistency between {key!r} and {modulePaths[map_["module"]][1]!r}')
modules = {} # path: str -> module: module modules = {} # path: str -> module: module
for i, path in enumerate(modulePaths):
for i, (path, (extraargs, _)) in enumerate(modulePaths.items()):
try: try:
# Build a name that is virtually guaranteed to be unique across a process. # Build a name that is virtually guaranteed to be unique across a process.
# Although importlib does not seem to perform any caching as of CPython 3.8, this is not guaranteed by spec. # Although importlib does not seem to perform any caching as of CPython 3.8, this is not guaranteed by spec.
@@ -152,8 +165,9 @@ class Config(dict):
raise InvalidConfig(f'Module {path!r} does not have a process function') raise InvalidConfig(f'Module {path!r} does not have a process function')
if not inspect.iscoroutinefunction(module.process): if not inspect.iscoroutinefunction(module.process):
raise InvalidConfig(f'Module {path!r} process attribute is not a coroutine function') raise InvalidConfig(f'Module {path!r} process attribute is not a coroutine function')
if len(inspect.signature(module.process).parameters) != 1:
raise InvalidConfig(f'Module {path!r} process function does not take exactly 1 parameter')
nargs = len(inspect.signature(module.process).parameters)
if nargs != 1 + extraargs:
raise InvalidConfig(f'Module {path!r} process function takes {nargs} parameter{"s" if nargs > 1 else ""}, not {1 + extraargs}')
modules[path] = module modules[path] = module


# Replace module value in maps # Replace module value in maps
@@ -401,7 +415,7 @@ class WebServer:
self.messageQueue = messageQueue self.messageQueue = messageQueue
self.config = config self.config = config


self._paths = {} # '/path' => ('#channel', auth, module) where auth is either False (no authentication) or the HTTP header value for basic auth
self._paths = {} # '/path' => ('#channel', auth, module, moduleargs) where auth is either False (no authentication) or the HTTP header value for basic auth


self._app = aiohttp.web.Application() self._app = aiohttp.web.Application()
self._app.add_routes([aiohttp.web.post('/{path:.+}', self.post)]) self._app.add_routes([aiohttp.web.post('/{path:.+}', self.post)])
@@ -410,7 +424,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']) 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']) 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:
@@ -431,7 +445,7 @@ class WebServer:
async def post(self, request): async def post(self, request):
logging.info(f'Received request for {request.path!r}') logging.info(f'Received request for {request.path!r}')
try: try:
channel, auth, module = self._paths[request.path]
channel, auth, module, moduleargs = self._paths[request.path]
except KeyError: except KeyError:
logging.info(f'Bad request: no path {request.path!r}') logging.info(f'Bad request: no path {request.path!r}')
raise aiohttp.web.HTTPNotFound() raise aiohttp.web.HTTPNotFound()
@@ -442,7 +456,7 @@ class WebServer:
raise aiohttp.web.HTTPForbidden() raise aiohttp.web.HTTPForbidden()
if module is not None: if module is not None:
try: try:
message = await module.process(request)
message = await module.process(request, *moduleargs)
except aiohttp.web.HTTPException as e: except aiohttp.web.HTTPException as e:
raise e raise e
except Exception as e: except Exception as e:


Loading…
Cancel
Save