Compare commits
232 commits
Author | SHA1 | Date | |
---|---|---|---|
bca1d919b2 | |||
8e1a0894f3 | |||
86f49edf3d | |||
fc1552304f | |||
66df6e56e2 | |||
05eeab7c9c | |||
33a650efda | |||
eb0231f4ba | |||
7ff165a6f7 | |||
3c2b2d346c | |||
6e05a2a3f9 | |||
6781f994f9 | |||
|
7ba365d6e8 | ||
|
347f639422 | ||
|
e8fcb07205 | ||
|
6cc4a7cfd9 | ||
|
e963478986 | ||
|
2ac1f123fd | ||
|
37def41cc8 | ||
|
1dc9d79a3b | ||
|
07f3bd4714 | ||
|
3017fb255a | ||
|
97b842a20e | ||
|
962812758b | ||
|
67ffd334e3 | ||
|
5140df64c6 | ||
|
eb28e99aa9 | ||
|
c251d1c9cb | ||
|
8fac0e1a5d | ||
|
272a427034 | ||
|
7915e78d87 | ||
|
efdfd3a2db | ||
|
5b7067f382 | ||
|
228f38f101 | ||
|
1fb1a0b8f3 | ||
|
880832275b | ||
|
e139c3a61b | ||
|
3288c3ff5a | ||
|
48b988efc0 | ||
|
8f3a6a77ff | ||
|
d25cfd3ba6 | ||
|
bc858f8bac | ||
|
13c7d0a431 | ||
|
c82ab8c5e0 | ||
|
53dd300414 | ||
|
1750a1fbed | ||
|
807a1e7b99 | ||
|
8632c87d4f | ||
|
5905136919 | ||
|
776ebb67c2 | ||
|
0ef10cc620 | ||
|
93211f33c7 | ||
|
7e3983626d | ||
|
8127e52fc7 | ||
|
d49bd60403 | ||
|
940cfd9bd3 | ||
|
2d17e00ec5 | ||
|
9ca48a083e | ||
|
8c59d2412e | ||
|
d7bcc786b0 | ||
|
a7104ac2a0 | ||
|
152c80b3ff | ||
|
ac4e00b54a | ||
|
760e5cbb15 | ||
|
2ca7201346 | ||
|
6c7d015b8d | ||
|
50e93180ee | ||
|
7b9605a363 | ||
|
29653fc04b | ||
|
0ab2507009 | ||
|
dd20888af4 | ||
|
0819503d8b | ||
|
2fa2557076 | ||
|
52b5ff87ea | ||
|
36b5e85f7e | ||
|
7f2ba3d83b | ||
|
363f8a497d | ||
|
d53ab56661 | ||
|
8469e679b3 | ||
|
90cc262a73 | ||
|
8e3f4e5771 | ||
|
8c63736879 | ||
|
baa20e51eb | ||
|
f1d2d2bd68 | ||
|
70d85af682 | ||
|
395daf4630 | ||
|
8d81b7ba44 | ||
|
36b3f6f877 | ||
|
e9b3f83cd1 | ||
|
916ddb7d18 | ||
|
bdd3940066 | ||
|
5bc8f81d8e | ||
|
11ee2ec2f4 | ||
|
b66b9717e4 | ||
|
c4e0bb76e9 | ||
|
14398e4b7d | ||
|
42317f74d9 | ||
|
f6bf150c7f | ||
|
e91865f175 | ||
|
a3607c5ee2 | ||
f4808719b0 | |||
8c18c2054e | |||
97e10df1a2 | |||
630dc98974 | |||
7b94c80bbd | |||
5048ef8c4c | |||
|
ff10e58cba | ||
|
6032dfe836 | ||
affe16c605 | |||
92b6c3618b | |||
0c8c5faad0 | |||
|
d799fc18a0 | ||
|
0ac65ba895 | ||
e42aa941e5 | |||
778ebcdad9 | |||
e0a407acd9 | |||
93f84859b4 | |||
b5912a8b6c | |||
d964f75c84 | |||
a3638fdc75 | |||
ab5927bfa4 | |||
8e5a90ec1f | |||
87ac023631 | |||
1ea47991e4 | |||
0d92c378c4 | |||
d82a0ba9bd | |||
793a5e8c8c | |||
09fe26633f | |||
|
afd68b791c | ||
|
b3004a5c8a | ||
|
2a78594861 | ||
|
5a5e9655be | ||
|
ea91eea7ee | ||
|
e148dfbad9 | ||
|
b3a2e00f43 | ||
|
d0481d741a | ||
|
85dd602f5c | ||
|
d2a3e618f4 | ||
|
d9fe01c1db | ||
|
5d39019ef7 | ||
|
7ffa47c267 | ||
|
624a1b2c08 | ||
|
6c5b3e1358 | ||
|
df2059b012 | ||
|
d0c4cd4227 | ||
|
7aa5df3871 | ||
|
e5f3d7bfbd | ||
|
ef79d39a9f | ||
|
179ef11ae7 | ||
|
8592be257f | ||
|
0aea63903d | ||
|
df8ce523cb | ||
|
58d0e83f8f | ||
|
d726db6ed2 | ||
|
1c00ff22f3 | ||
|
7e1be831df | ||
|
7f5c8583a1 | ||
|
4928ceb91a | ||
|
cda8898019 | ||
|
6b5d6e6e87 | ||
|
4bcdb25c80 | ||
|
ac7d1d9647 | ||
|
9a544b8f2b | ||
|
8f215c04fd | ||
|
7f20fb7ff9 | ||
|
2b7b79971d | ||
|
6aaac9149d | ||
|
eb3c795368 | ||
|
c03ae0a571 | ||
|
eabb446d05 | ||
|
6f7d38dbd7 | ||
|
530f40a129 | ||
|
528940abcc | ||
|
0ccec84eef | ||
|
c8f6c9ec28 | ||
|
c07d4bfa8d | ||
|
2d232fe1f7 | ||
|
3bebc88ee2 | ||
|
fa8f9b4a51 | ||
|
292d77274d | ||
|
8b32c972b8 | ||
|
c86145f794 | ||
|
a6b192fbd7 | ||
|
59e98b99a7 | ||
|
71c9c6cb0e | ||
|
932965c8af | ||
|
dcc73dfc81 | ||
|
fbcae98390 | ||
|
8a3bbef54c | ||
|
6943432367 | ||
|
febf2f857c | ||
|
c9045d407d | ||
|
922ebf5c78 | ||
|
00997360eb | ||
|
bc646b9f4a | ||
|
a974f073c9 | ||
|
0c0a42a4c9 | ||
|
a0a78bbad0 | ||
|
570e86941b | ||
|
2c8c618fe0 | ||
|
044876daf6 | ||
|
d952590d94 | ||
|
2bda1d5968 | ||
|
e2d85eaa21 | ||
|
19ef1f4e93 | ||
|
91b23cd166 | ||
|
32208294ea | ||
|
c5b7ea19ce | ||
|
562b29c8a2 | ||
|
6633020fba | ||
|
2d8c68665e | ||
|
6a2e0336d9 | ||
|
999b824874 | ||
|
abe6497421 | ||
|
fb17a87016 | ||
|
d3d5ea7df2 | ||
|
2499832e1c | ||
|
98cd9362aa | ||
|
4506632c6f | ||
|
aad02887f1 | ||
|
00f47f99a9 | ||
|
78b9533e2b | ||
|
139ec1670c | ||
|
54baf29d51 | ||
|
cf054631e8 | ||
|
6c138a65b4 | ||
|
887dc95e3d | ||
|
9883782f3a | ||
|
9615cdf3c7 | ||
|
ce83079d59 | ||
|
38e814c956 | ||
|
f11af8de16 |
52 changed files with 5421 additions and 127 deletions
2
.flake8
Normal file
2
.flake8
Normal file
|
@ -0,0 +1,2 @@
|
|||
[flake8]
|
||||
max-line-length = 88
|
7
.gitignore
vendored
Normal file
7
.gitignore
vendored
Normal file
|
@ -0,0 +1,7 @@
|
|||
.coverage
|
||||
.env
|
||||
.mypy_cache
|
||||
coverage.xml
|
||||
htmlcov
|
||||
**__pycache__
|
||||
config.yaml
|
|
@ -1,6 +1,6 @@
|
|||
repos:
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v2.4.0
|
||||
rev: v4.4.0
|
||||
hooks:
|
||||
- id: check-added-large-files
|
||||
- id: check-ast
|
||||
|
@ -11,19 +11,26 @@ repos:
|
|||
- id: debug-statements
|
||||
- id: detect-private-key
|
||||
- id: end-of-file-fixer
|
||||
- id: flake8
|
||||
- id: mixed-line-ending
|
||||
- id: trailing-whitespace
|
||||
- repo: https://github.com/pre-commit/mirrors-yapf
|
||||
rev: v0.29.0
|
||||
- repo: https://github.com/psf/black
|
||||
rev: 22.10.0
|
||||
hooks:
|
||||
- id: yapf
|
||||
- id: black
|
||||
language_version: python3
|
||||
- repo: https://github.com/PyCQA/pydocstyle
|
||||
rev: 5.0.1
|
||||
rev: 6.1.1
|
||||
hooks:
|
||||
- id: pydocstyle
|
||||
- repo: https://gitlab.com/smop/pre-commit-hooks
|
||||
rev: v1.0.0
|
||||
args:
|
||||
- --ignore=D200,D203,D212
|
||||
- repo: https://github.com/PyCQA/flake8
|
||||
rev: 6.0.0
|
||||
hooks:
|
||||
- id: check-poetry
|
||||
- id: check-gitlab-ci
|
||||
- id: flake8
|
||||
- repo: https://github.com/asottile/pyupgrade
|
||||
rev: v3.2.2
|
||||
hooks:
|
||||
- id: pyupgrade
|
||||
args:
|
||||
- --py38-plus
|
||||
|
|
103
CHANGELOG.md
Normal file
103
CHANGELOG.md
Normal file
|
@ -0,0 +1,103 @@
|
|||
# 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]
|
||||
|
||||
## [v3.5.0] - 2022-09-07
|
||||
|
||||
- Add formatter for grafana 9
|
||||
in [#45](https://github.com/nim65s/matrix-webhook/pull/45)
|
||||
by [@svenseeberg](https://github.com/svenseeberg)
|
||||
|
||||
## [v3.4.0] - 2022-08-12
|
||||
|
||||
- fix tests
|
||||
- add `matrix-webhook` script
|
||||
in [#25](https://github.com/nim65s/matrix-webhook/pull/25)
|
||||
and [#35](https://github.com/nim65s/matrix-webhook/pull/35)
|
||||
by [@a7p](https://github.com/a7p)
|
||||
- publish linux/arm64 image
|
||||
in [#37](https://github.com/nim65s/matrix-webhook/pull/35)
|
||||
by [@kusold](https://github.com/kusold)
|
||||
- update badges
|
||||
- setup dependabot
|
||||
- misc upgrades from poetry update, pre-commit.ci, and dependabot
|
||||
|
||||
## [v3.3.0] - 2022-03-04
|
||||
|
||||
- add pyupgrade
|
||||
- add gitlab formatter for google chat & microsoft teams
|
||||
in [#21](https://github.com/nim65s/matrix-webhook/pull/21)
|
||||
by [@GhislainC](https://github.com/GhislainC)
|
||||
- join room before sending message
|
||||
in [#12](https://github.com/nim65s/matrix-webhook/pull/12)
|
||||
by [@bboehmke](https://github.com/bboehmke)
|
||||
- Changed --api-key and envvar API_KEY to --api-keys and API_KEYS respectively
|
||||
- Changed handling of api key to use a list instead of single value
|
||||
can be used
|
||||
- Changed the formatters to a more plugin-based approach where each formatter is
|
||||
its own <formattername>.py file in formatters directory
|
||||
- Added pingdom formatter (currently handling http, dns, and tcp probe types)
|
||||
|
||||
## [v3.2.1] - 2021-08-28
|
||||
|
||||
- fix changelog
|
||||
|
||||
## [v3.2.0] - 2021-08-27
|
||||
|
||||
- add github & grafana formatters
|
||||
- add formatted_body to bypass markdown with direct
|
||||
[matrix-custom-HTML](https://matrix.org/docs/spec/client_server/r0.6.1#m-room-message-msgtypes)
|
||||
- allow "key" to be passed as a parameter
|
||||
- allow to use a sha256 HMAC hex digest with the key instead of the raw key
|
||||
- allow "room_id" to be passed as a parameter or with the data
|
||||
- rename "text" to "body".
|
||||
- Publish releases also on github from github actions
|
||||
- fix tests for recent synapse docker image
|
||||
|
||||
## [v3.1.1] - 2021-07-18
|
||||
|
||||
## [v3.1.0] - 2021-07-18
|
||||
|
||||
- Publish on PyPI & Docker Hub with Github Actions
|
||||
in [#10](https://github.com/nim65s/matrix-webhook/pull/10)
|
||||
by [@nim65s](https://github.com/nim65s)
|
||||
|
||||
## [v3.0.0] - 2021-07-18
|
||||
|
||||
- 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)
|
||||
|
||||
## [v2.0.0] - 2020-03-14
|
||||
- Update to matrix-nio & aiohttp & markdown
|
||||
|
||||
## [v1.0.0] - 2020-02-14
|
||||
- First release with matrix-client & http.server
|
||||
|
||||
[Unreleased]: https://github.com/nim65s/matrix-webhook/compare/v3.5.0...master
|
||||
[v3.5.0]: https://github.com/nim65s/matrix-webhook/compare/v3.4.0...v3.5.0
|
||||
[v3.4.0]: https://github.com/nim65s/matrix-webhook/compare/v3.3.0...v3.4.0
|
||||
[v3.3.0]: https://github.com/nim65s/matrix-webhook/compare/v3.2.1...v3.3.0
|
||||
[v3.2.1]: https://github.com/nim65s/matrix-webhook/compare/v3.2.0...v3.2.1
|
||||
[v3.2.0]: https://github.com/nim65s/matrix-webhook/compare/v3.1.1...v3.2.0
|
||||
[v3.1.1]: https://github.com/nim65s/matrix-webhook/compare/v3.1.0...v3.1.1
|
||||
[v3.1.0]: https://github.com/nim65s/matrix-webhook/compare/v3.0.0...v3.1.0
|
||||
[v3.0.0]: https://github.com/nim65s/matrix-webhook/compare/v2.0.0...v3.0.0
|
||||
[v2.0.0]: https://github.com/nim65s/matrix-webhook/compare/v1.0.0...v2.0.0
|
||||
[v1.0.0]: https://github.com/nim65s/matrix-webhook/releases/tag/v1.0.0
|
|
@ -1,9 +1,9 @@
|
|||
FROM python:3.8-slim
|
||||
FROM python:3.9
|
||||
|
||||
EXPOSE 4785
|
||||
|
||||
RUN pip3 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
|
||||
|
||||
|
|
105
README.md
105
README.md
|
@ -2,30 +2,74 @@
|
|||
|
||||
Post a message to a matrix room with a simple HTTP POST
|
||||
|
||||
## Configuration
|
||||
This is my own fork of https://github.com/nim65s/matrix-webhook
|
||||
It adds a yaml configuration with multi-api key endpoints and moves the filtes
|
||||
to more of a plugin-based system
|
||||
|
||||
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:
|
||||
## Install
|
||||
|
||||
For now, clone this repo and run `pip install .`
|
||||
|
||||
## Start
|
||||
|
||||
Create a matrix user for the bot, and launch this app with the following arguments and/or environment variables
|
||||
(environment variables update defaults, arguments take precedence):
|
||||
|
||||
```
|
||||
matrix-webhook -h
|
||||
# OR
|
||||
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_KEYS]
|
||||
[-c CONFIG] [-v]
|
||||
|
||||
Configuration for Matrix Webhook.
|
||||
|
||||
options:
|
||||
-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_KEYS, --api-keys API_KEYS
|
||||
comma separated list of shared secrets to use this
|
||||
service. Required. Environment variable: `API_KEYS`
|
||||
-c CONFIG, --config CONFIG
|
||||
configuration file. Default: `config.yaml`
|
||||
-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
|
||||
|
||||
## Dev
|
||||
|
||||
```
|
||||
pip3 install --user matrix-client
|
||||
./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
|
||||
- Configure your DNS for `${CHATONS_SERVICE:-matrixwebhook}.${CHATONS_DOMAIN:-localhost}` **and**
|
||||
`www.${CHATONS_SERVICE:-matrixwebhook}.${CHATONS_DOMAIN:-localhost}`
|
||||
- Configure your DNS for `${CHATONS_SERVICE:-matrixwebhook}.${CHATONS_DOMAIN:-localhost}`
|
||||
|
||||
```
|
||||
docker-compose up -d
|
||||
|
@ -34,10 +78,45 @@ docker-compose up -d
|
|||
## Test / Usage
|
||||
|
||||
```
|
||||
curl -d '{"text":"new contrib from toto: [44](http://radio.localhost/map/#44)", "key": "secret"}' 'matrixwebhook.localhost/!DPrUlnwOhBEfYwsDLh:matrix.org'
|
||||
curl -d '{"body":"new contrib from toto: [44](http://radio.localhost/map/#44)", "key": "secret"}' \
|
||||
'http://matrixwebhook.localhost/!DPrUlnwOhBEfYwsDLh:matrix.org'
|
||||
```
|
||||
(or localhost:4785 without docker)
|
||||
|
||||
### Formatters
|
||||
|
||||
These formatters will output custom messages depending on the specific formatter. Generally to set these up, on the remote provider you would create a webhook with `https://your.webhook.domain/?formatter=<formatter columun below>&api_key=<your apikey>`
|
||||
|
||||
| formatter | description | key location |
|
||||
| -- | - | - |
|
||||
| github | for github.com | in github JSON webhook settings as `secret` |
|
||||
| grafana | for grafana | in webhook URL with `api_key=<yourkey>` |
|
||||
| pingdom | for pingdom.com | in webhook URL with `api_key=<yourkey>` |
|
||||
| buildbot | buildbot reporter | in webhook URL with `api_key=<yourkey>` or in master.cfg credentials header as `api_key` |
|
||||
| generic | returns raw JSON that was recieved. For developing additional formatter plugins | in URL with api_key=<yourkey> |
|
||||
|
||||
For example, if your matrix-webhook was hosted at https://webhooks.example.com, and you were setting up pingdom and you have an api_key of "123", you would use the following URL for your webhook call from pingdom:
|
||||
`https://webhooks.example.com/?formatter=pingdom&api_key=123`
|
||||
|
||||
### For Gitlab
|
||||
|
||||
At a group level, Gitlab does not permit to setup webhooks. A workaround consists to use Google
|
||||
Chat or Microsoft Teams notification integration with a custom URL (Gitlab does not check if the url begins with the normal url of the service).
|
||||
|
||||
#### Google Chat
|
||||
|
||||
Add a Google Chat integration with an URL ending with `?formatter=gitlab_gchat&key=API_KEY`
|
||||
|
||||
#### Microsoft Teams
|
||||
|
||||
Add a Microsoft Teams integration with an URL ending with `?formatter=gitlab_teams&key=API_KEY`
|
||||
|
||||
## 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)
|
||||
|
||||
## Unit tests
|
||||
|
||||
```
|
||||
docker-compose -f test.yml up --exit-code-from tests --force-recreate --build
|
||||
```
|
||||
|
|
19
config-dist.yaml
Normal file
19
config-dist.yaml
Normal file
|
@ -0,0 +1,19 @@
|
|||
hostname: localhost
|
||||
port: 4785
|
||||
# matrix-specific settings
|
||||
matrix:
|
||||
# URL of homeserver to connect
|
||||
url: https://matrix.org
|
||||
# user to connect to homserver as
|
||||
id: username
|
||||
# password for the user
|
||||
pw: password
|
||||
# keys to allow These should be random strings
|
||||
# these could be generated with something like `openssl rand -hex 24`
|
||||
# change these, you only need
|
||||
api_keys:
|
||||
RandomTextForKey: "!room_id:server.domain" # Can add a comment for what the key is used
|
||||
secondRandomkey: "!a_different_room_id:server.domain"
|
||||
thirdKey: #This one has no room specified, so it must be specified in the payload data or url
|
||||
log:
|
||||
level: debug
|
|
@ -6,6 +6,7 @@ networks:
|
|||
|
||||
services:
|
||||
bot:
|
||||
image: nim65s/matrix-webhook
|
||||
build: .
|
||||
restart: unless-stopped
|
||||
env_file:
|
||||
|
@ -14,4 +15,4 @@ services:
|
|||
- web
|
||||
labels:
|
||||
traefik.enable: "true"
|
||||
traefik.frontend.rule: "Host: ${CHATONS_SERVICE:-matrixwebhook}.${CHATONS_DOMAIN:-localhost}, www.${CHATONS_SERVICE:-matrixwebhook}.${CHATONS_DOMAIN:-localhost}"
|
||||
traefik.http.routers.matrix-webhook.rule: "Host(`${CHATONS_SERVICE:-matrixwebhook}.${CHATONS_DOMAIN:-localhost}`)"
|
||||
|
|
18
docs/release.md
Normal file
18
docs/release.md
Normal file
|
@ -0,0 +1,18 @@
|
|||
# Publish a new release
|
||||
|
||||
A github actions handle the build of the release archives, and push them to PyPI and Github Releases.
|
||||
To trigger it, we just need to:
|
||||
|
||||
1. use poetry to update the version number
|
||||
2. update the changelog
|
||||
3. git commit
|
||||
4. git tag
|
||||
5. git push
|
||||
6. git push --tags
|
||||
|
||||
|
||||
For this, an helper script is provided:
|
||||
|
||||
```bash
|
||||
./docs/release.sh [patch|minor|major|x.y.z]
|
||||
```
|
22
docs/release.sh
Executable file
22
docs/release.sh
Executable file
|
@ -0,0 +1,22 @@
|
|||
#!/bin/bash -eux
|
||||
# ./docs/release.sh [patch|minor|major|x.y.z]
|
||||
|
||||
[[ $(basename "$PWD") == docs ]] && cd ..
|
||||
|
||||
|
||||
OLD=$(poetry version -s)
|
||||
|
||||
poetry version "$1"
|
||||
|
||||
NEW=$(poetry version -s)
|
||||
DATE=$(date +%Y-%m-%d)
|
||||
|
||||
sed -i "/^## \[Unreleased\]/a \\\n## [v$NEW] - $DATE" CHANGELOG.md
|
||||
sed -i "/^\[Unreleased\]/s/$OLD/$NEW/" CHANGELOG.md
|
||||
sed -i "/^\[Unreleased\]/a [v$NEW]: https://github.com/nim65s/matrix-webhook/compare/v$OLD...v$NEW" CHANGELOG.md
|
||||
|
||||
git add pyproject.toml CHANGELOG.md
|
||||
git commit -m "Release v$NEW"
|
||||
git tag -s "v$NEW" -m "Release v$NEW"
|
||||
git push
|
||||
git push --tags
|
|
@ -1,98 +0,0 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
Matrix Webhook.
|
||||
|
||||
Post a message to a matrix room with a simple HTTP POST
|
||||
v1: matrix-client & http.server
|
||||
v2: matrix-nio & aiohttp & markdown
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import os
|
||||
from http import HTTPStatus
|
||||
from signal import SIGINT, SIGTERM
|
||||
|
||||
from markdown import markdown
|
||||
|
||||
from aiohttp import web
|
||||
from nio import AsyncClient
|
||||
|
||||
SERVER_ADDRESS = ('', int(os.environ.get('PORT', 4785)))
|
||||
MATRIX_URL = os.environ.get('MATRIX_URL', 'https://matrix.org')
|
||||
MATRIX_ID = os.environ.get('MATRIX_ID', '@wwm:matrix.org')
|
||||
MATRIX_PW = os.environ['MATRIX_PW']
|
||||
API_KEY = os.environ['API_KEY']
|
||||
CLIENT = AsyncClient(MATRIX_URL, 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.
|
||||
"""
|
||||
data = await request.read()
|
||||
data = json.loads(data.decode())
|
||||
status, ret = HTTPStatus.BAD_REQUEST, 'I need a json dict with text & key'
|
||||
if all(key in data for key in ['text', 'key']):
|
||||
status, ret = HTTPStatus.UNAUTHORIZED, 'I need the good "key"'
|
||||
if data['key'] == API_KEY:
|
||||
status, ret = HTTPStatus.OK, 'OK'
|
||||
await CLIENT.room_send(room_id=str(request.rel_url)[1:],
|
||||
message_type="m.room.message",
|
||||
content={
|
||||
"msgtype": "m.text",
|
||||
"body": data['text'],
|
||||
"format": "org.matrix.custom.html",
|
||||
"formatted_body": markdown(data['text']),
|
||||
})
|
||||
|
||||
return web.Response(text='{"status": %i, "ret": "%s"}' % (status, ret),
|
||||
content_type='application/json',
|
||||
status=status)
|
||||
|
||||
|
||||
async def main(event):
|
||||
"""
|
||||
Launch main coroutine.
|
||||
|
||||
matrix client login & start web server
|
||||
"""
|
||||
await CLIENT.login(MATRIX_PW)
|
||||
|
||||
server = web.Server(handler)
|
||||
runner = web.ServerRunner(server)
|
||||
await runner.setup()
|
||||
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."""
|
||||
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))
|
||||
|
||||
loop.close()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
run()
|
15
matrix_webhook/__main__.py
Normal file
15
matrix_webhook/__main__.py
Normal file
|
@ -0,0 +1,15 @@
|
|||
"""Matrix Webhook module entrypoint."""
|
||||
import logging
|
||||
|
||||
from . import app, conf
|
||||
|
||||
|
||||
def main():
|
||||
"""Start everything."""
|
||||
log_format = "%(asctime)s - %(name)s - %(lineno)d - %(levelname)s - %(message)s"
|
||||
logging.basicConfig(level=50 - 10 * conf.VERBOSE, format=log_format)
|
||||
app.run()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
56
matrix_webhook/app.py
Normal file
56
matrix_webhook/app.py
Normal file
|
@ -0,0 +1,56 @@
|
|||
"""Matrix Webhook app."""
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
from signal import SIGINT, SIGTERM
|
||||
|
||||
from aiohttp import web
|
||||
|
||||
from . import conf, handler, utils
|
||||
|
||||
LOGGER = logging.getLogger("matrix_webhook.app")
|
||||
|
||||
|
||||
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 utils.CLIENT.login(conf.MATRIX_PW)
|
||||
|
||||
server = web.Server(handler.matrix_webhook)
|
||||
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 utils.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()
|
105
matrix_webhook/conf.py
Normal file
105
matrix_webhook/conf.py
Normal file
|
@ -0,0 +1,105 @@
|
|||
"""Configuration for Matrix Webhook."""
|
||||
import argparse
|
||||
import os
|
||||
import sys
|
||||
import yaml
|
||||
|
||||
|
||||
def get_numeric_log_level(log_level):
|
||||
"""Return a number that will calculate to the verbosity level"""
|
||||
if log_level.lower() == "debug":
|
||||
return 4
|
||||
elif log_level.lower() == "info":
|
||||
return 3
|
||||
elif log_level.lower() == "warning":
|
||||
return 2
|
||||
elif log_level.lower() == "error":
|
||||
return 1
|
||||
elif log_level.lower() == "critical":
|
||||
return 0
|
||||
else:
|
||||
return 2
|
||||
|
||||
|
||||
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`",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-p",
|
||||
"--matrix-pw",
|
||||
help="matrix password. Required. Environment variable: `MATRIX_PW`",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-k",
|
||||
"--api-keys",
|
||||
help="comma separated list of shared secrets to use this service. Required. Environment variable: `API_KEYS`",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-c",
|
||||
"--config",
|
||||
help="configuration file. Default: `config.yaml`",
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"-v", "--verbose", action="count", default=0, help="increment verbosity level"
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.config:
|
||||
with open(args.config) as f:
|
||||
config = yaml.safe_load(f)
|
||||
SERVER_ADDRESS = (config["hostname"], config["port"])
|
||||
MATRIX_URL = config["matrix"]["url"]
|
||||
MATRIX_ID = config["matrix"]["id"]
|
||||
MATRIX_PW = config["matrix"]["pw"]
|
||||
API_KEYS = config["api_keys"]
|
||||
LOG_FILE = config["log"]
|
||||
VERBOSE = get_numeric_log_level(config["log"]["level"])
|
||||
else:
|
||||
SERVER_ADDRESS = (args.host, args.port)
|
||||
MATRIX_URL = args.matrix_url
|
||||
LOG_FILE = args.log
|
||||
if not args.matrix_id:
|
||||
print("Missing matrix user-id. Use -i or --matrix-id or specify in config.yaml")
|
||||
sys.exit(1)
|
||||
else:
|
||||
MATRIX_ID = args.matrix_id
|
||||
|
||||
if not args.matrix_pw:
|
||||
print(
|
||||
"Missing matrix password. Use -p or --matrix-pw or specify in config.yaml"
|
||||
)
|
||||
sys.exit(1)
|
||||
else:
|
||||
MATRIX_PW = args.matrix_pw
|
||||
|
||||
if not args.api_keys:
|
||||
print("Missing api keys. Use -k or --api-keys or specify in config.yaml")
|
||||
sys.exit(1)
|
||||
else:
|
||||
API_KEYS = args.api_keys.split(",")
|
||||
VERBOSE = args.verbose
|
BIN
matrix_webhook/formatters/__pycache__/github.cpython-310.pyc
Normal file
BIN
matrix_webhook/formatters/__pycache__/github.cpython-310.pyc
Normal file
Binary file not shown.
8
matrix_webhook/formatters/alertmanager.py
Normal file
8
matrix_webhook/formatters/alertmanager.py
Normal file
|
@ -0,0 +1,8 @@
|
|||
|
||||
def formatter(data, headers):
|
||||
"""Pretty-print an alertmanager notification."""
|
||||
text = ""
|
||||
for alert in data['alerts']:
|
||||
text += f"[{alert['status']}] - {alert['labels']['summary']}\n\n"
|
||||
data["body"] = text
|
||||
return data
|
34
matrix_webhook/formatters/buildbot.py
Normal file
34
matrix_webhook/formatters/buildbot.py
Normal file
|
@ -0,0 +1,34 @@
|
|||
from datetime import datetime
|
||||
|
||||
|
||||
def formatter(data, headers):
|
||||
"""Pretty-print a buildbot notification."""
|
||||
buildid = data["buildid"]
|
||||
buildstate = data["state_string"]
|
||||
buildlink = data["url"]
|
||||
reason = data["buildset"]["reason"]
|
||||
project = data["properties"]["project"][0]
|
||||
submittime = datetime.fromtimestamp(data["buildset"]["submitted_at"])
|
||||
try:
|
||||
if buildstate == "starting":
|
||||
data["body"] = (
|
||||
f"###Buildbot job #{buildid} for {project} - {buildstate}\n\n"
|
||||
f"{reason}\n\n"
|
||||
f"**started at** {submittime}\n\n"
|
||||
f"[view details]({buildlink})"
|
||||
)
|
||||
elif buildstate == "build successful":
|
||||
data["body"] = (
|
||||
f"###Buildbot job #{buildid} for {project} - {buildstate}\n\n"
|
||||
f"**completed at** {datetime.fromtimestamp(data['complete_at'])}\n\n"
|
||||
f"[view details]({buildlink})"
|
||||
)
|
||||
else:
|
||||
data["body"] = (
|
||||
f"###Buildbot job #{buildid} for {project} - {buildstate}\n\n"
|
||||
f"[view details]({buildlink})"
|
||||
)
|
||||
except Exception as error:
|
||||
print(error)
|
||||
|
||||
return data
|
36
matrix_webhook/formatters/crowdsec.py
Normal file
36
matrix_webhook/formatters/crowdsec.py
Normal file
|
@ -0,0 +1,36 @@
|
|||
import requests
|
||||
|
||||
|
||||
def get_abuse_confidence(ip):
|
||||
"""get abuseipdb's confidence level on an ip passed in, and return that value"""
|
||||
base_url = "https://api.abuseipdb.com/api/v2/check"
|
||||
api_key = "YOUR API KEY"
|
||||
headers = {"Key": api_key, "Accept": "application/json"}
|
||||
data = {"ipAddress": ip, "maxAgeInDays": 90}
|
||||
r = requests.get(base_url, headers=headers, json=data)
|
||||
confidence = r.json()["data"]["abuseConfidenceScore"]
|
||||
whitelist = r.json()["data"]["isWhitelisted"]
|
||||
return [confidence, whitelist]
|
||||
|
||||
|
||||
def formatter(data, headers):
|
||||
"""format a message sent with crowdsec http endpoints"""
|
||||
data_out = ""
|
||||
for row in data["body"]:
|
||||
ip = row["host"]
|
||||
duration = row["duration"]
|
||||
confidence, whitelisted = get_abuse_confidence(ip)
|
||||
if "crowdsecurity" in row["scenario"]:
|
||||
source, scenario, *_ = row["scenario"].split("/")
|
||||
row[
|
||||
"scenario"
|
||||
] = f"[{scenario}](https://hub.crowdsec.net/author/crowdsecurity/configurations/{scenario})"
|
||||
data_out += f"{ip} has been banned {duration} due to {row['scenario']}\n\n"
|
||||
if whitelisted:
|
||||
data_out += "**Note: AbuseIPDB has whitelisted this address\n\n"
|
||||
data_out += (
|
||||
f"[AbuseIPDB](https://www.abuseipdb.com/check/{row['host']})({confidence}%) | "
|
||||
f"[Crowdsec](https://app.crowdsec.net/cti/{row['host']})\n\n"
|
||||
)
|
||||
data["body"] = data_out
|
||||
return data
|
4
matrix_webhook/formatters/generic.py
Normal file
4
matrix_webhook/formatters/generic.py
Normal file
|
@ -0,0 +1,4 @@
|
|||
def formatter(data, headers):
|
||||
"""Just dump the json data"""
|
||||
data["body"] = f"{data}"
|
||||
return data
|
15
matrix_webhook/formatters/github.py
Normal file
15
matrix_webhook/formatters/github.py
Normal file
|
@ -0,0 +1,15 @@
|
|||
def formatter(data, headers):
|
||||
"""Pretty-print a github notification."""
|
||||
# TODO: Write nice useful formatters. This is only an example.
|
||||
if headers["X-GitHub-Event"] == "push":
|
||||
pusher, ref, a, b, c = [
|
||||
data[k] for k in ["pusher", "ref", "after", "before", "compare"]
|
||||
]
|
||||
pusher = f"[@{pusher['name']}](https://github.com/{pusher['name']})"
|
||||
data["body"] = f"{pusher} pushed on {ref}: [{b} → {a}]({c}):\n\n"
|
||||
for commit in data["commits"]:
|
||||
data["body"] += f"- [{commit['message']}]({commit['url']})\n"
|
||||
else:
|
||||
data["body"] = "notification from github"
|
||||
data["digest"] = headers["X-Hub-Signature-256"].replace("sha256=", "")
|
||||
return data
|
12
matrix_webhook/formatters/grafana.py
Normal file
12
matrix_webhook/formatters/grafana.py
Normal file
|
@ -0,0 +1,12 @@
|
|||
def formatter(data, headers):
|
||||
"""Pretty-print a grafana notification."""
|
||||
text = ""
|
||||
if "title" in data:
|
||||
text = "#### " + data["title"] + "\n"
|
||||
if "message" in data:
|
||||
text = text + data["message"] + "\n\n"
|
||||
if "evalMatches" in data:
|
||||
for match in data["evalMatches"]:
|
||||
text = text + "* " + match["metric"] + ": " + str(match["value"]) + "\n"
|
||||
data["body"] = text
|
||||
return data
|
9
matrix_webhook/formatters/grafana_9x.py
Normal file
9
matrix_webhook/formatters/grafana_9x.py
Normal file
|
@ -0,0 +1,9 @@
|
|||
def grafana_9x(data, headers):
|
||||
"""Pretty-print a Grafana newer than v9.x notification."""
|
||||
text = ""
|
||||
if "title" in data:
|
||||
text = "#### " + data["title"] + "\n"
|
||||
if "message" in data:
|
||||
text = text + data["message"].replace("\n", "\n\n") + "\n\n"
|
||||
data["body"] = text
|
||||
return data
|
64
matrix_webhook/formatters/pingdom.py
Normal file
64
matrix_webhook/formatters/pingdom.py
Normal file
|
@ -0,0 +1,64 @@
|
|||
from datetime import datetime
|
||||
|
||||
|
||||
def formatter(data, headers):
|
||||
"""Pretty-print a pingdom notification."""
|
||||
# JSON data formatting was obtained from https://www.pingdom.com/resources/webhooks/
|
||||
# these are common to all check types
|
||||
check_id = data["check_id"]
|
||||
check_name = data["check_name"]
|
||||
current_state = data["current_state"]
|
||||
tags = data["tags"]
|
||||
local_time = datetime.fromtimestamp(data["state_changed_timestamp"])
|
||||
|
||||
if data["check_type"].lower() == "http":
|
||||
# http https or http_custom check types
|
||||
try:
|
||||
check_url = data["check_params"]["full_url"]
|
||||
message = ""
|
||||
message += f"###{check_name} is {current_state}\n\n{check_url}"
|
||||
message += f" marked {current_state} at {local_time} ⚬ "
|
||||
message += f"[view details](https://my.pingdom.com/reports/responsetime#check={check_id})"
|
||||
if tags:
|
||||
message += f"\n\nTags: {tags}"
|
||||
data["body"] = message
|
||||
except Exception as error:
|
||||
data["body"] = (
|
||||
f"Error: An attempt to post from pingdom was malformed "
|
||||
"(or I don't know how to handle what was sent).\n\n"
|
||||
f"{repr(error)}"
|
||||
)
|
||||
elif data["check_type"].lower() == "dns":
|
||||
# There are a bunch of values that are blanke when you do a test
|
||||
# so ignore them if value is unset
|
||||
try:
|
||||
first_ip = data["first_probe"]["ip"]
|
||||
except KeyError:
|
||||
first_ip = "unknown"
|
||||
try:
|
||||
second_ip = data["second_probe"]["ip"]
|
||||
except KeyError:
|
||||
second_ip = "unknown"
|
||||
try:
|
||||
first_location = data["first_probe"]["location"]
|
||||
except KeyError:
|
||||
first_location = "unknown"
|
||||
try:
|
||||
second_location = data["second_probe"]["location"]
|
||||
except KeyError:
|
||||
second_location = "unknown"
|
||||
try:
|
||||
expected_ip = data["check_params"]["expected_ip"]
|
||||
data["body"] = (
|
||||
f"###{check_name} is {current_state}\n\n"
|
||||
f"expected {expected_ip} but got:\n\n"
|
||||
f" {first_ip} ({first_location})\n\n"
|
||||
f" {second_ip} ({second_location})\n\n"
|
||||
f" marked {current_state} at {local_time} ⚬ "
|
||||
f"[view details](https://my.pingdom.com/reports/responsetime#check={check_id})"
|
||||
)
|
||||
|
||||
except Exception as error:
|
||||
print(error)
|
||||
|
||||
return data
|
5
matrix_webhook/formatters/slack.py
Normal file
5
matrix_webhook/formatters/slack.py
Normal file
|
@ -0,0 +1,5 @@
|
|||
def formatter(data, headers):
|
||||
""" format a message sent with slack api endpoints"""
|
||||
text = data["attachments"][0]["text"]
|
||||
data["body"] = f"{text}"
|
||||
return data
|
91
matrix_webhook/handler.py
Normal file
91
matrix_webhook/handler.py
Normal file
|
@ -0,0 +1,91 @@
|
|||
"""Matrix Webhook main request handler."""
|
||||
|
||||
import json
|
||||
import logging
|
||||
from http import HTTPStatus
|
||||
from hmac import HMAC
|
||||
import importlib
|
||||
|
||||
from markdown import markdown
|
||||
|
||||
from . import conf, utils
|
||||
|
||||
LOGGER = logging.getLogger("matrix_webhook.handler")
|
||||
|
||||
|
||||
async def matrix_webhook(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_b = await request.read()
|
||||
|
||||
try:
|
||||
data = json.loads(data_b.decode())
|
||||
except json.decoder.JSONDecodeError:
|
||||
return utils.create_json_response(HTTPStatus.BAD_REQUEST, "Invalid JSON")
|
||||
|
||||
# legacy naming
|
||||
if "text" in data and "body" not in data:
|
||||
data["body"] = data["text"]
|
||||
|
||||
# allow key to be passed as a parameter
|
||||
if "key" in request.rel_url.query and "key" not in data:
|
||||
data["key"] = request.rel_url.query["key"]
|
||||
|
||||
if "formatter" in request.rel_url.query:
|
||||
try:
|
||||
format = request.rel_url.query["formatter"]
|
||||
plugin = importlib.import_module(f"matrix_webhook.formatters.{format}", "formatter")
|
||||
data = plugin.formatter(data, request.headers)
|
||||
except ModuleNotFoundError:
|
||||
return utils.create_json_response(
|
||||
HTTPStatus.BAD_REQUEST, "Unknown formatter"
|
||||
)
|
||||
|
||||
if "room_id" in request.rel_url.query and "room_id" not in data:
|
||||
data["room_id"] = request.rel_url.query["room_id"]
|
||||
if "room_id" not in data:
|
||||
data["room_id"] = request.path.lstrip("/")
|
||||
|
||||
# If we get a good SHA-256 HMAC digest,
|
||||
# we can consider that the sender has the right API key
|
||||
if "digest" in data:
|
||||
if data["digest"] == HMAC(conf.API_KEY.encode(), data_b, "sha256").hexdigest():
|
||||
data["key"] = conf.API_KEY
|
||||
else: # but if there is a wrong digest, an informative error should be provided
|
||||
return utils.create_json_response(
|
||||
HTTPStatus.UNAUTHORIZED, "Invalid SHA-256 HMAC digest"
|
||||
)
|
||||
|
||||
missing = []
|
||||
for key in ["body", "key", "room_id"]:
|
||||
if key not in data or not data[key]:
|
||||
missing.append(key)
|
||||
if missing:
|
||||
return utils.create_json_response(
|
||||
HTTPStatus.BAD_REQUEST, f"Missing {', '.join(missing)}"
|
||||
)
|
||||
|
||||
if data["key"] not in conf.API_KEYS:
|
||||
return utils.create_json_response(HTTPStatus.UNAUTHORIZED, "Invalid API key")
|
||||
|
||||
if "formatted_body" in data:
|
||||
formatted_body = data["formatted_body"]
|
||||
else:
|
||||
formatted_body = markdown(str(data["body"]), extensions=["extra"])
|
||||
|
||||
# try to join room first -> non none response means error
|
||||
resp = await utils.join_room(data["room_id"])
|
||||
if resp is not None:
|
||||
return resp
|
||||
|
||||
content = {
|
||||
"msgtype": "m.text",
|
||||
"body": data["body"],
|
||||
"format": "org.matrix.custom.html",
|
||||
"formatted_body": formatted_body,
|
||||
}
|
||||
return await utils.send_room_message(data["room_id"], content)
|
78
matrix_webhook/utils.py
Normal file
78
matrix_webhook/utils.py
Normal file
|
@ -0,0 +1,78 @@
|
|||
"""Matrix Webhook utils."""
|
||||
|
||||
import logging
|
||||
from http import HTTPStatus
|
||||
|
||||
from aiohttp import web
|
||||
from nio import AsyncClient
|
||||
from nio.exceptions import LocalProtocolError
|
||||
from nio.responses import RoomSendError, JoinError
|
||||
|
||||
from . import conf
|
||||
|
||||
ERROR_MAP = {
|
||||
"M_FORBIDDEN": HTTPStatus.FORBIDDEN,
|
||||
"M_CONSENT_NOT_GIVEN": HTTPStatus.FORBIDDEN,
|
||||
}
|
||||
LOGGER = logging.getLogger("matrix_webhook.utils")
|
||||
CLIENT = AsyncClient(conf.MATRIX_URL, conf.MATRIX_ID)
|
||||
|
||||
|
||||
def error_map(resp):
|
||||
"""Map response errors to HTTP status."""
|
||||
if resp.status_code == "M_UNKNOWN":
|
||||
# in this case, we should directly consider the HTTP status from the response
|
||||
# ref. https://matrix.org/docs/spec/client_server/r0.6.1#api-standards
|
||||
return resp.transport_response.status
|
||||
return ERROR_MAP[resp.status_code]
|
||||
|
||||
|
||||
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 join_room(room_id):
|
||||
"""Try to join the room."""
|
||||
LOGGER.debug(f"Join room {room_id=}")
|
||||
|
||||
for _ in range(10):
|
||||
try:
|
||||
resp = await CLIENT.join(room_id)
|
||||
if isinstance(resp, JoinError):
|
||||
if resp.status_code == "M_UNKNOWN_TOKEN":
|
||||
LOGGER.warning("Reconnecting")
|
||||
await CLIENT.login(conf.MATRIX_PW)
|
||||
else:
|
||||
return create_json_response(error_map(resp), resp.message)
|
||||
else:
|
||||
return None
|
||||
except LocalProtocolError as e:
|
||||
LOGGER.error(f"Send error: {e}")
|
||||
LOGGER.warning("Trying again")
|
||||
return create_json_response(HTTPStatus.GATEWAY_TIMEOUT, "Homeserver not responding")
|
||||
|
||||
|
||||
async def send_room_message(room_id, content):
|
||||
"""Send a message to a room."""
|
||||
LOGGER.debug(f"Sending room message in {room_id=}: {content=}")
|
||||
|
||||
for _ in range(10):
|
||||
try:
|
||||
resp = await CLIENT.room_send(
|
||||
room_id=room_id, message_type="m.room.message", content=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), resp.message)
|
||||
else:
|
||||
return create_json_response(HTTPStatus.OK, "OK")
|
||||
except LocalProtocolError as e:
|
||||
LOGGER.error(f"Send error: {e}")
|
||||
LOGGER.warning("Trying again")
|
||||
return create_json_response(HTTPStatus.GATEWAY_TIMEOUT, "Homeserver not responding")
|
2577
package-lock.json
generated
Normal file
2577
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
5
package.json
Normal file
5
package.json
Normal file
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"dependencies": {
|
||||
"@mermaid-js/mermaid-cli": "^8.13.3"
|
||||
}
|
||||
}
|
1040
poetry.lock
generated
Normal file
1040
poetry.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
36
pyproject.toml
Normal file
36
pyproject.toml
Normal file
|
@ -0,0 +1,36 @@
|
|||
[tool.poetry]
|
||||
name = "matrix-webhook"
|
||||
version = "3.5.0"
|
||||
description = "Post a message to a matrix room with a simple HTTP POST"
|
||||
authors = ["Guilhem Saurel <guilhem.saurel@laas.fr>"]
|
||||
license = "BSD-2-Clause"
|
||||
readme = "README.md"
|
||||
homepage = "https://github.com/nim65s/matrix-webhook"
|
||||
repository = "https://github.com/nim65s/matrix-webhook.git"
|
||||
|
||||
[tool.poetry.urls]
|
||||
"changelog" = "https://github.com/nim65s/matrix-webhook/blob/master/CHANGELOG.md"
|
||||
|
||||
[tool.poetry.dependencies]
|
||||
python = "^3.8"
|
||||
Markdown = "^3.3.4"
|
||||
matrix-nio = ">=0.18.3,<0.21.0"
|
||||
PyYAML = "^6.0"
|
||||
|
||||
[tool.poetry.dev-dependencies]
|
||||
httpx = "^0.23.0"
|
||||
black = "^22.8.0"
|
||||
coverage = "^6.4.4"
|
||||
pydocstyle = "^6.1.1"
|
||||
flake8 = "^5.0.4"
|
||||
pyupgrade = "^2.31.0"
|
||||
|
||||
[tool.pydocstyle]
|
||||
ignore = ["D200", "D203", "D204", "D212"]
|
||||
|
||||
[build-system]
|
||||
requires = ["poetry-core>=1.0.0"]
|
||||
build-backend = "poetry.core.masonry.api"
|
||||
|
||||
[tool.poetry.scripts]
|
||||
matrix-webhook = "matrix_webhook.__main__:main"
|
12
test.yml
Normal file
12
test.yml
Normal file
|
@ -0,0 +1,12 @@
|
|||
version: '3'
|
||||
|
||||
services:
|
||||
tests:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: tests/Dockerfile
|
||||
entrypoint: ""
|
||||
env_file:
|
||||
- tests/.env
|
||||
volumes:
|
||||
- ./:/app
|
4
tests/.env
Normal file
4
tests/.env
Normal file
|
@ -0,0 +1,4 @@
|
|||
MATRIX_URL=http://tests
|
||||
MATRIX_ID=bot
|
||||
MATRIX_PW=pw
|
||||
API_KEY=ak
|
36
tests/Dockerfile
Normal file
36
tests/Dockerfile
Normal file
|
@ -0,0 +1,36 @@
|
|||
# Leverage a synapse base to be able to:
|
||||
# "from synapse._scripts.register_new_matrix_user import request_registration"
|
||||
FROM matrixdotorg/synapse
|
||||
|
||||
# The config dir defaults to /data which is a volume made to keep data.
|
||||
# Here, we want to trash those (and avoid the permission issues) by using something else
|
||||
ENV SYNAPSE_CONFIG_DIR=/srv SYNAPSE_DATA_DIR=/srv SYNAPSE_SERVER_NAME=tests SYNAPSE_REPORT_STATS=no
|
||||
|
||||
# Generate configuration and keys for synapse
|
||||
WORKDIR $SYNAPSE_CONFIG_DIR
|
||||
RUN chown -R 991:991 . \
|
||||
&& /start.py generate \
|
||||
&& sed -i 's=/data=/srv=;s=8008=80=;s=#sup=sup=;' homeserver.yaml \
|
||||
&& echo "" >> homeserver.yaml \
|
||||
&& echo "rc_message:" >> homeserver.yaml \
|
||||
&& echo " burst_count: 1000" >> homeserver.yaml \
|
||||
&& echo "rc_registration:" >> homeserver.yaml \
|
||||
&& echo " burst_count: 1000" >> homeserver.yaml \
|
||||
&& echo "rc_registration_token_validity:" >> homeserver.yaml \
|
||||
&& echo " burst_count: 1000" >> homeserver.yaml \
|
||||
&& echo "rc_login:" >> homeserver.yaml \
|
||||
&& echo " address:" >> homeserver.yaml \
|
||||
&& echo " burst_count: 1000" >> homeserver.yaml \
|
||||
&& echo " account:" >> homeserver.yaml \
|
||||
&& echo " burst_count: 1000" >> homeserver.yaml \
|
||||
&& echo " failed_attempts:" >> homeserver.yaml \
|
||||
&& echo " burst_count: 1000" >> homeserver.yaml \
|
||||
&& echo "rc_joins:" >> homeserver.yaml \
|
||||
&& echo " burst_count: 1000" >> homeserver.yaml \
|
||||
&& python -m synapse.app.homeserver --config-path homeserver.yaml --generate-keys
|
||||
|
||||
RUN pip install --no-cache-dir markdown matrix-nio httpx coverage
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
CMD ./tests/start.py -vvv
|
1
tests/__init__.py
Normal file
1
tests/__init__.py
Normal file
|
@ -0,0 +1 @@
|
|||
"""Make this directory a valid module for unittests autodiscover to work."""
|
1
tests/example_github_push.json
Normal file
1
tests/example_github_push.json
Normal file
File diff suppressed because one or more lines are too long
1
tests/example_gitlab_gchat.json
Normal file
1
tests/example_gitlab_gchat.json
Normal file
|
@ -0,0 +1 @@
|
|||
{"text":"John Doe pushed to branch \u003chttps://gitlab.com/jdoe/test/commits/master|master\u003e of \u003chttps://gitlab.com/jdoe/test|John Doe / test\u003e (\u003chttps://gitlab.com/jdoe/test/compare/b76004b20503d4d506e51a670de095cc063e4707...3517b06c64c9d349e2213650d6c009db0471361e|Compare changes\u003e)\n\u003chttps://gitlab.com/jdoe/test/-/commit/3517b06c64c9d349e2213650d6c009db0471361e|3517b06c\u003e: Merge branch 'prod' into 'master' - John Doe\n\n\u003chttps://gitlab.com/jdoe/test/-/commit/1f661795b220c5fe352f391eb8de3ac4fcc6fc1d|1f661795\u003e: Merge branch 'revert-a827b196' into 'prod' - John Doe\n\n\u003chttps://gitlab.com/jdoe/test/-/commit/b76004b20503d4d506e51a670de095cc063e4707|b76004b2\u003e: Merge branch 'revert-a827b196' into 'master' - John Doe"}
|
1
tests/example_gitlab_teams.json
Normal file
1
tests/example_gitlab_teams.json
Normal file
|
@ -0,0 +1 @@
|
|||
{"sections":[{"activityTitle":"John Doe pushed to branch [master](https://gitlab.com/jdoe/test/commits/master)","activitySubtitle":"in [John Doe / test](https://gitlab.com/jdoe/test)","activityText":"[Compare changes](https://gitlab.com/jdoe/test/compare/b76004b20503d4d506e51a670de095cc063e4707...3517b06c64c9d349e2213650d6c009db0471361e)","activityImage":"https://secure.gravatar.com/avatar/80\u0026d=identicon"},{"text":"[3517b06c](https://gitlab.com/jdoe/test/-/commit/3517b06c64c9d349e2213650d6c009db0471361e): Merge branch 'prod' into 'master' - John Doe\n\n[1f661795](https://gitlab.com/jdoe/test/-/commit/1f661795b220c5fe352f391eb8de3ac4fcc6fc1d): Merge branch 'revert-a827b196' into 'prod' - John Doe\n\n[b76004b2](https://gitlab.com/jdoe/test/-/commit/b76004b20503d4d506e51a670de095cc063e4707): Merge branch 'revert-a827b196' into 'master' - John Doe"}],"title":"John Doe / test","summary":"John Doe pushed to branch [master](https://gitlab.com/jdoe/test/commits/master) of [John Doe / test](https://gitlab.com/jdoe/test) ([Compare changes](https://gitlab.com/jdoe/test/compare/b76004b20503d4d506e51a670de095cc063e4707...3517b06c64c9d349e2213650d6c009db0471361e))"}
|
22
tests/example_grafana.json
Normal file
22
tests/example_grafana.json
Normal file
|
@ -0,0 +1,22 @@
|
|||
{
|
||||
"dashboardId":1,
|
||||
"evalMatches":[
|
||||
{
|
||||
"value":1,
|
||||
"metric":"Count",
|
||||
"tags":{}
|
||||
}
|
||||
],
|
||||
"imageUrl":"https://grafana.com/assets/img/blog/mixed_styles.png",
|
||||
"message":"Notification Message",
|
||||
"orgId":1,
|
||||
"panelId":2,
|
||||
"ruleId":1,
|
||||
"ruleName":"Panel Title alert",
|
||||
"ruleUrl":"http://localhost:3000/d/hZ7BuVbWz/test-dashboard?fullscreen\u0026edit\u0026tab=alert\u0026panelId=2\u0026orgId=1",
|
||||
"state":"alerting",
|
||||
"tags":{
|
||||
"tag name":"tag value"
|
||||
},
|
||||
"title":"[Alerting] Panel Title alert"
|
||||
}
|
41
tests/example_grafana_9x.json
Normal file
41
tests/example_grafana_9x.json
Normal file
|
@ -0,0 +1,41 @@
|
|||
{
|
||||
"receiver": "",
|
||||
"status": "firing",
|
||||
"alerts": [
|
||||
{
|
||||
"status": "firing",
|
||||
"labels": {
|
||||
"alertname": "TestAlert",
|
||||
"instance": "Grafana"
|
||||
},
|
||||
"annotations": {
|
||||
"summary": "Notification test"
|
||||
},
|
||||
"startsAt": "2022-09-07T15:00:26.722304913+02:00",
|
||||
"endsAt": "0001-01-01T00:00:00Z",
|
||||
"generatorURL": "",
|
||||
"fingerprint": "57c6d9296de2ad39",
|
||||
"silenceURL": "https://grafana.example.com/alerting/silence/new?alertmanager=grafana&matcher=alertname%3DTestAlert&matcher=instance%3DGrafana",
|
||||
"dashboardURL": "",
|
||||
"panelURL": "",
|
||||
"valueString": "[ metric='foo' labels={instance=bar} value=10 ]"
|
||||
}
|
||||
],
|
||||
"groupLabels": {},
|
||||
"commonLabels": {
|
||||
"alertname": "TestAlert",
|
||||
"instance": "Grafana"
|
||||
},
|
||||
"commonAnnotations": {
|
||||
"summary": "Notification test"
|
||||
},
|
||||
"externalURL": "https://grafana.example.com/",
|
||||
"version": "1",
|
||||
"groupKey": "{alertname=\"TestAlert\", instance=\"Grafana\"}2022-09-07 15:00:26.722304913 +0200 CEST m=+246580.963796811",
|
||||
"truncatedAlerts": 0,
|
||||
"orgId": 1,
|
||||
"title": "[FIRING:1] (TestAlert Grafana)",
|
||||
"state": "alerting",
|
||||
"message": "**Firing**\n\nValue: [ metric='foo' labels={instance=bar} value=10 ]\nLabels:\n - alertname = TestAlert\n - instance = Grafana\nAnnotations:\n - summary = Notification test\nSilence: https://grafana.example.com/alerting/silence/new?alertmanager=grafana&matcher=alertname%3DTestAlert&matcher=instance%3DGrafana\n",
|
||||
"key": "ak"
|
||||
}
|
33
tests/example_pingdom_dns.json
Normal file
33
tests/example_pingdom_dns.json
Normal file
|
@ -0,0 +1,33 @@
|
|||
{
|
||||
"check_id": 12345,
|
||||
"check_name": "Pingdom-dns build test",
|
||||
"check_type": "DNS",
|
||||
"check_params": {
|
||||
"hostname": "www.example.com",
|
||||
"basic_auth": false,
|
||||
"expected_ip": "123.4.5.6",
|
||||
"ipv6": false,
|
||||
"nameserver": "example.com"
|
||||
},
|
||||
"tags": [
|
||||
"example_tag"
|
||||
],
|
||||
"previous_state": "UP",
|
||||
"current_state": "DOWN",
|
||||
"importance_level": "HIGH",
|
||||
"state_changed_timestamp": 1451610061,
|
||||
"state_changed_utc_time": "2016-01-01T01:01:01",
|
||||
"long_description": "Long error message",
|
||||
"description": "Short error message",
|
||||
"first_probe": {
|
||||
"ip": "123.4.5.6",
|
||||
"ipv6": "2001:4800:1020:209::5",
|
||||
"location": "Stockholm, Sweden"
|
||||
},
|
||||
"second_probe": {
|
||||
"ip": "123.4.5.6",
|
||||
"ipv6": "2001:4800:1020:209::5",
|
||||
"location": "Austin, US",
|
||||
"version": 1
|
||||
}
|
||||
}
|
36
tests/example_pingdom_http.json
Normal file
36
tests/example_pingdom_http.json
Normal file
|
@ -0,0 +1,36 @@
|
|||
{
|
||||
"check_id": 12345,
|
||||
"check_name": "Pingdom-http build test",
|
||||
"check_type": "HTTP",
|
||||
"check_params": {
|
||||
"basic_auth": false,
|
||||
"encryption": true,
|
||||
"full_url": "https://www.example.com/path",
|
||||
"header": "User-Agent:Pingdom.com_bot",
|
||||
"hostname": "www.example.com",
|
||||
"ipv6": false,
|
||||
"port": 443,
|
||||
"url": "/path"
|
||||
},
|
||||
"tags": [
|
||||
"example_tag"
|
||||
],
|
||||
"previous_state": "UP",
|
||||
"current_state": "DOWN",
|
||||
"importance_level": "HIGH",
|
||||
"state_changed_timestamp": 1451610061,
|
||||
"state_changed_utc_time": "2016-01-01T01:01:01",
|
||||
"long_description": "Long error message",
|
||||
"description": "Short error message",
|
||||
"first_probe": {
|
||||
"ip": "123.4.5.6",
|
||||
"ipv6": "2001:4800:1020:209::5",
|
||||
"location": "Stockholm, Sweden"
|
||||
},
|
||||
"second_probe": {
|
||||
"ip": "123.4.5.6",
|
||||
"ipv6": "2001:4800:1020:209::5",
|
||||
"location": "Austin, US",
|
||||
"version": 1
|
||||
}
|
||||
}
|
34
tests/example_pingdom_tcp.json
Normal file
34
tests/example_pingdom_tcp.json
Normal file
|
@ -0,0 +1,34 @@
|
|||
{
|
||||
"body":{
|
||||
"check_id": 12345,
|
||||
"check_name": "Pingdom-tcp build test",
|
||||
"check_type": "PORT_TCP",
|
||||
"check_params": {
|
||||
"hostname": "www.example.com",
|
||||
"basic_auth": false,
|
||||
"ipv6": false,
|
||||
"port": 80
|
||||
},
|
||||
"tags": [
|
||||
"example_tag"
|
||||
],
|
||||
"previous_state": "UP",
|
||||
"current_state": "DOWN",
|
||||
"importance_level": "HIGH",
|
||||
"state_changed_timestamp": 1451610061,
|
||||
"state_changed_utc_time": "2016-01-01T01:01:01",
|
||||
"long_description": "Long error message",
|
||||
"description": "Short error message",
|
||||
"first_probe": {
|
||||
"ip": "123.4.5.6",
|
||||
"ipv6": "2001:4800:1020:209::5",
|
||||
"location": "Stockholm, Sweden"
|
||||
},
|
||||
"second_probe": {
|
||||
"ip": "123.4.5.6",
|
||||
"ipv6": "2001:4800:1020:209::5",
|
||||
"location": "Austin, US",
|
||||
"version": 1
|
||||
}
|
||||
}
|
||||
}
|
122
tests/start.py
Executable file
122
tests/start.py
Executable file
|
@ -0,0 +1,122 @@
|
|||
#!/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
|
||||
from unittest import main
|
||||
|
||||
import httpx
|
||||
import yaml
|
||||
from synapse._scripts.register_new_matrix_user import request_registration
|
||||
|
||||
BOT_URL = "http://localhost:4785"
|
||||
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,
|
||||
params=None,
|
||||
key_as_param=False,
|
||||
room_as_parameter=False,
|
||||
):
|
||||
"""Bot requests boilerplate."""
|
||||
if params is None:
|
||||
params = {}
|
||||
if key is not None:
|
||||
if key_as_param:
|
||||
params["key"] = key
|
||||
else:
|
||||
req["key"] = key
|
||||
if room_as_parameter:
|
||||
params["room_id"] = room_id
|
||||
url = BOT_URL if room_id is None or room_as_parameter else f"{BOT_URL}/{room_id}"
|
||||
return httpx.post(url, params=params, json=req).json()
|
||||
|
||||
|
||||
def wait_available(url: str, key: str, timeout: int = 10) -> bool:
|
||||
"""Wait until a service answer correctly or timeout."""
|
||||
|
||||
def check_json(url: str, key: str) -> bool:
|
||||
"""
|
||||
Ensure a service at a given url answers with valid json including a certain key.
|
||||
"""
|
||||
try:
|
||||
data = httpx.get(url).json()
|
||||
return key in data
|
||||
except httpx.ConnectError:
|
||||
return False
|
||||
|
||||
start = time()
|
||||
while True:
|
||||
if check_json(url, key):
|
||||
return True
|
||||
if time() > start + timeout:
|
||||
return False
|
||||
|
||||
|
||||
def run_and_test():
|
||||
"""Launch the bot and its tests."""
|
||||
# Start the server, and wait for it
|
||||
LOGGER.info("Spawning synapse")
|
||||
srv = Popen(
|
||||
[
|
||||
"python",
|
||||
"-m",
|
||||
"synapse.app.homeserver",
|
||||
"--config-path",
|
||||
"/srv/homeserver.yaml",
|
||||
]
|
||||
)
|
||||
if not wait_available(f"{MATRIX_URL}/_matrix/client/r0/login", "flows"):
|
||||
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
|
||||
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({'data': '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())
|
130
tests/test_github.py
Normal file
130
tests/test_github.py
Normal file
|
@ -0,0 +1,130 @@
|
|||
"""Test module for grafana formatter."""
|
||||
|
||||
import unittest
|
||||
|
||||
import httpx
|
||||
import nio
|
||||
|
||||
from .start import BOT_URL, FULL_ID, MATRIX_ID, MATRIX_PW, MATRIX_URL
|
||||
|
||||
SHA256 = "fd7522672889385736be8ffc86d1f8de2e15668864f49af729b5c63e5e0698c4"
|
||||
|
||||
|
||||
def headers(sha256=SHA256, event="push"):
|
||||
"""Mock headers from github webhooks."""
|
||||
return {
|
||||
# 'Request URL': 'https://bot.saurel.me/room?formatter=github',
|
||||
# 'Request method': 'POST',
|
||||
"Accept": "*/*",
|
||||
"content-type": "application/json",
|
||||
"User-Agent": "GitHub-Hookshot/8d33975",
|
||||
"X-GitHub-Delivery": "636b9b1c-0761-11ec-8a8a-5e435c5ac4f4",
|
||||
"X-GitHub-Event": event,
|
||||
"X-GitHub-Hook-ID": "311845633",
|
||||
"X-GitHub-Hook-Installation-Target-ID": "171114171",
|
||||
"X-GitHub-Hook-Installation-Target-Type": "repository",
|
||||
"X-Hub-Signature": "sha1=ea68fdfcb2f328aaa8f50d176f355e5d4fc95d94",
|
||||
"X-Hub-Signature-256": f"sha256={sha256}",
|
||||
}
|
||||
|
||||
|
||||
class GithubFormatterTest(unittest.IsolatedAsyncioTestCase):
|
||||
"""Github formatter test class."""
|
||||
|
||||
async def test_github_notification(self):
|
||||
"""Send a mock github webhook, and check the result."""
|
||||
messages = []
|
||||
client = nio.AsyncClient(MATRIX_URL, MATRIX_ID)
|
||||
|
||||
await client.login(MATRIX_PW)
|
||||
room = await client.room_create()
|
||||
|
||||
with open("tests/example_github_push.json", "rb") as f:
|
||||
example_github_push = f.read().strip()
|
||||
self.assertEqual(
|
||||
httpx.post(
|
||||
f"{BOT_URL}/{room.room_id}",
|
||||
params={
|
||||
"formatter": "github",
|
||||
},
|
||||
content=example_github_push,
|
||||
headers=headers(event="something else"),
|
||||
).json(),
|
||||
{"status": 200, "ret": "OK"},
|
||||
)
|
||||
|
||||
sync = await client.sync()
|
||||
messages = await client.room_messages(room.room_id, sync.next_batch)
|
||||
await client.close()
|
||||
|
||||
message = messages.chunk[0]
|
||||
self.assertEqual(message.sender, FULL_ID)
|
||||
self.assertEqual(
|
||||
message.formatted_body,
|
||||
"<p>notification from github</p>",
|
||||
)
|
||||
|
||||
async def test_github_push(self):
|
||||
"""Send a mock github push webhook, and check the result."""
|
||||
messages = []
|
||||
client = nio.AsyncClient(MATRIX_URL, MATRIX_ID)
|
||||
|
||||
await client.login(MATRIX_PW)
|
||||
room = await client.room_create()
|
||||
|
||||
with open("tests/example_github_push.json", "rb") as f:
|
||||
example_github_push = f.read().strip()
|
||||
self.assertEqual(
|
||||
httpx.post(
|
||||
f"{BOT_URL}/{room.room_id}",
|
||||
params={
|
||||
"formatter": "github",
|
||||
},
|
||||
content=example_github_push,
|
||||
headers=headers(),
|
||||
).json(),
|
||||
{"status": 200, "ret": "OK"},
|
||||
)
|
||||
|
||||
sync = await client.sync()
|
||||
messages = await client.room_messages(room.room_id, sync.next_batch)
|
||||
await client.close()
|
||||
|
||||
before = "ac7d1d9647008145e9d0cf65d24744d0db4862b8"
|
||||
after = "4bcdb25c809391baaabc264d9309059f9f48ead2"
|
||||
GH = "https://github.com"
|
||||
expected = f'<p><a href="{GH}/nim65s">@nim65s</a> pushed on refs/heads/devel: '
|
||||
expected += f'<a href="{GH}/nim65s/matrix-webhook/compare/ac7d1d964700...'
|
||||
expected += f'4bcdb25c8093">{before} → {after}</a>:</p>\n<ul>\n<li>'
|
||||
expected += f'<a href="{GH}/nim65s/matrix-webhook/commit/{after}">'
|
||||
expected += "formatters: also get headers</a></li>\n</ul>"
|
||||
|
||||
message = messages.chunk[0]
|
||||
self.assertEqual(message.sender, FULL_ID)
|
||||
self.assertEqual(
|
||||
message.formatted_body,
|
||||
expected,
|
||||
)
|
||||
|
||||
async def test_github_wrong_digest(self):
|
||||
"""Send a mock github push webhook with a wrong digest."""
|
||||
client = nio.AsyncClient(MATRIX_URL, MATRIX_ID)
|
||||
|
||||
await client.login(MATRIX_PW)
|
||||
room = await client.room_create()
|
||||
|
||||
with open("tests/example_github_push.json", "rb") as f:
|
||||
example_github_push = f.read().strip()
|
||||
|
||||
self.assertEqual(
|
||||
httpx.post(
|
||||
f"{BOT_URL}/{room.room_id}",
|
||||
params={
|
||||
"formatter": "github",
|
||||
},
|
||||
content=example_github_push,
|
||||
headers=headers("wrong digest"),
|
||||
).json(),
|
||||
{"status": 401, "ret": "Invalid SHA-256 HMAC digest"},
|
||||
)
|
||||
await client.close()
|
54
tests/test_gitlab_gchat.py
Normal file
54
tests/test_gitlab_gchat.py
Normal file
|
@ -0,0 +1,54 @@
|
|||
"""
|
||||
Test module for gitlab "google chat" formatter.
|
||||
|
||||
"""
|
||||
|
||||
import unittest
|
||||
|
||||
import httpx
|
||||
import nio
|
||||
|
||||
from .start import BOT_URL, FULL_ID, KEY, MATRIX_ID, MATRIX_PW, MATRIX_URL
|
||||
|
||||
|
||||
class GitlabGchatFormatterTest(unittest.IsolatedAsyncioTestCase):
|
||||
"""Gitlab "google chat" formatter test class."""
|
||||
|
||||
async def test_gitlab_gchat_body(self):
|
||||
"""Send a markdown message, and check the result."""
|
||||
messages = []
|
||||
client = nio.AsyncClient(MATRIX_URL, MATRIX_ID)
|
||||
|
||||
await client.login(MATRIX_PW)
|
||||
room = await client.room_create()
|
||||
|
||||
with open("tests/example_gitlab_gchat.json") as f:
|
||||
example_gitlab_gchat_request = f.read()
|
||||
self.assertEqual(
|
||||
httpx.post(
|
||||
f"{BOT_URL}/{room.room_id}",
|
||||
params={"formatter": "gitlab_gchat", "key": KEY},
|
||||
content=example_gitlab_gchat_request,
|
||||
).json(),
|
||||
{"status": 200, "ret": "OK"},
|
||||
)
|
||||
|
||||
sync = await client.sync()
|
||||
messages = await client.room_messages(room.room_id, sync.next_batch)
|
||||
await client.close()
|
||||
|
||||
message = messages.chunk[0]
|
||||
self.assertEqual(message.sender, FULL_ID)
|
||||
self.assertEqual(
|
||||
message.body,
|
||||
"John Doe pushed to branch [master](https://gitlab.com/jdoe/test/commits/m"
|
||||
+ "aster) of [John Doe / test](https://gitlab.com/jdoe/test) ([Compare chan"
|
||||
+ "ges](https://gitlab.com/jdoe/test/compare/b76004b20503d4d506e51a670de095"
|
||||
+ "cc063e4707...3517b06c64c9d349e2213650d6c009db0471361e))\n[3517b06c](http"
|
||||
+ "s://gitlab.com/jdoe/test/-/commit/3517b06c64c9d349e2213650d6c009db047136"
|
||||
+ "1e): Merge branch 'prod' into 'master' - John Doe\n\n[1f661795](https://"
|
||||
+ "gitlab.com/jdoe/test/-/commit/1f661795b220c5fe352f391eb8de3ac4fcc6fc1d):"
|
||||
+ " Merge branch 'revert-a827b196' into 'prod' - John Doe\n\n[b76004b2](htt"
|
||||
+ "ps://gitlab.com/jdoe/test/-/commit/b76004b20503d4d506e51a670de095cc063e4"
|
||||
+ "707): Merge branch 'revert-a827b196' into 'master' - John Doe",
|
||||
)
|
55
tests/test_gitlab_teams.py
Normal file
55
tests/test_gitlab_teams.py
Normal file
|
@ -0,0 +1,55 @@
|
|||
"""
|
||||
Test module for gitlab "teams" formatter.
|
||||
|
||||
"""
|
||||
|
||||
import unittest
|
||||
|
||||
import httpx
|
||||
import nio
|
||||
|
||||
from .start import BOT_URL, FULL_ID, KEY, MATRIX_ID, MATRIX_PW, MATRIX_URL
|
||||
|
||||
|
||||
class GitlabTeamsFormatterTest(unittest.IsolatedAsyncioTestCase):
|
||||
"""Gitlab "teams" formatter test class."""
|
||||
|
||||
async def test_gitlab_teams_body(self):
|
||||
"""Send a markdown message, and check the result."""
|
||||
messages = []
|
||||
client = nio.AsyncClient(MATRIX_URL, MATRIX_ID)
|
||||
|
||||
await client.login(MATRIX_PW)
|
||||
room = await client.room_create()
|
||||
|
||||
with open("tests/example_gitlab_teams.json") as f:
|
||||
example_gitlab_teams_request = f.read()
|
||||
self.assertEqual(
|
||||
httpx.post(
|
||||
f"{BOT_URL}/{room.room_id}",
|
||||
params={"formatter": "gitlab_teams", "key": KEY},
|
||||
content=example_gitlab_teams_request,
|
||||
).json(),
|
||||
{"status": 200, "ret": "OK"},
|
||||
)
|
||||
|
||||
sync = await client.sync()
|
||||
messages = await client.room_messages(room.room_id, sync.next_batch)
|
||||
await client.close()
|
||||
|
||||
message = messages.chunk[0]
|
||||
self.assertEqual(message.sender, FULL_ID)
|
||||
self.assertEqual(
|
||||
message.body,
|
||||
"John Doe pushed to branch [master](https://gitlab.com/jdoe/test/commits"
|
||||
+ "/master) in [John Doe / test](https://gitlab.com/jdoe/test) \u2192 [Com"
|
||||
+ "pare changes](https://gitlab.com/jdoe/test/compare/b76004b20503d4d506e5"
|
||||
+ "1a670de095cc063e4707...3517b06c64c9d349e2213650d6c009db0471361e) \n\n*"
|
||||
+ " [3517b06c](https://gitlab.com/jdoe/test/-/commit/3517b06c64c9d349e2213"
|
||||
+ "650d6c009db0471361e): Merge branch 'prod' into 'master' - John Doe \n*"
|
||||
+ " [1f661795](https://gitlab.com/jdoe/test/-/commit/1f661795b220c5fe352f3"
|
||||
+ "91eb8de3ac4fcc6fc1d): Merge branch 'revert-a827b196' into 'prod' - John"
|
||||
+ " Doe \n* [b76004b2](https://gitlab.com/jdoe/test/-/commit/b76004b20503"
|
||||
+ "d4d506e51a670de095cc063e4707): Merge branch 'revert-a827b196' into 'mas"
|
||||
+ "ter' - John Doe",
|
||||
)
|
46
tests/test_grafana.py
Normal file
46
tests/test_grafana.py
Normal file
|
@ -0,0 +1,46 @@
|
|||
"""
|
||||
Test module for grafana formatter.
|
||||
|
||||
ref https://grafana.com/docs/grafana/latest/alerting/old-alerting/notifications/#webhook
|
||||
"""
|
||||
|
||||
import unittest
|
||||
|
||||
import httpx
|
||||
import nio
|
||||
|
||||
from .start import BOT_URL, FULL_ID, KEY, MATRIX_ID, MATRIX_PW, MATRIX_URL
|
||||
|
||||
|
||||
class GrafanaFormatterTest(unittest.IsolatedAsyncioTestCase):
|
||||
"""Grafana formatter test class."""
|
||||
|
||||
async def test_grafana_body(self):
|
||||
"""Send a markdown message, and check the result."""
|
||||
messages = []
|
||||
client = nio.AsyncClient(MATRIX_URL, MATRIX_ID)
|
||||
|
||||
await client.login(MATRIX_PW)
|
||||
room = await client.room_create()
|
||||
|
||||
with open("tests/example_grafana.json") as f:
|
||||
example_grafana_request = f.read()
|
||||
self.assertEqual(
|
||||
httpx.post(
|
||||
f"{BOT_URL}/{room.room_id}",
|
||||
params={"formatter": "grafana", "key": KEY},
|
||||
content=example_grafana_request,
|
||||
).json(),
|
||||
{"status": 200, "ret": "OK"},
|
||||
)
|
||||
|
||||
sync = await client.sync()
|
||||
messages = await client.room_messages(room.room_id, sync.next_batch)
|
||||
await client.close()
|
||||
|
||||
message = messages.chunk[0]
|
||||
self.assertEqual(message.sender, FULL_ID)
|
||||
self.assertEqual(
|
||||
message.body,
|
||||
"#### [Alerting] Panel Title alert\nNotification Message\n\n* Count: 1\n",
|
||||
)
|
51
tests/test_grafana_9x.py
Normal file
51
tests/test_grafana_9x.py
Normal file
|
@ -0,0 +1,51 @@
|
|||
"""
|
||||
Test module for grafana v9 formatter.
|
||||
|
||||
ref https://grafana.com/docs/grafana/latest/alerting/old-alerting/notifications/#webhook
|
||||
"""
|
||||
|
||||
import unittest
|
||||
|
||||
import httpx
|
||||
import nio
|
||||
|
||||
from .start import BOT_URL, FULL_ID, MATRIX_ID, MATRIX_PW, MATRIX_URL
|
||||
|
||||
|
||||
class Grafana9xFormatterTest(unittest.IsolatedAsyncioTestCase):
|
||||
"""Grafana formatter test class."""
|
||||
|
||||
async def test_grafana_body(self):
|
||||
"""Send a markdown message, and check the result."""
|
||||
messages = []
|
||||
client = nio.AsyncClient(MATRIX_URL, MATRIX_ID)
|
||||
|
||||
await client.login(MATRIX_PW)
|
||||
room = await client.room_create()
|
||||
|
||||
with open("tests/example_grafana_9x.json") as f:
|
||||
example_grafana_request = f.read()
|
||||
self.assertEqual(
|
||||
httpx.post(
|
||||
f"{BOT_URL}/{room.room_id}",
|
||||
params={"formatter": "grafana_9x"},
|
||||
content=example_grafana_request,
|
||||
).json(),
|
||||
{"status": 200, "ret": "OK"},
|
||||
)
|
||||
|
||||
sync = await client.sync()
|
||||
messages = await client.room_messages(room.room_id, sync.next_batch)
|
||||
await client.close()
|
||||
|
||||
message = messages.chunk[0]
|
||||
self.assertEqual(message.sender, FULL_ID)
|
||||
expected_body = (
|
||||
"#### [FIRING:1] (TestAlert Grafana)\n**Firing**\n\n\n\nValue: [ metr"
|
||||
"ic='foo' labels={instance=bar} value=10 ]\n\nLabels:\n\n - alertname "
|
||||
"= TestAlert\n\n - instance = Grafana\n\nAnnotations:\n\n - summary = "
|
||||
"Notification test\n\nSilence: https://grafana.example.com/alerting/si"
|
||||
"lence/new?alertmanager=grafana&matcher=alertname%3DTestAlert&matcher="
|
||||
"instance%3DGrafana\n\n\n\n"
|
||||
)
|
||||
self.assertEqual(message.body, expected_body)
|
51
tests/test_grafana_forward.py
Normal file
51
tests/test_grafana_forward.py
Normal file
|
@ -0,0 +1,51 @@
|
|||
"""
|
||||
Test version 9 compatibility of grafana formatter.
|
||||
|
||||
ref https://grafana.com/docs/grafana/latest/alerting/old-alerting/notifications/#webhook
|
||||
"""
|
||||
|
||||
import unittest
|
||||
|
||||
import httpx
|
||||
import nio
|
||||
|
||||
from .start import BOT_URL, FULL_ID, MATRIX_ID, MATRIX_PW, MATRIX_URL
|
||||
|
||||
|
||||
class GrafanaForwardFormatterTest(unittest.IsolatedAsyncioTestCase):
|
||||
"""Grafana formatter test class."""
|
||||
|
||||
async def test_grafana_body(self):
|
||||
"""Send a markdown message, and check the result."""
|
||||
messages = []
|
||||
client = nio.AsyncClient(MATRIX_URL, MATRIX_ID)
|
||||
|
||||
await client.login(MATRIX_PW)
|
||||
room = await client.room_create()
|
||||
|
||||
with open("tests/example_grafana_9x.json") as f:
|
||||
example_grafana_request = f.read()
|
||||
self.assertEqual(
|
||||
httpx.post(
|
||||
f"{BOT_URL}/{room.room_id}",
|
||||
params={"formatter": "grafana"},
|
||||
content=example_grafana_request,
|
||||
).json(),
|
||||
{"status": 200, "ret": "OK"},
|
||||
)
|
||||
|
||||
sync = await client.sync()
|
||||
messages = await client.room_messages(room.room_id, sync.next_batch)
|
||||
await client.close()
|
||||
|
||||
message = messages.chunk[0]
|
||||
self.assertEqual(message.sender, FULL_ID)
|
||||
expected_body = (
|
||||
"#### [FIRING:1] (TestAlert Grafana)\n**Firing**\n\n\n\nValue: [ metr"
|
||||
"ic='foo' labels={instance=bar} value=10 ]\n\nLabels:\n\n - alertname "
|
||||
"= TestAlert\n\n - instance = Grafana\n\nAnnotations:\n\n - summary = "
|
||||
"Notification test\n\nSilence: https://grafana.example.com/alerting/si"
|
||||
"lence/new?alertmanager=grafana&matcher=alertname%3DTestAlert&matcher="
|
||||
"instance%3DGrafana\n\n\n\n"
|
||||
)
|
||||
self.assertEqual(message.body, expected_body)
|
44
tests/test_pingdom.py
Normal file
44
tests/test_pingdom.py
Normal file
|
@ -0,0 +1,44 @@
|
|||
"""
|
||||
Test module for pingdom formatter.
|
||||
"""
|
||||
|
||||
import unittest
|
||||
|
||||
import httpx
|
||||
import nio
|
||||
|
||||
from .start import BOT_URL, FULL_ID, KEY, MATRIX_ID, MATRIX_PW, MATRIX_URL
|
||||
|
||||
|
||||
class PingdomFormatterTest(unittest.IsolatedAsyncioTestCase):
|
||||
"""Grafana formatter test class."""
|
||||
|
||||
async def test_pingdom_http_body(self):
|
||||
"""Send a markdown message, and check the result."""
|
||||
messages = []
|
||||
client = nio.AsyncClient(MATRIX_URL, MATRIX_ID)
|
||||
|
||||
await client.login(MATRIX_PW)
|
||||
room = await client.room_create()
|
||||
|
||||
with open("tests/example_pingdom_http.json") as f:
|
||||
example_pingdom_request = f.read()
|
||||
self.assertEqual(
|
||||
httpx.post(
|
||||
f"{BOT_URL}/{room.room_id}",
|
||||
params={"formatter": "pingdom", "key": KEY},
|
||||
content=example_pingdom_request,
|
||||
).json(),
|
||||
{"status": 200, "ret": "OK"},
|
||||
)
|
||||
|
||||
sync = await client.sync()
|
||||
messages = await client.room_messages(room.room_id, sync.next_batch)
|
||||
await client.close()
|
||||
|
||||
message = messages.chunk[0]
|
||||
self.assertEqual(message.sender, FULL_ID)
|
||||
self.assertEqual(
|
||||
message.body,
|
||||
"#### [Alerting] Panel Title alert\nNotification Message\n\n* Count: 1\n",
|
||||
)
|
169
tests/tests.py
Normal file
169
tests/tests.py
Normal file
|
@ -0,0 +1,169 @@
|
|||
"""Main test module."""
|
||||
|
||||
import unittest
|
||||
|
||||
import nio
|
||||
|
||||
from .start import FULL_ID, KEY, MATRIX_ID, MATRIX_PW, MATRIX_URL, bot_req
|
||||
|
||||
|
||||
class BotTest(unittest.IsolatedAsyncioTestCase):
|
||||
"""Main test class."""
|
||||
|
||||
def test_errors(self):
|
||||
"""Check the bot's error paths."""
|
||||
self.assertEqual(bot_req(), {"status": 400, "ret": "Invalid JSON"})
|
||||
self.assertEqual(
|
||||
bot_req({"toto": 3}),
|
||||
{"status": 400, "ret": "Missing body, key, room_id"},
|
||||
)
|
||||
self.assertEqual(
|
||||
bot_req({"body": 3}, "wrong_key", "wrong_room"),
|
||||
{"status": 401, "ret": "Invalid API key"},
|
||||
)
|
||||
self.assertEqual(
|
||||
bot_req({"body": 3}, "wrong_key", "wrong_room", key_as_param=True),
|
||||
{"status": 401, "ret": "Invalid API key"},
|
||||
)
|
||||
self.assertEqual(
|
||||
bot_req({"body": 3}, KEY, params={"formatter": "wrong_formatter"}),
|
||||
{"status": 400, "ret": "Unknown formatter"},
|
||||
)
|
||||
# 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({"body": 3}, KEY, "wrong_room"),
|
||||
{"status": 400, "ret": "wrong_room was not legal room ID or room alias"},
|
||||
)
|
||||
self.assertEqual(
|
||||
bot_req({"body": 3}, KEY, "wrong_room", key_as_param=True),
|
||||
{"status": 400, "ret": "wrong_room was not legal room ID or room alias"},
|
||||
)
|
||||
|
||||
async def test_message(self):
|
||||
"""Send a markdown message with the old format, and check the result."""
|
||||
text = "# Hello"
|
||||
messages = []
|
||||
client = nio.AsyncClient(MATRIX_URL, MATRIX_ID)
|
||||
|
||||
await client.login(MATRIX_PW)
|
||||
room = await client.room_create()
|
||||
|
||||
self.assertEqual(
|
||||
bot_req({"text": text}, KEY, room.room_id), {"status": 200, "ret": "OK"}
|
||||
)
|
||||
|
||||
sync = await client.sync()
|
||||
messages = await client.room_messages(room.room_id, sync.next_batch)
|
||||
await client.close()
|
||||
|
||||
message = messages.chunk[0]
|
||||
self.assertEqual(message.sender, FULL_ID)
|
||||
self.assertEqual(message.body, text)
|
||||
self.assertEqual(message.formatted_body, "<h1>Hello</h1>")
|
||||
|
||||
async def test_room_id_req(self):
|
||||
"""Send a markdown message in a room given as data, and check the result."""
|
||||
body = "# Hello"
|
||||
messages = []
|
||||
client = nio.AsyncClient(MATRIX_URL, MATRIX_ID)
|
||||
|
||||
await client.login(MATRIX_PW)
|
||||
room = await client.room_create()
|
||||
|
||||
self.assertEqual(
|
||||
bot_req({"body": body, "room_id": room.room_id}, KEY, room.room_id),
|
||||
{"status": 200, "ret": "OK"},
|
||||
)
|
||||
|
||||
sync = await client.sync()
|
||||
messages = await client.room_messages(room.room_id, sync.next_batch)
|
||||
await client.close()
|
||||
|
||||
message = messages.chunk[0]
|
||||
self.assertEqual(message.sender, FULL_ID)
|
||||
self.assertEqual(message.body, body)
|
||||
self.assertEqual(message.formatted_body, "<h1>Hello</h1>")
|
||||
|
||||
async def test_room_id_parameter(self):
|
||||
"""Send a markdown message in a room given as parameter."""
|
||||
body = "# Hello"
|
||||
messages = []
|
||||
client = nio.AsyncClient(MATRIX_URL, MATRIX_ID)
|
||||
|
||||
await client.login(MATRIX_PW)
|
||||
room = await client.room_create()
|
||||
|
||||
self.assertEqual(
|
||||
bot_req({"body": body}, KEY, room.room_id, room_as_parameter=True),
|
||||
{"status": 200, "ret": "OK"},
|
||||
)
|
||||
|
||||
sync = await client.sync()
|
||||
messages = await client.room_messages(room.room_id, sync.next_batch)
|
||||
await client.close()
|
||||
|
||||
message = messages.chunk[0]
|
||||
self.assertEqual(message.sender, FULL_ID)
|
||||
self.assertEqual(message.body, body)
|
||||
self.assertEqual(message.formatted_body, "<h1>Hello</h1>")
|
||||
|
||||
async def test_markdown_body(self):
|
||||
"""Send a markdown message, and check the result."""
|
||||
body = "# Hello"
|
||||
messages = []
|
||||
client = nio.AsyncClient(MATRIX_URL, MATRIX_ID)
|
||||
|
||||
await client.login(MATRIX_PW)
|
||||
room = await client.room_create()
|
||||
|
||||
self.assertEqual(
|
||||
bot_req({"body": body}, KEY, room.room_id), {"status": 200, "ret": "OK"}
|
||||
)
|
||||
|
||||
sync = await client.sync()
|
||||
messages = await client.room_messages(room.room_id, sync.next_batch)
|
||||
await client.close()
|
||||
|
||||
message = messages.chunk[0]
|
||||
self.assertEqual(message.sender, FULL_ID)
|
||||
self.assertEqual(message.body, body)
|
||||
self.assertEqual(message.formatted_body, "<h1>Hello</h1>")
|
||||
|
||||
async def test_formatted_body(self):
|
||||
"""Send a formatted message, and check the result."""
|
||||
body = "Formatted message"
|
||||
formatted_body = "<del>markdown</del><strong>Formatted</strong> message"
|
||||
messages = []
|
||||
client = nio.AsyncClient(MATRIX_URL, MATRIX_ID)
|
||||
|
||||
await client.login(MATRIX_PW)
|
||||
room = await client.room_create()
|
||||
|
||||
self.assertEqual(
|
||||
bot_req(
|
||||
{"body": body, "formatted_body": formatted_body}, KEY, room.room_id
|
||||
),
|
||||
{"status": 200, "ret": "OK"},
|
||||
)
|
||||
|
||||
sync = await client.sync()
|
||||
messages = await client.room_messages(room.room_id, sync.next_batch)
|
||||
await client.close()
|
||||
|
||||
message = messages.chunk[0]
|
||||
self.assertEqual(message.sender, FULL_ID)
|
||||
self.assertEqual(message.body, body)
|
||||
self.assertEqual(message.formatted_body, formatted_body)
|
||||
|
||||
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({"body": "Re"}, KEY, room.room_id),
|
||||
{"status": 200, "ret": "OK"},
|
||||
)
|
Loading…
Add table
Add a link
Reference in a new issue