Initial commit
Signed-off-by: Christopher Arndt <chris@chrisarndt.de>
This commit is contained in:
commit
c78eb82282
|
@ -0,0 +1,2 @@
|
|||
testconfig.json
|
||||
.env
|
|
@ -0,0 +1,4 @@
|
|||
testconfig.json
|
||||
.env
|
||||
*.py[co]
|
||||
__pycache__/
|
|
@ -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"]
|
|
@ -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/
|
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"homeserver": "https://matrix.org",
|
||||
"template": "${DRONE_BUILD_STATUS}",
|
||||
"markdown": "no"
|
||||
}
|
|
@ -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)
|
Loading…
Reference in New Issue