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