Merge pull request #9 from nim65s/devel

setup packaging
This commit is contained in:
Guilhem Saurel 2021-07-18 18:43:29 +02:00 committed by GitHub
commit 6943432367
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 351 additions and 226 deletions

33
CHANGELOG.md Normal file
View 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

View file

@ -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"]

View file

@ -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

View file

@ -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
```

View file

@ -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
View 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
View 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
View file

@ -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"},

View file

@ -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

View file

@ -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())

View file

@ -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"},
)