commit
6943432367
11 changed files with 351 additions and 226 deletions
33
CHANGELOG.md
Normal file
33
CHANGELOG.md
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
# Changelog
|
||||||
|
All notable changes to this project will be documented in this file.
|
||||||
|
|
||||||
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||||
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
|
## [Unreleased]
|
||||||
|
|
||||||
|
- Simplify code
|
||||||
|
in [#1](https://github.com/nim65s/matrix-webhook/pull/1)
|
||||||
|
by [@homeworkprod](https://github.com/homeworkprod)
|
||||||
|
- Update aiohttp use and docs
|
||||||
|
in [#5](https://github.com/nim65s/matrix-webhook/pull/5)
|
||||||
|
by [@svenseeberg](https://github.com/svenseeberg)
|
||||||
|
- Setup Tests, Coverage & CI ; update tooling
|
||||||
|
in [#7](https://github.com/nim65s/matrix-webhook/pull/7)
|
||||||
|
by [@nim65s](https://github.com/nim65s)
|
||||||
|
- Setup argparse & logging
|
||||||
|
in [#8](https://github.com/nim65s/matrix-webhook/pull/8)
|
||||||
|
by [@nim65s](https://github.com/nim65s)
|
||||||
|
- Setup packaging
|
||||||
|
in [#9](https://github.com/nim65s/matrix-webhook/pull/9)
|
||||||
|
by [@nim65s](https://github.com/nim65s)
|
||||||
|
|
||||||
|
## [1.0.0] - 2020-03-14
|
||||||
|
- Update to matrix-nio & aiohttp & markdown
|
||||||
|
|
||||||
|
## [1.0.0] - 2020-02-14
|
||||||
|
- First release with matrix-client & http.server
|
||||||
|
|
||||||
|
[Unreleased]: https://github.com/nim65s/matrix-webhook/compare/v2.0.0...devel
|
||||||
|
[2.0.0]: https://github.com/nim65s/matrix-webhook/compare/v1.0.0...v2.0.0
|
||||||
|
[1.0.0]: https://github.com/nim65s/matrix-webhook/releases/tag/v1.0.0
|
|
@ -4,6 +4,6 @@ EXPOSE 4785
|
||||||
|
|
||||||
RUN pip install --no-cache-dir markdown matrix-nio
|
RUN pip install --no-cache-dir markdown matrix-nio
|
||||||
|
|
||||||
ADD matrix_webhook.py /
|
ADD matrix_webhook matrix_webhook
|
||||||
|
|
||||||
CMD /matrix_webhook.py
|
ENTRYPOINT ["python", "-m", "matrix_webhook"]
|
||||||
|
|
2
LICENSE
2
LICENSE
|
@ -1,4 +1,4 @@
|
||||||
Copyright (c) 2019-2020 tetaneutral.net All rights reserved.
|
Copyright (c) 2019-2021 tetaneutral.net All rights reserved.
|
||||||
|
|
||||||
BSD 2 Clause License
|
BSD 2 Clause License
|
||||||
|
|
||||||
|
|
62
README.md
62
README.md
|
@ -7,27 +7,59 @@
|
||||||
|
|
||||||
Post a message to a matrix room with a simple HTTP POST
|
Post a message to a matrix room with a simple HTTP POST
|
||||||
|
|
||||||
## Configuration
|
## Install
|
||||||
|
|
||||||
Create a matrix user for the bot, make it join the rooms you want it to talk into, and then set the following
|
```
|
||||||
environment variables:
|
python3 -m pip install matrix-webhook
|
||||||
|
# OR
|
||||||
|
docker pull nim65s/matrix-webhook
|
||||||
|
```
|
||||||
|
|
||||||
|
## Start
|
||||||
|
|
||||||
|
Create a matrix user for the bot, make it join the rooms you want it to talk into, and launch it with the following
|
||||||
|
arguments or environment variables:
|
||||||
|
|
||||||
|
```
|
||||||
|
python -m matrix_webhook -h
|
||||||
|
# OR
|
||||||
|
docker run --rm -it nim65s/matrix-webhook -h
|
||||||
|
```
|
||||||
|
|
||||||
|
```
|
||||||
|
usage: python -m matrix_webhook [-h] [-H HOST] [-P PORT] [-u MATRIX_URL] -i MATRIX_ID -p MATRIX_PW -k API_KEY [-v]
|
||||||
|
|
||||||
|
Configuration for Matrix Webhook.
|
||||||
|
|
||||||
|
|
||||||
|
optional arguments:
|
||||||
|
-h, --help show this help message and exit
|
||||||
|
-H HOST, --host HOST host to listen to. Default: `''`. Environment variable: `HOST`
|
||||||
|
-P PORT, --port PORT port to listed to. Default: 4785. Environment variable: `PORT`
|
||||||
|
-u MATRIX_URL, --matrix-url MATRIX_URL
|
||||||
|
matrix homeserver url. Default: `https://matrix.org`. Environment variable: `MATRIX_URL`
|
||||||
|
-i MATRIX_ID, --matrix-id MATRIX_ID
|
||||||
|
matrix user-id. Required. Environment variable: `MATRIX_ID`
|
||||||
|
-p MATRIX_PW, --matrix-pw MATRIX_PW
|
||||||
|
matrix password. Required. Environment variable: `MATRIX_PW`
|
||||||
|
-k API_KEY, --api-key API_KEY
|
||||||
|
shared secret to use this service. Required. Environment variable: `API_KEY`
|
||||||
|
-v, --verbose increment verbosity level
|
||||||
|
```
|
||||||
|
|
||||||
- `MATRIX_URL`: the url of the matrix homeserver
|
|
||||||
- `MATRIX_ID`: the user id of the bot on this server
|
|
||||||
- `MATRIX_PW`: the password for this user
|
|
||||||
- `API_KEY`: a secret to share with the users of the service
|
|
||||||
- `HOST`: HOST to listen on, all interfaces if `''` (default).
|
|
||||||
- `PORT`: PORT to listed on, default to 4785.
|
|
||||||
|
|
||||||
## Dev
|
## Dev
|
||||||
|
|
||||||
```
|
```
|
||||||
pip3 install --user markdown matrix-nio
|
poetry install
|
||||||
./matrix_webhook.py
|
# or python3 -m pip install --user markdown matrix-nio
|
||||||
|
python3 -m matrix_webhook
|
||||||
```
|
```
|
||||||
|
|
||||||
## Prod
|
## Prod
|
||||||
|
|
||||||
|
A `docker-compose.yml` is provided:
|
||||||
|
|
||||||
- Use [Traefik](https://traefik.io/) on the `web` docker network, eg. with
|
- Use [Traefik](https://traefik.io/) on the `web` docker network, eg. with
|
||||||
[proxyta.net](https://framagit.org/oxyta.net/proxyta.net)
|
[proxyta.net](https://framagit.org/oxyta.net/proxyta.net)
|
||||||
- Put the configuration into a `.env` file
|
- Put the configuration into a `.env` file
|
||||||
|
@ -47,4 +79,10 @@ curl -d '{"text":"new contrib from toto: [44](http://radio.localhost/map/#44)",
|
||||||
|
|
||||||
## Test room
|
## Test room
|
||||||
|
|
||||||
[#matrix-webhook:tetaneutral.net](https://matrix.to/#/!DPrUlnwOhBEfYwsDLh:matrix.org?via=laas.fr&via=tetaneutral.net&via=aen.im)
|
#matrix-webhook:tetaneutral.net](https://matrix.to/#/!DPrUlnwOhBEfYwsDLh:matrix.org?via=laas.fr&via=tetaneutral.net&via=aen.im)
|
||||||
|
|
||||||
|
## Unit tests
|
||||||
|
|
||||||
|
```
|
||||||
|
docker-compose -f test.yml up --exit-code-from tests --force-recreate --build
|
||||||
|
```
|
||||||
|
|
|
@ -1,187 +0,0 @@
|
||||||
#!/usr/bin/env python3
|
|
||||||
"""
|
|
||||||
Matrix Webhook.
|
|
||||||
|
|
||||||
Post a message to a matrix room with a simple HTTP POST
|
|
||||||
"""
|
|
||||||
|
|
||||||
import argparse
|
|
||||||
import asyncio
|
|
||||||
import json
|
|
||||||
import logging
|
|
||||||
import os
|
|
||||||
from http import HTTPStatus
|
|
||||||
from signal import SIGINT, SIGTERM
|
|
||||||
|
|
||||||
from aiohttp import web
|
|
||||||
from markdown import markdown
|
|
||||||
from nio import AsyncClient
|
|
||||||
from nio.exceptions import LocalProtocolError
|
|
||||||
|
|
||||||
parser = argparse.ArgumentParser(description=__doc__)
|
|
||||||
parser.add_argument(
|
|
||||||
"-H",
|
|
||||||
"--host",
|
|
||||||
default=os.environ.get("HOST", ""),
|
|
||||||
help="host to listen to. Default: `''`. Environment variable: `HOST`",
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
|
||||||
"-P",
|
|
||||||
"--port",
|
|
||||||
type=int,
|
|
||||||
default=os.environ.get("PORT", 4785),
|
|
||||||
help="port to listed to. Default: 4785. Environment variable: `PORT`",
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
|
||||||
"-u",
|
|
||||||
"--matrix-url",
|
|
||||||
default=os.environ.get("MATRIX_URL", "https://matrix.org"),
|
|
||||||
help="matrix homeserver url. Default: `https://matrix.org`. Environment variable: `MATRIX_URL`",
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
|
||||||
"-i",
|
|
||||||
"--matrix-id",
|
|
||||||
help="matrix user-id. Required. Environment variable: `MATRIX_ID`",
|
|
||||||
**(
|
|
||||||
{"default": os.environ["MATRIX_ID"]}
|
|
||||||
if "MATRIX_ID" in os.environ
|
|
||||||
else {"required": True}
|
|
||||||
),
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
|
||||||
"-p",
|
|
||||||
"--matrix-pw",
|
|
||||||
help="matrix password. Required. Environment variable: `MATRIX_PW`",
|
|
||||||
**(
|
|
||||||
{"default": os.environ["MATRIX_PW"]}
|
|
||||||
if "MATRIX_PW" in os.environ
|
|
||||||
else {"required": True}
|
|
||||||
),
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
|
||||||
"-k",
|
|
||||||
"--api-key",
|
|
||||||
help="shared secret to use this service. Required. Environment variable: `API_KEY`",
|
|
||||||
**(
|
|
||||||
{"default": os.environ["API_KEY"]}
|
|
||||||
if "API_KEY" in os.environ
|
|
||||||
else {"required": True}
|
|
||||||
),
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
|
||||||
"-v", "--verbose", action="count", default=0, help="increment verbosity level"
|
|
||||||
)
|
|
||||||
|
|
||||||
args = parser.parse_args()
|
|
||||||
logging.basicConfig(level=50 - 10 * args.verbose)
|
|
||||||
|
|
||||||
SERVER_ADDRESS = (args.host, args.port)
|
|
||||||
MATRIX_URL = args.matrix_url
|
|
||||||
MATRIX_ID = args.matrix_id
|
|
||||||
MATRIX_PW = args.matrix_pw
|
|
||||||
API_KEY = args.api_key
|
|
||||||
CLIENT = AsyncClient(args.matrix_url, args.matrix_id)
|
|
||||||
|
|
||||||
|
|
||||||
async def handler(request):
|
|
||||||
"""
|
|
||||||
Coroutine given to the server, st. it knows what to do with an HTTP request.
|
|
||||||
|
|
||||||
This one handles a POST, checks its content, and forwards it to the matrix room.
|
|
||||||
"""
|
|
||||||
logging.debug(f"Handling {request=}")
|
|
||||||
data = await request.read()
|
|
||||||
|
|
||||||
try:
|
|
||||||
data = json.loads(data.decode())
|
|
||||||
except json.decoder.JSONDecodeError:
|
|
||||||
return create_json_response(HTTPStatus.BAD_REQUEST, "Invalid JSON")
|
|
||||||
|
|
||||||
if not all(key in data for key in ["text", "key"]):
|
|
||||||
return create_json_response(
|
|
||||||
HTTPStatus.BAD_REQUEST, "Missing text and/or API key property"
|
|
||||||
)
|
|
||||||
|
|
||||||
if data["key"] != API_KEY:
|
|
||||||
return create_json_response(HTTPStatus.UNAUTHORIZED, "Invalid API key")
|
|
||||||
|
|
||||||
room_id = request.path[1:]
|
|
||||||
content = {
|
|
||||||
"msgtype": "m.text",
|
|
||||||
"body": data["text"],
|
|
||||||
"format": "org.matrix.custom.html",
|
|
||||||
"formatted_body": markdown(str(data["text"]), extensions=["extra"]),
|
|
||||||
}
|
|
||||||
try:
|
|
||||||
await send_room_message(room_id, content)
|
|
||||||
except LocalProtocolError as e: # Connection lost, try another login
|
|
||||||
logging.error(f"Send error: {e}")
|
|
||||||
logging.warning("Reconnecting and trying again")
|
|
||||||
await CLIENT.login(MATRIX_PW)
|
|
||||||
await send_room_message(room_id, content)
|
|
||||||
|
|
||||||
return create_json_response(HTTPStatus.OK, "OK")
|
|
||||||
|
|
||||||
|
|
||||||
def create_json_response(status, ret):
|
|
||||||
"""Create a JSON response."""
|
|
||||||
logging.debug(f"Creating json response: {status=}, {ret=}")
|
|
||||||
response_data = {"status": status, "ret": ret}
|
|
||||||
return web.json_response(response_data, status=status)
|
|
||||||
|
|
||||||
|
|
||||||
async def send_room_message(room_id, content):
|
|
||||||
"""Send a message to a room."""
|
|
||||||
logging.debug(f"Sending room message in {room_id=}: {content=}")
|
|
||||||
return await CLIENT.room_send(
|
|
||||||
room_id=room_id, message_type="m.room.message", content=content
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
async def main(event):
|
|
||||||
"""
|
|
||||||
Launch main coroutine.
|
|
||||||
|
|
||||||
matrix client login & start web server
|
|
||||||
"""
|
|
||||||
logging.info(f"Log in {MATRIX_ID=} on {MATRIX_URL=}")
|
|
||||||
await CLIENT.login(MATRIX_PW)
|
|
||||||
|
|
||||||
server = web.Server(handler)
|
|
||||||
runner = web.ServerRunner(server)
|
|
||||||
await runner.setup()
|
|
||||||
logging.info(f"Binding on {SERVER_ADDRESS=}")
|
|
||||||
site = web.TCPSite(runner, *SERVER_ADDRESS)
|
|
||||||
await site.start()
|
|
||||||
|
|
||||||
# Run until we get a shutdown request
|
|
||||||
await event.wait()
|
|
||||||
|
|
||||||
# Cleanup
|
|
||||||
await runner.cleanup()
|
|
||||||
await CLIENT.close()
|
|
||||||
|
|
||||||
|
|
||||||
def terminate(event, signal):
|
|
||||||
"""Close handling stuff."""
|
|
||||||
event.set()
|
|
||||||
asyncio.get_event_loop().remove_signal_handler(signal)
|
|
||||||
|
|
||||||
|
|
||||||
def run():
|
|
||||||
"""Launch everything."""
|
|
||||||
logging.info("Matrix Webhook starting...")
|
|
||||||
loop = asyncio.get_event_loop()
|
|
||||||
event = asyncio.Event()
|
|
||||||
|
|
||||||
for sig in (SIGINT, SIGTERM):
|
|
||||||
loop.add_signal_handler(sig, terminate, event, sig)
|
|
||||||
|
|
||||||
loop.run_until_complete(main(event))
|
|
||||||
|
|
||||||
logging.info("Matrix Webhook closing...")
|
|
||||||
loop.close()
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
run()
|
|
143
matrix_webhook/__main__.py
Normal file
143
matrix_webhook/__main__.py
Normal file
|
@ -0,0 +1,143 @@
|
||||||
|
"""
|
||||||
|
Matrix Webhook.
|
||||||
|
|
||||||
|
Post a message to a matrix room with a simple HTTP POST
|
||||||
|
"""
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
from http import HTTPStatus
|
||||||
|
from signal import SIGINT, SIGTERM
|
||||||
|
|
||||||
|
from aiohttp import web
|
||||||
|
from markdown import markdown
|
||||||
|
from nio import AsyncClient
|
||||||
|
from nio.exceptions import LocalProtocolError
|
||||||
|
from nio.responses import RoomSendError
|
||||||
|
|
||||||
|
from . import conf
|
||||||
|
|
||||||
|
ERROR_MAP = {"M_FORBIDDEN": HTTPStatus.FORBIDDEN}
|
||||||
|
|
||||||
|
CLIENT = AsyncClient(conf.MATRIX_URL, conf.MATRIX_ID)
|
||||||
|
LOGGER = logging.getLogger("matrix-webhook")
|
||||||
|
|
||||||
|
|
||||||
|
async def handler(request):
|
||||||
|
"""
|
||||||
|
Coroutine given to the server, st. it knows what to do with an HTTP request.
|
||||||
|
|
||||||
|
This one handles a POST, checks its content, and forwards it to the matrix room.
|
||||||
|
"""
|
||||||
|
LOGGER.debug(f"Handling {request=}")
|
||||||
|
data = await request.read()
|
||||||
|
|
||||||
|
try:
|
||||||
|
data = json.loads(data.decode())
|
||||||
|
except json.decoder.JSONDecodeError:
|
||||||
|
return create_json_response(HTTPStatus.BAD_REQUEST, "Invalid JSON")
|
||||||
|
|
||||||
|
if not all(key in data for key in ["text", "key"]):
|
||||||
|
return create_json_response(
|
||||||
|
HTTPStatus.BAD_REQUEST, "Missing text and/or API key property"
|
||||||
|
)
|
||||||
|
|
||||||
|
if data["key"] != conf.API_KEY:
|
||||||
|
return create_json_response(HTTPStatus.UNAUTHORIZED, "Invalid API key")
|
||||||
|
|
||||||
|
room_id = request.path[1:]
|
||||||
|
content = {
|
||||||
|
"msgtype": "m.text",
|
||||||
|
"body": data["text"],
|
||||||
|
"format": "org.matrix.custom.html",
|
||||||
|
"formatted_body": markdown(str(data["text"]), extensions=["extra"]),
|
||||||
|
}
|
||||||
|
for _ in range(10):
|
||||||
|
try:
|
||||||
|
resp = await send_room_message(room_id, content)
|
||||||
|
if isinstance(resp, RoomSendError):
|
||||||
|
if resp.status_code == "M_UNKNOWN_TOKEN":
|
||||||
|
LOGGER.warning("Reconnecting")
|
||||||
|
await CLIENT.login(conf.MATRIX_PW)
|
||||||
|
else:
|
||||||
|
return create_json_response(
|
||||||
|
ERROR_MAP[resp.status_code], resp.message
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
break
|
||||||
|
except LocalProtocolError as e:
|
||||||
|
LOGGER.error(f"Send error: {e}")
|
||||||
|
LOGGER.warning("Trying again")
|
||||||
|
else:
|
||||||
|
return create_json_response(
|
||||||
|
HTTPStatus.GATEWAY_TIMEOUT, "Homeserver not responding"
|
||||||
|
)
|
||||||
|
|
||||||
|
return create_json_response(HTTPStatus.OK, "OK")
|
||||||
|
|
||||||
|
|
||||||
|
def create_json_response(status, ret):
|
||||||
|
"""Create a JSON response."""
|
||||||
|
LOGGER.debug(f"Creating json response: {status=}, {ret=}")
|
||||||
|
response_data = {"status": status, "ret": ret}
|
||||||
|
return web.json_response(response_data, status=status)
|
||||||
|
|
||||||
|
|
||||||
|
async def send_room_message(room_id, content):
|
||||||
|
"""Send a message to a room."""
|
||||||
|
LOGGER.debug(f"Sending room message in {room_id=}: {content=}")
|
||||||
|
return await CLIENT.room_send(
|
||||||
|
room_id=room_id, message_type="m.room.message", content=content
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def main(event):
|
||||||
|
"""
|
||||||
|
Launch main coroutine.
|
||||||
|
|
||||||
|
matrix client login & start web server
|
||||||
|
"""
|
||||||
|
LOGGER.info(f"Log in {conf.MATRIX_ID=} on {conf.MATRIX_URL=}")
|
||||||
|
await CLIENT.login(conf.MATRIX_PW)
|
||||||
|
|
||||||
|
server = web.Server(handler)
|
||||||
|
runner = web.ServerRunner(server)
|
||||||
|
await runner.setup()
|
||||||
|
LOGGER.info(f"Binding on {conf.SERVER_ADDRESS=}")
|
||||||
|
site = web.TCPSite(runner, *conf.SERVER_ADDRESS)
|
||||||
|
await site.start()
|
||||||
|
|
||||||
|
# Run until we get a shutdown request
|
||||||
|
await event.wait()
|
||||||
|
|
||||||
|
# Cleanup
|
||||||
|
await runner.cleanup()
|
||||||
|
await CLIENT.close()
|
||||||
|
|
||||||
|
|
||||||
|
def terminate(event, signal):
|
||||||
|
"""Close handling stuff."""
|
||||||
|
event.set()
|
||||||
|
asyncio.get_event_loop().remove_signal_handler(signal)
|
||||||
|
|
||||||
|
|
||||||
|
def run():
|
||||||
|
"""Launch everything."""
|
||||||
|
LOGGER.info("Starting...")
|
||||||
|
loop = asyncio.get_event_loop()
|
||||||
|
event = asyncio.Event()
|
||||||
|
|
||||||
|
for sig in (SIGINT, SIGTERM):
|
||||||
|
loop.add_signal_handler(sig, terminate, event, sig)
|
||||||
|
|
||||||
|
loop.run_until_complete(main(event))
|
||||||
|
|
||||||
|
LOGGER.info("Closing...")
|
||||||
|
loop.close()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
log_format = "%(asctime)s - %(name)s - %(lineno)d - %(levelname)s - %(message)s"
|
||||||
|
logging.basicConfig(level=50 - 10 * conf.VERBOSE, format=log_format)
|
||||||
|
run()
|
66
matrix_webhook/conf.py
Normal file
66
matrix_webhook/conf.py
Normal file
|
@ -0,0 +1,66 @@
|
||||||
|
"""Configuration for Matrix Webhook."""
|
||||||
|
import argparse
|
||||||
|
import os
|
||||||
|
|
||||||
|
parser = argparse.ArgumentParser(description=__doc__, prog="python -m matrix_webhook")
|
||||||
|
parser.add_argument(
|
||||||
|
"-H",
|
||||||
|
"--host",
|
||||||
|
default=os.environ.get("HOST", ""),
|
||||||
|
help="host to listen to. Default: `''`. Environment variable: `HOST`",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"-P",
|
||||||
|
"--port",
|
||||||
|
type=int,
|
||||||
|
default=os.environ.get("PORT", 4785),
|
||||||
|
help="port to listed to. Default: 4785. Environment variable: `PORT`",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"-u",
|
||||||
|
"--matrix-url",
|
||||||
|
default=os.environ.get("MATRIX_URL", "https://matrix.org"),
|
||||||
|
help="matrix homeserver url. Default: `https://matrix.org`. Environment variable: `MATRIX_URL`",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"-i",
|
||||||
|
"--matrix-id",
|
||||||
|
help="matrix user-id. Required. Environment variable: `MATRIX_ID`",
|
||||||
|
**(
|
||||||
|
{"default": os.environ["MATRIX_ID"]}
|
||||||
|
if "MATRIX_ID" in os.environ
|
||||||
|
else {"required": True}
|
||||||
|
),
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"-p",
|
||||||
|
"--matrix-pw",
|
||||||
|
help="matrix password. Required. Environment variable: `MATRIX_PW`",
|
||||||
|
**(
|
||||||
|
{"default": os.environ["MATRIX_PW"]}
|
||||||
|
if "MATRIX_PW" in os.environ
|
||||||
|
else {"required": True}
|
||||||
|
),
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"-k",
|
||||||
|
"--api-key",
|
||||||
|
help="shared secret to use this service. Required. Environment variable: `API_KEY`",
|
||||||
|
**(
|
||||||
|
{"default": os.environ["API_KEY"]}
|
||||||
|
if "API_KEY" in os.environ
|
||||||
|
else {"required": True}
|
||||||
|
),
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"-v", "--verbose", action="count", default=0, help="increment verbosity level"
|
||||||
|
)
|
||||||
|
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
SERVER_ADDRESS = (args.host, args.port)
|
||||||
|
MATRIX_URL = args.matrix_url
|
||||||
|
MATRIX_ID = args.matrix_id
|
||||||
|
MATRIX_PW = args.matrix_pw
|
||||||
|
API_KEY = args.api_key
|
||||||
|
VERBOSE = args.verbose
|
38
poetry.lock
generated
38
poetry.lock
generated
|
@ -87,7 +87,7 @@ tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "black"
|
name = "black"
|
||||||
version = "21.6b0"
|
version = "21.7b0"
|
||||||
description = "The uncompromising code formatter."
|
description = "The uncompromising code formatter."
|
||||||
category = "dev"
|
category = "dev"
|
||||||
optional = false
|
optional = false
|
||||||
|
@ -99,7 +99,7 @@ click = ">=7.1.2"
|
||||||
mypy-extensions = ">=0.4.3"
|
mypy-extensions = ">=0.4.3"
|
||||||
pathspec = ">=0.8.1,<1"
|
pathspec = ">=0.8.1,<1"
|
||||||
regex = ">=2020.1.8"
|
regex = ">=2020.1.8"
|
||||||
toml = ">=0.10.1"
|
tomli = ">=0.2.6,<2.0.0"
|
||||||
|
|
||||||
[package.extras]
|
[package.extras]
|
||||||
colorama = ["colorama (>=0.4.3)"]
|
colorama = ["colorama (>=0.4.3)"]
|
||||||
|
@ -301,7 +301,7 @@ testing = ["coverage", "pyyaml"]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "matrix-nio"
|
name = "matrix-nio"
|
||||||
version = "0.18.3"
|
version = "0.18.4"
|
||||||
description = "A Python Matrix client library, designed according to sans I/O principles."
|
description = "A Python Matrix client library, designed according to sans I/O principles."
|
||||||
category = "main"
|
category = "main"
|
||||||
optional = false
|
optional = false
|
||||||
|
@ -320,7 +320,7 @@ pycryptodome = ">=3.10.1,<4.0.0"
|
||||||
unpaddedbase64 = ">=2.1.0,<3.0.0"
|
unpaddedbase64 = ">=2.1.0,<3.0.0"
|
||||||
|
|
||||||
[package.extras]
|
[package.extras]
|
||||||
e2e = ["python-olm (>=3.1.3,<4.0.0)", "peewee (>=3.14.4,<4.0.0)", "cachetools (>=4.2.1,<5.0.0)", "atomicwrites (>=1.4.0,<2.0.0)"]
|
e2e = ["atomicwrites (>=1.4.0,<2.0.0)", "cachetools (>=4.2.1,<5.0.0)", "peewee (>=3.14.4,<4.0.0)", "python-olm (>=3.1.3,<4.0.0)"]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "mccabe"
|
name = "mccabe"
|
||||||
|
@ -348,11 +348,11 @@ python-versions = "*"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pathspec"
|
name = "pathspec"
|
||||||
version = "0.8.1"
|
version = "0.9.0"
|
||||||
description = "Utility library for gitignore style pattern matching of file paths."
|
description = "Utility library for gitignore style pattern matching of file paths."
|
||||||
category = "dev"
|
category = "dev"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
|
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pycodestyle"
|
name = "pycodestyle"
|
||||||
|
@ -463,12 +463,12 @@ optional = false
|
||||||
python-versions = "*"
|
python-versions = "*"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "toml"
|
name = "tomli"
|
||||||
version = "0.10.2"
|
version = "1.0.4"
|
||||||
description = "Python Library for Tom's Obvious, Minimal Language"
|
description = "A lil' TOML parser"
|
||||||
category = "dev"
|
category = "dev"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*"
|
python-versions = ">=3.6"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "typing-extensions"
|
name = "typing-extensions"
|
||||||
|
@ -568,8 +568,8 @@ attrs = [
|
||||||
{file = "attrs-21.2.0.tar.gz", hash = "sha256:ef6aaac3ca6cd92904cdd0d83f629a15f18053ec84e6432106f7a4d04ae4f5fb"},
|
{file = "attrs-21.2.0.tar.gz", hash = "sha256:ef6aaac3ca6cd92904cdd0d83f629a15f18053ec84e6432106f7a4d04ae4f5fb"},
|
||||||
]
|
]
|
||||||
black = [
|
black = [
|
||||||
{file = "black-21.6b0-py3-none-any.whl", hash = "sha256:dfb8c5a069012b2ab1e972e7b908f5fb42b6bbabcba0a788b86dc05067c7d9c7"},
|
{file = "black-21.7b0-py3-none-any.whl", hash = "sha256:1c7aa6ada8ee864db745b22790a32f94b2795c253a75d6d9b5e439ff10d23116"},
|
||||||
{file = "black-21.6b0.tar.gz", hash = "sha256:dc132348a88d103016726fe360cb9ede02cecf99b76e3660ce6c596be132ce04"},
|
{file = "black-21.7b0.tar.gz", hash = "sha256:c8373c6491de9362e39271630b65b964607bc5c79c83783547d76c839b3aa219"},
|
||||||
]
|
]
|
||||||
certifi = [
|
certifi = [
|
||||||
{file = "certifi-2021.5.30-py2.py3-none-any.whl", hash = "sha256:50b1e4f8446b06f41be7dd6338db18e0990601dce795c2b1686458aa7e8fa7d8"},
|
{file = "certifi-2021.5.30-py2.py3-none-any.whl", hash = "sha256:50b1e4f8446b06f41be7dd6338db18e0990601dce795c2b1686458aa7e8fa7d8"},
|
||||||
|
@ -696,8 +696,8 @@ markdown = [
|
||||||
{file = "Markdown-3.3.4.tar.gz", hash = "sha256:31b5b491868dcc87d6c24b7e3d19a0d730d59d3e46f4eea6430a321bed387a49"},
|
{file = "Markdown-3.3.4.tar.gz", hash = "sha256:31b5b491868dcc87d6c24b7e3d19a0d730d59d3e46f4eea6430a321bed387a49"},
|
||||||
]
|
]
|
||||||
matrix-nio = [
|
matrix-nio = [
|
||||||
{file = "matrix-nio-0.18.3.tar.gz", hash = "sha256:7f2e92f5b219367e47824bfe8bd2b1a06ce83ae28956f112dd3c2112a4d27085"},
|
{file = "matrix-nio-0.18.4.tar.gz", hash = "sha256:e5f0a62ff66474f5c56dc40c3eb3c74a29943800589ae6947ea224c288f3ab41"},
|
||||||
{file = "matrix_nio-0.18.3-py3-none-any.whl", hash = "sha256:a28653f96760b045c7edc53b645872cf2facc1639dc8cf56d748cd5e54ed2d3d"},
|
{file = "matrix_nio-0.18.4-py3-none-any.whl", hash = "sha256:7ea00ae362a3621624b8ff463a2b06cb945ffa12e2f3919cae5321d06285a361"},
|
||||||
]
|
]
|
||||||
mccabe = [
|
mccabe = [
|
||||||
{file = "mccabe-0.6.1-py2.py3-none-any.whl", hash = "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42"},
|
{file = "mccabe-0.6.1-py2.py3-none-any.whl", hash = "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42"},
|
||||||
|
@ -747,8 +747,8 @@ mypy-extensions = [
|
||||||
{file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"},
|
{file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"},
|
||||||
]
|
]
|
||||||
pathspec = [
|
pathspec = [
|
||||||
{file = "pathspec-0.8.1-py2.py3-none-any.whl", hash = "sha256:aa0cb481c4041bf52ffa7b0d8fa6cd3e88a2ca4879c533c9153882ee2556790d"},
|
{file = "pathspec-0.9.0-py2.py3-none-any.whl", hash = "sha256:7d15c4ddb0b5c802d161efc417ec1a2558ea2653c2e8ad9c19098201dc1c993a"},
|
||||||
{file = "pathspec-0.8.1.tar.gz", hash = "sha256:86379d6b86d75816baba717e64b1a3a3469deb93bb76d613c9ce79edc5cb68fd"},
|
{file = "pathspec-0.9.0.tar.gz", hash = "sha256:e564499435a2673d586f6b2130bb5b95f04a3ba06f81b8f895b651a3c76aabb1"},
|
||||||
]
|
]
|
||||||
pycodestyle = [
|
pycodestyle = [
|
||||||
{file = "pycodestyle-2.7.0-py2.py3-none-any.whl", hash = "sha256:514f76d918fcc0b55c6680472f0a37970994e07bbb80725808c17089be302068"},
|
{file = "pycodestyle-2.7.0-py2.py3-none-any.whl", hash = "sha256:514f76d918fcc0b55c6680472f0a37970994e07bbb80725808c17089be302068"},
|
||||||
|
@ -880,9 +880,9 @@ snowballstemmer = [
|
||||||
{file = "snowballstemmer-2.1.0-py2.py3-none-any.whl", hash = "sha256:b51b447bea85f9968c13b650126a888aabd4cb4463fca868ec596826325dedc2"},
|
{file = "snowballstemmer-2.1.0-py2.py3-none-any.whl", hash = "sha256:b51b447bea85f9968c13b650126a888aabd4cb4463fca868ec596826325dedc2"},
|
||||||
{file = "snowballstemmer-2.1.0.tar.gz", hash = "sha256:e997baa4f2e9139951b6f4c631bad912dfd3c792467e2f03d7239464af90e914"},
|
{file = "snowballstemmer-2.1.0.tar.gz", hash = "sha256:e997baa4f2e9139951b6f4c631bad912dfd3c792467e2f03d7239464af90e914"},
|
||||||
]
|
]
|
||||||
toml = [
|
tomli = [
|
||||||
{file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"},
|
{file = "tomli-1.0.4-py3-none-any.whl", hash = "sha256:0713b16ff91df8638a6a694e295c8159ab35ba93e3424a626dd5226d386057be"},
|
||||||
{file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"},
|
{file = "tomli-1.0.4.tar.gz", hash = "sha256:be670d0d8d7570fd0ea0113bd7bb1ba3ac6706b4de062cc4c952769355c9c268"},
|
||||||
]
|
]
|
||||||
typing-extensions = [
|
typing-extensions = [
|
||||||
{file = "typing_extensions-3.10.0.0-py2-none-any.whl", hash = "sha256:0ac0f89795dd19de6b97debb0c6af1c70987fd80a2d62d1958f7e56fcc31b497"},
|
{file = "typing_extensions-3.10.0.0-py2-none-any.whl", hash = "sha256:0ac0f89795dd19de6b97debb0c6af1c70987fd80a2d62d1958f7e56fcc31b497"},
|
||||||
|
|
|
@ -17,4 +17,4 @@ RUN pip install --no-cache-dir markdown matrix-nio httpx coverage
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
CMD ./tests/start.py
|
CMD ./tests/start.py -vvv
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
#!/usr/bin/env python
|
#!/usr/bin/env python
|
||||||
"""Entry point to start an instrumentalized bot for coverage and run tests."""
|
"""Entry point to start an instrumentalized bot for coverage and run tests."""
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import logging
|
||||||
from os import environ
|
from os import environ
|
||||||
from subprocess import Popen, run
|
from subprocess import Popen, run
|
||||||
from time import time
|
from time import time
|
||||||
|
@ -15,6 +17,12 @@ KEY, MATRIX_URL, MATRIX_ID, MATRIX_PW = (
|
||||||
environ[v] for v in ["API_KEY", "MATRIX_URL", "MATRIX_ID", "MATRIX_PW"]
|
environ[v] for v in ["API_KEY", "MATRIX_URL", "MATRIX_ID", "MATRIX_PW"]
|
||||||
)
|
)
|
||||||
FULL_ID = f'@{MATRIX_ID}:{MATRIX_URL.split("/")[2]}'
|
FULL_ID = f'@{MATRIX_ID}:{MATRIX_URL.split("/")[2]}'
|
||||||
|
LOGGER = logging.getLogger("matrix-webhook.tests.start")
|
||||||
|
|
||||||
|
parser = argparse.ArgumentParser(description=__doc__)
|
||||||
|
parser.add_argument(
|
||||||
|
"-v", "--verbose", action="count", default=0, help="increment verbosity level"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def bot_req(req=None, key=None, room_id=None):
|
def bot_req(req=None, key=None, room_id=None):
|
||||||
|
@ -47,6 +55,7 @@ def wait_available(url: str, key: str, timeout: int = 10) -> bool:
|
||||||
def run_and_test():
|
def run_and_test():
|
||||||
"""Launch the bot and its tests."""
|
"""Launch the bot and its tests."""
|
||||||
# Start the server, and wait for it
|
# Start the server, and wait for it
|
||||||
|
LOGGER.info("Spawning synapse")
|
||||||
srv = Popen(
|
srv = Popen(
|
||||||
[
|
[
|
||||||
"python",
|
"python",
|
||||||
|
@ -60,29 +69,38 @@ def run_and_test():
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# Register a user for the bot.
|
# Register a user for the bot.
|
||||||
|
LOGGER.info("Registering the bot")
|
||||||
with open("/srv/homeserver.yaml") as f:
|
with open("/srv/homeserver.yaml") as f:
|
||||||
secret = yaml.safe_load(f.read()).get("registration_shared_secret", None)
|
secret = yaml.safe_load(f.read()).get("registration_shared_secret", None)
|
||||||
request_registration(MATRIX_ID, MATRIX_PW, MATRIX_URL, secret, admin=True)
|
request_registration(MATRIX_ID, MATRIX_PW, MATRIX_URL, secret, admin=True)
|
||||||
|
|
||||||
# Start the bot, and wait for it
|
# Start the bot, and wait for it
|
||||||
bot = Popen(["coverage", "run", "matrix_webhook.py"])
|
LOGGER.info("Spawning the bot")
|
||||||
|
bot = Popen(["coverage", "run", "-m", "matrix_webhook", "-vvvvv"])
|
||||||
if not wait_available(BOT_URL, "status"):
|
if not wait_available(BOT_URL, "status"):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# Run the main unittest module
|
# Run the main unittest module
|
||||||
|
LOGGER.info("Runnig unittests")
|
||||||
ret = main(module=None, exit=False).result.wasSuccessful()
|
ret = main(module=None, exit=False).result.wasSuccessful()
|
||||||
|
|
||||||
|
LOGGER.info("Stopping synapse")
|
||||||
srv.terminate()
|
srv.terminate()
|
||||||
|
|
||||||
# TODO Check what the bot says when the server is offline
|
# TODO Check what the bot says when the server is offline
|
||||||
# print(bot_req({'text': 'bye'}, KEY), {'status': 200, 'ret': 'OK'})
|
# print(bot_req({'text': 'bye'}, KEY), {'status': 200, 'ret': 'OK'})
|
||||||
|
|
||||||
|
LOGGER.info("Stopping the bot")
|
||||||
bot.terminate()
|
bot.terminate()
|
||||||
|
|
||||||
|
LOGGER.info("Processing coverage")
|
||||||
for cmd in ["report", "html", "xml"]:
|
for cmd in ["report", "html", "xml"]:
|
||||||
run(["coverage", cmd])
|
run(["coverage", cmd])
|
||||||
return ret
|
return ret
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
args = parser.parse_args()
|
||||||
|
log_format = "%(asctime)s - %(name)s - %(lineno)d - %(levelname)s - %(message)s"
|
||||||
|
logging.basicConfig(level=50 - 10 * args.verbose, format=log_format)
|
||||||
exit(not run_and_test())
|
exit(not run_and_test())
|
||||||
|
|
|
@ -20,9 +20,11 @@ class BotTest(unittest.IsolatedAsyncioTestCase):
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
bot_req({"text": 3, "key": None}), {"status": 401, "ret": "Invalid API key"}
|
bot_req({"text": 3, "key": None}), {"status": 401, "ret": "Invalid API key"}
|
||||||
)
|
)
|
||||||
|
# TODO: if the client from matrix_webhook has olm support, this won't be a 403 from synapse,
|
||||||
# TODO: we are not sending to a real room, so this should not be "OK"
|
# but a LocalProtocolError from matrix_webhook
|
||||||
self.assertEqual(bot_req({"text": 3}, KEY), {"status": 200, "ret": "OK"})
|
self.assertEqual(
|
||||||
|
bot_req({"text": 3}, KEY), {"status": 403, "ret": "Unknown room"}
|
||||||
|
)
|
||||||
|
|
||||||
async def test_message(self):
|
async def test_message(self):
|
||||||
"""Send a markdown message, and check the result."""
|
"""Send a markdown message, and check the result."""
|
||||||
|
@ -45,3 +47,15 @@ class BotTest(unittest.IsolatedAsyncioTestCase):
|
||||||
self.assertEqual(message.sender, FULL_ID)
|
self.assertEqual(message.sender, FULL_ID)
|
||||||
self.assertEqual(message.body, text)
|
self.assertEqual(message.body, text)
|
||||||
self.assertEqual(message.formatted_body, "<h1>Hello</h1>")
|
self.assertEqual(message.formatted_body, "<h1>Hello</h1>")
|
||||||
|
|
||||||
|
async def test_reconnect(self):
|
||||||
|
"""Check the reconnecting path."""
|
||||||
|
client = nio.AsyncClient(MATRIX_URL, MATRIX_ID)
|
||||||
|
await client.login(MATRIX_PW)
|
||||||
|
room = await client.room_create()
|
||||||
|
await client.logout(all_devices=True)
|
||||||
|
await client.close()
|
||||||
|
self.assertEqual(
|
||||||
|
bot_req({"text": "Re"}, KEY, room.room_id),
|
||||||
|
{"status": 200, "ret": "OK"},
|
||||||
|
)
|
||||||
|
|
Loading…
Reference in a new issue