Compare commits

..

1 commit

Author SHA1 Message Date
Guilhem Saurel
88ec57877c v3.1.0 2021-07-18 22:56:27 +02:00
50 changed files with 736 additions and 4596 deletions

View file

@ -1,2 +0,0 @@
[flake8]
max-line-length = 88

28
.github/workflows/docker-hub.yml vendored Normal file
View file

@ -0,0 +1,28 @@
name: Publish
on:
push:
branches:
- 'master'
tags:
- 'v*'
jobs:
docker-hub:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: docker/metadata-action@v3
id: meta
with:
images: nim65s/matrix-webhook
- uses: docker/login-action@v1
with:
username: nim65s
password: ${{ secrets.DOCKERHUB_TOKEN }}
- uses: docker/build-push-action@v2
with:
context: .
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}

8
.github/workflows/lint.yml vendored Normal file
View file

@ -0,0 +1,8 @@
name: Lints
on: [push, pull_request]
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: psf/black@stable

15
.github/workflows/pypi.yml vendored Normal file
View file

@ -0,0 +1,15 @@
name: PyPI
on:
push:
tags:
- 'v*'
jobs:
pypi:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- run: pip install -U poetry twine wheel
- run: poetry build
- run: twine upload --non-interactive -u __token__ -p ${{ secrets.PYPI_TOKEN }} dist/*

9
.github/workflows/test.yml vendored Normal file
View file

@ -0,0 +1,9 @@
name: Tests
on: [push, pull_request]
jobs:
tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- run: docker-compose -f test.yml up --exit-code-from tests
- uses: codecov/codecov-action@v1

2
.gitignore vendored
View file

@ -3,5 +3,3 @@
.mypy_cache
coverage.xml
htmlcov
**__pycache__
config.yaml

View file

@ -1,6 +1,6 @@
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.4.0
rev: v4.0.1
hooks:
- id: check-added-large-files
- id: check-ast
@ -14,7 +14,7 @@ repos:
- id: mixed-line-ending
- id: trailing-whitespace
- repo: https://github.com/psf/black
rev: 22.10.0
rev: 21.6b0
hooks:
- id: black
language_version: python3
@ -22,15 +22,12 @@ repos:
rev: 6.1.1
hooks:
- id: pydocstyle
args:
- --ignore=D200,D203,D212
- repo: https://github.com/PyCQA/flake8
rev: 6.0.0
rev: 3.9.2
hooks:
- id: flake8
- repo: https://github.com/asottile/pyupgrade
rev: v3.2.2
- repo: https://gitlab.com/smop/pre-commit-hooks
rev: v1.0.0
hooks:
- id: pyupgrade
args:
- --py38-plus
- id: check-poetry
- id: check-gitlab-ci

View file

@ -6,67 +6,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [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
## [3.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)
by [@nim65s](https://github.com/)
## [v3.0.0] - 2021-07-18
## [3.0.0] - 2021-07-18
- Simplify code
in [#1](https://github.com/nim65s/matrix-webhook/pull/1)
@ -84,20 +30,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
in [#9](https://github.com/nim65s/matrix-webhook/pull/9)
by [@nim65s](https://github.com/nim65s)
## [v2.0.0] - 2020-03-14
## [2.0.0] - 2020-03-14
- Update to matrix-nio & aiohttp & markdown
## [v1.0.0] - 2020-02-14
## [1.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
[Unreleased]: https://github.com/nim65s/matrix-webhook/compare/v2.0.0...devel
[2.0.0]: https://github.com/nim65s/matrix-webhook/compare/v1.0.0...v2.0.0
[1.0.0]: https://github.com/nim65s/matrix-webhook/releases/tag/v1.0.0

View file

@ -1,55 +1,51 @@
# Matrix Webhook
[![Tests](https://github.com/nim65s/matrix-webhook/actions/workflows/test.yml/badge.svg)](https://github.com/nim65s/matrix-webhook/actions/workflows/test.yml)
[![Lints](https://github.com/nim65s/matrix-webhook/actions/workflows/lint.yml/badge.svg)](https://github.com/nim65s/matrix-webhook/actions/workflows/lint.yml)
[![Docker-Hub](https://github.com/nim65s/matrix-webhook/actions/workflows/docker-hub.yml/badge.svg)](https://hub.docker.com/r/nim65s/matrix-webhook)
[![PyPI](https://github.com/nim65s/matrix-webhook/actions/workflows/pypi.yml/badge.svg)](https://pypi.org/project/matrix-webhook/)
[![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black)
[![codecov](https://codecov.io/gh/nim65s/matrix-webhook/branch/master/graph/badge.svg?token=BLGISGCYKG)](https://codecov.io/gh/nim65s/matrix-webhook)
Post a message to a matrix room with a simple HTTP POST
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
## Install
For now, clone this repo and run `pip install .`
```
python3 -m pip install matrix-webhook
# OR
docker pull nim65s/matrix-webhook
```
## 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):
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:
```
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]
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.
options:
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`
-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`
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`
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`
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
```
@ -78,42 +74,14 @@ docker-compose up -d
## Test / Usage
```
curl -d '{"body":"new contrib from toto: [44](http://radio.localhost/map/#44)", "key": "secret"}' \
curl -d '{"text":"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)
#matrix-webhook:tetaneutral.net](https://matrix.to/#/!DPrUlnwOhBEfYwsDLh:matrix.org?via=laas.fr&via=tetaneutral.net&via=aen.im)
## Unit tests

View file

@ -1,19 +0,0 @@
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

View file

@ -6,7 +6,6 @@ networks:
services:
bot:
image: nim65s/matrix-webhook
build: .
restart: unless-stopped
env_file:

View file

@ -1,18 +0,0 @@
# 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]
```

View file

@ -1,22 +0,0 @@
#!/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

View file

@ -1,15 +1,143 @@
"""Matrix Webhook module entrypoint."""
"""
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 . import app, conf
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")
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()
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__":
main()
log_format = "%(asctime)s - %(name)s - %(lineno)d - %(levelname)s - %(message)s"
logging.basicConfig(level=50 - 10 * conf.VERBOSE, format=log_format)
run()

View file

@ -1,56 +0,0 @@
"""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()

View file

@ -1,25 +1,6 @@
"""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(
@ -39,67 +20,47 @@ 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`",
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-keys",
help="comma separated list of shared secrets to use this service. Required. Environment variable: `API_KEYS`",
"--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(
"-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
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

View file

@ -1,8 +0,0 @@
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

View file

@ -1,34 +0,0 @@
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

View file

@ -1,36 +0,0 @@
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

View file

@ -1,4 +0,0 @@
def formatter(data, headers):
"""Just dump the json data"""
data["body"] = f"{data}"
return data

View file

@ -1,15 +0,0 @@
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

View file

@ -1,12 +0,0 @@
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

View file

@ -1,9 +0,0 @@
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

View file

@ -1,64 +0,0 @@
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

View file

@ -1,5 +0,0 @@
def formatter(data, headers):
""" format a message sent with slack api endpoints"""
text = data["attachments"][0]["text"]
data["body"] = f"{text}"
return data

View file

@ -1,91 +0,0 @@
"""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)

View file

@ -1,78 +0,0 @@
"""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

File diff suppressed because it is too large Load diff

View file

@ -1,5 +0,0 @@
{
"dependencies": {
"@mermaid-js/mermaid-cli": "^8.13.3"
}
}

1014
poetry.lock generated

File diff suppressed because it is too large Load diff

View file

@ -1,6 +1,6 @@
[tool.poetry]
name = "matrix-webhook"
version = "3.5.0"
version = "3.1.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"
@ -8,29 +8,21 @@ 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"
matrix-nio = "^0.18.3"
[tool.poetry.dev-dependencies]
httpx = "^0.23.0"
black = "^22.8.0"
coverage = "^6.4.4"
httpx = "^0.18.2"
coverage = "^5.5"
black = "^21.6b0"
pydocstyle = "^6.1.1"
flake8 = "^5.0.4"
pyupgrade = "^2.31.0"
flake8 = "^3.9.2"
[tool.pydocstyle]
ignore = ["D200", "D203", "D204", "D212"]
ignore = ["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"

View file

@ -4,29 +4,13 @@ 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
ENV SYNAPSE_CONFIG_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

File diff suppressed because one or more lines are too long

View file

@ -1 +0,0 @@
{"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"}

View file

@ -1 +0,0 @@
{"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))"}

View file

@ -1,22 +0,0 @@
{
"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"
}

View file

@ -1,41 +0,0 @@
{
"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"
}

View file

@ -1,33 +0,0 @@
{
"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
}
}

View file

@ -1,36 +0,0 @@
{
"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
}
}

View file

@ -1,34 +0,0 @@
{
"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
}
}
}

View file

@ -25,35 +25,19 @@ parser.add_argument(
)
def bot_req(
req=None,
key=None,
room_id=None,
params=None,
key_as_param=False,
room_as_parameter=False,
):
def bot_req(req=None, key=None, room_id=None):
"""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()
req["key"] = key
url = BOT_URL if room_id is None else f"{BOT_URL}/{room_id}"
return httpx.post(url, 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.
"""
"""Ensure a service at a given url answers with valid json containing a certain key."""
try:
data = httpx.get(url).json()
return key in data
@ -104,7 +88,7 @@ def run_and_test():
srv.terminate()
# TODO Check what the bot says when the server is offline
# print(bot_req({'data': 'bye'}, KEY), {'status': 200, 'ret': 'OK'})
# print(bot_req({'text': 'bye'}, KEY), {'status': 200, 'ret': 'OK'})
LOGGER.info("Stopping the bot")
bot.terminate()

View file

@ -1,130 +0,0 @@
"""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()

View file

@ -1,54 +0,0 @@
"""
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",
)

View file

@ -1,55 +0,0 @@
"""
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",
)

View file

@ -1,46 +0,0 @@
"""
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",
)

View file

@ -1,51 +0,0 @@
"""
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)

View file

@ -1,51 +0,0 @@
"""
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)

View file

@ -1,44 +0,0 @@
"""
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",
)

View file

@ -15,33 +15,19 @@ class BotTest(unittest.IsolatedAsyncioTestCase):
self.assertEqual(bot_req(), {"status": 400, "ret": "Invalid JSON"})
self.assertEqual(
bot_req({"toto": 3}),
{"status": 400, "ret": "Missing body, key, room_id"},
{"status": 400, "ret": "Missing text and/or API key property"},
)
self.assertEqual(
bot_req({"body": 3}, "wrong_key", "wrong_room"),
{"status": 401, "ret": "Invalid API key"},
bot_req({"text": 3, "key": None}), {"status": 401, "ret": "Invalid API key"}
)
# TODO: if the client from matrix_webhook has olm support, this won't be a 403 from synapse,
# but a LocalProtocolError from matrix_webhook
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"},
bot_req({"text": 3}, KEY), {"status": 403, "ret": "Unknown room"}
)
async def test_message(self):
"""Send a markdown message with the old format, and check the result."""
"""Send a markdown message, and check the result."""
text = "# Hello"
messages = []
client = nio.AsyncClient(MATRIX_URL, MATRIX_ID)
@ -62,100 +48,6 @@ class BotTest(unittest.IsolatedAsyncioTestCase):
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)
@ -164,6 +56,6 @@ class BotTest(unittest.IsolatedAsyncioTestCase):
await client.logout(all_devices=True)
await client.close()
self.assertEqual(
bot_req({"body": "Re"}, KEY, room.room_id),
bot_req({"text": "Re"}, KEY, room.room_id),
{"status": 200, "ret": "OK"},
)