Initial commit
Signed-off-by: Christopher Arndt <chris@chrisarndt.de>
This commit is contained in:
		
						commit
						c78eb82282
					
				
							
								
								
									
										2
									
								
								.dockerignore
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								.dockerignore
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,2 @@
 | 
			
		||||
testconfig.json
 | 
			
		||||
.env
 | 
			
		||||
							
								
								
									
										4
									
								
								.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@ -0,0 +1,4 @@
 | 
			
		||||
testconfig.json
 | 
			
		||||
.env
 | 
			
		||||
*.py[co]
 | 
			
		||||
__pycache__/
 | 
			
		||||
							
								
								
									
										6
									
								
								Dockerfile
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								Dockerfile
									
									
									
									
									
										Normal 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
									
								
							
							
						
						
									
										79
									
								
								README.md
									
									
									
									
									
										Normal 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/
 | 
			
		||||
							
								
								
									
										5
									
								
								matrixchat-notify-config.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								matrixchat-notify-config.json
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,5 @@
 | 
			
		||||
{
 | 
			
		||||
    "homeserver": "https://matrix.org",
 | 
			
		||||
    "template": "${DRONE_BUILD_STATUS}",
 | 
			
		||||
    "markdown": "no"
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										193
									
								
								matrixchat-notify.py
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										193
									
								
								matrixchat-notify.py
									
									
									
									
									
										Executable 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)
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user