Initial commit

Signed-off-by: Christopher Arndt <chris@chrisarndt.de>
This commit is contained in:
Christopher Arndt 2023-07-22 16:22:35 +02:00
commit c78eb82282
6 changed files with 289 additions and 0 deletions

2
.dockerignore Normal file
View File

@ -0,0 +1,2 @@
testconfig.json
.env

4
.gitignore vendored Normal file
View File

@ -0,0 +1,4 @@
testconfig.json
.env
*.py[co]
__pycache__/

6
Dockerfile Normal file
View File

@ -0,0 +1,6 @@
FROM python:3.11-alpine
RUN python3 -m pip --no-cache-dir install markdown matrix-nio
ADD matrixchat-notify.py /bin/
ADD matrixchat-notify-config.json /etc/
RUN chmod +x /bin/matrixchat-notify.py
ENTRYPOINT ["/bin/matrixchat-notify.py", "-c", "/etc/matrixchat-notify-config.json"]

79
README.md Normal file
View File

@ -0,0 +1,79 @@
# drone-matrixchat-notify
A [drone.io] [plugin] to send notifications to Matrix chat rooms from
CI pipeline steps.
Example pipeline configuration:
```yaml
kind: pipeline
type: docker
name: default
steps:
- name: build
image: alpine
commands:
- ./build
- name: notify
image: spotlightkid/drone-matrixchat-notify
settings:
homeserver: 'https://matrix.org'
roomid: '!xxxxxx@matrix.org'
userid: '@drone-bot@matrix.org'
password:
from_secret: drone-bot-pw
template: '${DRONE_REPO} ${DRONE_COMMIT_SHA} ${DRONE_BUILD_STATUS}'
```
# Configuration settings
* `accesstoken`
Access token to use for authentication instead of `password`. Either an
access token or a password is required.
* `deviceid`
Device ID to send with access token.
* `devicename`
Device name to send with access token.
* `homeserver` *(default:* `https://matrix.org`*)*
The Matrix homeserver URL.
* `markdown`
If set to `yes`, `y`, `true` or `on`, the message resulting from template
substtution is considered to be in Markdown format and will be rendered to
HTML and sent as a formatted message with `org.matrix.custom.html` format.
* `password`
Password to use for authenticating the user set with `userid`. Either a
password or an access token is required.
* `roomid` *(required)*
ID of matrix chat room to send messages to (ID, not alias).
* `template` *(default:* `${DRONE_BUILD_STATUS}`*)*
The message template. Valid placeholders of the form `${PLACEHOLDER}` will
be substituted with the values of the matching environment variables.
See this [reference] for environment variables available drone.io in CI
pipelines.
* `userid` *(required)*
ID of user on homeserver to send message as (ID, not username).
[drone.io]: https://drone.io/
[plugin]: https://docs.drone.io/plugins/overview/
[reference]: https://docs.drone.io/pipeline/environment/reference/

View File

@ -0,0 +1,5 @@
{
"homeserver": "https://matrix.org",
"template": "${DRONE_BUILD_STATUS}",
"markdown": "no"
}

193
matrixchat-notify.py Executable file
View File

@ -0,0 +1,193 @@
#!/usr/bin/env python3
"""Notify of drone.io CI pipeline results on Matrix chat.
Requires:
* <https://github.com/poljar/matrix-nio>
* Optional: <https://pypi.org/project/markdown/>
"""
import argparse
import asyncio
import logging
import json
import os
import sys
from distutils.util import strtobool
from os.path import exists, isdir, join
from string import Template
from nio import AsyncClient, LoginResponse
PROG = "matrixchat-notify"
CONFIG_FILENAME = f"{PROG}-config.json"
DEFAULT_TEMPLATE = "${DRONE_BUILD_STATUS}"
DEFAULT_HOMESERVER = "https://matrix.org"
log = logging.getLogger(PROG)
SETTINGS_KEYS = (
"accesstoken",
"deviceid",
"devicename",
"homeserver",
"markdown",
"password",
"roomid",
"template",
"userid",
)
def tobool(s):
try:
return strtobool(s)
except ValueError:
return False
def read_config_from_file(filename):
config = {}
if exists(filename):
with open(filename) as fp:
config = json.load(fp)
for setting in SETTINGS_KEYS:
val = os.getenv("PLUGIN_" + setting.upper())
if val is not None:
config[setting] = val
if not config.get(setting):
log.debug(f"Configuration setting '{setting}' not set or empty in config.")
return config
async def send_notification(config, message):
token = config.get("accesstoken")
device_id = config.get("deviceid")
homeserver = config.get("homeserver", DEFAULT_HOMESERVER)
client = AsyncClient(homeserver, config["userid"])
log.debug("Created AsyncClient: %r", client)
if token and device_id:
log.debug("Using access token for authentication.")
client.access_token = token
client.device_id = device_id
else:
log.debug("Trying to log in with password...")
resp = await client.login(config["password"], device_name=config.get("devicename"))
# check that we logged in succesfully
if isinstance(resp, LoginResponse):
log.debug("Matrix login successful.")
log.debug("Access token: %s", resp.access_token)
log.debug("Device ID: %s", resp.device_id)
else:
log.error(f"Failed to log in: {resp}")
await client.close()
return
if isinstance(message, dict):
message.setdefault("msgtype", "m.notice")
resp = await client.room_send(
config["roomid"], message_type="m.room.message", content=message
)
else:
resp = await client.room_send(
config["roomid"],
message_type="m.room.message",
content={"msgtype": "m.notice", "body": message},
)
log.info(
"Sent notification message to %s. Response status: %s",
homeserver,
resp.transport_response.status,
)
await client.close()
def main(args=None):
ap = argparse.ArgumentParser(prog=PROG, description=__doc__.splitlines()[0])
ap.add_argument(
"-c",
"--config",
metavar="PATH",
default=CONFIG_FILENAME,
help="Configuration file path (default: '%(default)s')",
)
ap.add_argument(
"-d",
"--dry-run",
action="store_true",
help="Don't send notification message, only print it.",
)
ap.add_argument(
"-m",
"--render-markdown",
action="store_true",
help="Message is in Markdown format and will be rendered to HTML.",
)
ap.add_argument(
"-v",
"--verbose",
action="store_true",
help="Enable debug level logging.",
)
args = ap.parse_args(args)
logging.basicConfig(
level=getattr(
logging, "DEBUG" if args.verbose else os.environ.get("PLUGIN_LOG_LEVEL", "INFO")
),
format=os.environ.get("PLUGIN_LOG_FORMAT", "%(levelname)s: %(message)s"),
)
try:
config = read_config_from_file(args.config)
except Exception as exc:
return f"Could not parse configuration: {exc}"
template = config.get("template", DEFAULT_TEMPLATE)
message = Template(template).safe_substitute(os.environ)
if tobool(config.get("markdown")) or args.render_markdown:
log.debug("Rendering markdown message to HTML.")
try:
import markdown
formatted = markdown.markdown(message)
except: ## noqa
log.exception("Failed to render message with markdown.")
return 1
body = message
message = {"formatted_body": formatted}
message["body"] = body
message["format"] = "org.matrix.custom.html"
if not args.dry_run:
if not config.get("userid"):
return "userid not found in configuration."
if not config.get("roomid"):
return "roomid not found in configuration."
if not config.get("password") and not config.get("accesstoken"):
return "No password or accesstoken found in configuration."
try:
log.debug("Sending notification to Matrix chat...")
asyncio.run(send_notification(config, message))
except KeyboardInterrupt:
log.info("Interrupted.")
else:
print(message)
if __name__ == "__main__":
sys.exit(main() or 0)