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
|
||||
|
||||
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
|
||||
|
||||
|
|
62
README.md
62
README.md
|
@ -7,27 +7,59 @@
|
|||
|
||||
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
|
||||
|
||||
```
|
||||
pip3 install --user markdown matrix-nio
|
||||
./matrix_webhook.py
|
||||
poetry install
|
||||
# or python3 -m pip install --user markdown matrix-nio
|
||||
python3 -m matrix_webhook
|
||||
```
|
||||
|
||||
## Prod
|
||||
|
||||
A `docker-compose.yml` is provided:
|
||||
|
||||
- Use [Traefik](https://traefik.io/) on the `web` docker network, eg. with
|
||||
[proxyta.net](https://framagit.org/oxyta.net/proxyta.net)
|
||||
- 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
|
||||
|
||||
[#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]]
|
||||
name = "black"
|
||||
version = "21.6b0"
|
||||
version = "21.7b0"
|
||||
description = "The uncompromising code formatter."
|
||||
category = "dev"
|
||||
optional = false
|
||||
|
@ -99,7 +99,7 @@ click = ">=7.1.2"
|
|||
mypy-extensions = ">=0.4.3"
|
||||
pathspec = ">=0.8.1,<1"
|
||||
regex = ">=2020.1.8"
|
||||
toml = ">=0.10.1"
|
||||
tomli = ">=0.2.6,<2.0.0"
|
||||
|
||||
[package.extras]
|
||||
colorama = ["colorama (>=0.4.3)"]
|
||||
|
@ -301,7 +301,7 @@ testing = ["coverage", "pyyaml"]
|
|||
|
||||
[[package]]
|
||||
name = "matrix-nio"
|
||||
version = "0.18.3"
|
||||
version = "0.18.4"
|
||||
description = "A Python Matrix client library, designed according to sans I/O principles."
|
||||
category = "main"
|
||||
optional = false
|
||||
|
@ -320,7 +320,7 @@ pycryptodome = ">=3.10.1,<4.0.0"
|
|||
unpaddedbase64 = ">=2.1.0,<3.0.0"
|
||||
|
||||
[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]]
|
||||
name = "mccabe"
|
||||
|
@ -348,11 +348,11 @@ python-versions = "*"
|
|||
|
||||
[[package]]
|
||||
name = "pathspec"
|
||||
version = "0.8.1"
|
||||
version = "0.9.0"
|
||||
description = "Utility library for gitignore style pattern matching of file paths."
|
||||
category = "dev"
|
||||
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]]
|
||||
name = "pycodestyle"
|
||||
|
@ -463,12 +463,12 @@ optional = false
|
|||
python-versions = "*"
|
||||
|
||||
[[package]]
|
||||
name = "toml"
|
||||
version = "0.10.2"
|
||||
description = "Python Library for Tom's Obvious, Minimal Language"
|
||||
name = "tomli"
|
||||
version = "1.0.4"
|
||||
description = "A lil' TOML parser"
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*"
|
||||
python-versions = ">=3.6"
|
||||
|
||||
[[package]]
|
||||
name = "typing-extensions"
|
||||
|
@ -568,8 +568,8 @@ attrs = [
|
|||
{file = "attrs-21.2.0.tar.gz", hash = "sha256:ef6aaac3ca6cd92904cdd0d83f629a15f18053ec84e6432106f7a4d04ae4f5fb"},
|
||||
]
|
||||
black = [
|
||||
{file = "black-21.6b0-py3-none-any.whl", hash = "sha256:dfb8c5a069012b2ab1e972e7b908f5fb42b6bbabcba0a788b86dc05067c7d9c7"},
|
||||
{file = "black-21.6b0.tar.gz", hash = "sha256:dc132348a88d103016726fe360cb9ede02cecf99b76e3660ce6c596be132ce04"},
|
||||
{file = "black-21.7b0-py3-none-any.whl", hash = "sha256:1c7aa6ada8ee864db745b22790a32f94b2795c253a75d6d9b5e439ff10d23116"},
|
||||
{file = "black-21.7b0.tar.gz", hash = "sha256:c8373c6491de9362e39271630b65b964607bc5c79c83783547d76c839b3aa219"},
|
||||
]
|
||||
certifi = [
|
||||
{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"},
|
||||
]
|
||||
matrix-nio = [
|
||||
{file = "matrix-nio-0.18.3.tar.gz", hash = "sha256:7f2e92f5b219367e47824bfe8bd2b1a06ce83ae28956f112dd3c2112a4d27085"},
|
||||
{file = "matrix_nio-0.18.3-py3-none-any.whl", hash = "sha256:a28653f96760b045c7edc53b645872cf2facc1639dc8cf56d748cd5e54ed2d3d"},
|
||||
{file = "matrix-nio-0.18.4.tar.gz", hash = "sha256:e5f0a62ff66474f5c56dc40c3eb3c74a29943800589ae6947ea224c288f3ab41"},
|
||||
{file = "matrix_nio-0.18.4-py3-none-any.whl", hash = "sha256:7ea00ae362a3621624b8ff463a2b06cb945ffa12e2f3919cae5321d06285a361"},
|
||||
]
|
||||
mccabe = [
|
||||
{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"},
|
||||
]
|
||||
pathspec = [
|
||||
{file = "pathspec-0.8.1-py2.py3-none-any.whl", hash = "sha256:aa0cb481c4041bf52ffa7b0d8fa6cd3e88a2ca4879c533c9153882ee2556790d"},
|
||||
{file = "pathspec-0.8.1.tar.gz", hash = "sha256:86379d6b86d75816baba717e64b1a3a3469deb93bb76d613c9ce79edc5cb68fd"},
|
||||
{file = "pathspec-0.9.0-py2.py3-none-any.whl", hash = "sha256:7d15c4ddb0b5c802d161efc417ec1a2558ea2653c2e8ad9c19098201dc1c993a"},
|
||||
{file = "pathspec-0.9.0.tar.gz", hash = "sha256:e564499435a2673d586f6b2130bb5b95f04a3ba06f81b8f895b651a3c76aabb1"},
|
||||
]
|
||||
pycodestyle = [
|
||||
{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.tar.gz", hash = "sha256:e997baa4f2e9139951b6f4c631bad912dfd3c792467e2f03d7239464af90e914"},
|
||||
]
|
||||
toml = [
|
||||
{file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"},
|
||||
{file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"},
|
||||
tomli = [
|
||||
{file = "tomli-1.0.4-py3-none-any.whl", hash = "sha256:0713b16ff91df8638a6a694e295c8159ab35ba93e3424a626dd5226d386057be"},
|
||||
{file = "tomli-1.0.4.tar.gz", hash = "sha256:be670d0d8d7570fd0ea0113bd7bb1ba3ac6706b4de062cc4c952769355c9c268"},
|
||||
]
|
||||
typing-extensions = [
|
||||
{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
|
||||
|
||||
CMD ./tests/start.py
|
||||
CMD ./tests/start.py -vvv
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
#!/usr/bin/env python
|
||||
"""Entry point to start an instrumentalized bot for coverage and run tests."""
|
||||
|
||||
import argparse
|
||||
import logging
|
||||
from os import environ
|
||||
from subprocess import Popen, run
|
||||
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"]
|
||||
)
|
||||
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):
|
||||
|
@ -47,6 +55,7 @@ def wait_available(url: str, key: str, timeout: int = 10) -> bool:
|
|||
def run_and_test():
|
||||
"""Launch the bot and its tests."""
|
||||
# Start the server, and wait for it
|
||||
LOGGER.info("Spawning synapse")
|
||||
srv = Popen(
|
||||
[
|
||||
"python",
|
||||
|
@ -60,29 +69,38 @@ def run_and_test():
|
|||
return False
|
||||
|
||||
# Register a user for the bot.
|
||||
LOGGER.info("Registering the bot")
|
||||
with open("/srv/homeserver.yaml") as f:
|
||||
secret = yaml.safe_load(f.read()).get("registration_shared_secret", None)
|
||||
request_registration(MATRIX_ID, MATRIX_PW, MATRIX_URL, secret, admin=True)
|
||||
|
||||
# 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"):
|
||||
return False
|
||||
|
||||
# Run the main unittest module
|
||||
LOGGER.info("Runnig unittests")
|
||||
ret = main(module=None, exit=False).result.wasSuccessful()
|
||||
|
||||
LOGGER.info("Stopping synapse")
|
||||
srv.terminate()
|
||||
|
||||
# TODO Check what the bot says when the server is offline
|
||||
# print(bot_req({'text': 'bye'}, KEY), {'status': 200, 'ret': 'OK'})
|
||||
|
||||
LOGGER.info("Stopping the bot")
|
||||
bot.terminate()
|
||||
|
||||
LOGGER.info("Processing coverage")
|
||||
for cmd in ["report", "html", "xml"]:
|
||||
run(["coverage", cmd])
|
||||
return ret
|
||||
|
||||
|
||||
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())
|
||||
|
|
|
@ -20,9 +20,11 @@ class BotTest(unittest.IsolatedAsyncioTestCase):
|
|||
self.assertEqual(
|
||||
bot_req({"text": 3, "key": None}), {"status": 401, "ret": "Invalid API key"}
|
||||
)
|
||||
|
||||
# TODO: we are not sending to a real room, so this should not be "OK"
|
||||
self.assertEqual(bot_req({"text": 3}, KEY), {"status": 200, "ret": "OK"})
|
||||
# TODO: if the client from matrix_webhook has olm support, this won't be a 403 from synapse,
|
||||
# but a LocalProtocolError from matrix_webhook
|
||||
self.assertEqual(
|
||||
bot_req({"text": 3}, KEY), {"status": 403, "ret": "Unknown room"}
|
||||
)
|
||||
|
||||
async def test_message(self):
|
||||
"""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.body, text)
|
||||
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