Compare commits

...

10 Commits

Author SHA1 Message Date
Christopher Arndt 8315fee398 docs: improve config example
separate sections for required/optional config settings

Signed-off-by: Christopher Arndt <chris@chrisarndt.de>
2023-07-24 15:43:57 +02:00
Christopher Arndt 37ba136b5c docs: fix minor readme typo
Signed-off-by: Christopher Arndt <chris@chrisarndt.de>
2023-07-24 14:20:46 +02:00
Christopher Arndt 70a70531be docs: add readme badges and fix source code line references
Signed-off-by: Christopher Arndt <chris@chrisarndt.de>
2023-07-24 14:17:04 +02:00
Christopher Arndt d6e24aa359 feat: support rendering message template with Jinja
Add config setting to enable Jinja templates

Signed-off-by: Christopher Arndt <chris@chrisarndt.de>
2023-07-24 01:07:32 +02:00
Christopher Arndt 92d85d31bf fix: do not require bleach module by default
Signed-off-by: Christopher Arndt <chris@chrisarndt.de>
2023-07-23 23:54:03 +02:00
Christopher Arndt 1e374c61e6 fix: order of setting descriptions in readme
Signed-off-by: Christopher Arndt <chris@chrisarndt.de>
2023-07-23 23:15:08 +02:00
Christopher Arndt 5c5cd14030 feat: support markdown extension and bleach generated HTML
Add config settings for enabled markdown extensions and allowed tags and attributes in HTML output with sensible defaults

Signed-off-by: Christopher Arndt <chris@chrisarndt.de>
2023-07-23 23:09:55 +02:00
Christopher Arndt db7edd5dae feat: add environment white-list config setting and cmdline option
See description in readme for details

Signed-off-by: Christopher Arndt <chris@chrisarndt.de>
2023-07-23 20:28:33 +02:00
Christopher Arndt da31628d86 feat: log level from environment is recognised case-insensitively
Signed-off-by: Christopher Arndt <chris@chrisarndt.de>
2023-07-23 18:02:03 +02:00
Christopher Arndt d3579b6e0f docs: minor readme word order fix
Signed-off-by: Christopher Arndt <chris@chrisarndt.de>
2023-07-22 22:13:59 +02:00
3 changed files with 235 additions and 37 deletions

View File

@ -1,5 +1,5 @@
FROM python:3.11-alpine
RUN python3 -m pip --no-cache-dir install markdown matrix-nio
RUN python3 -m pip --no-cache-dir install bleach jinja2 markdown matrix-nio
ADD matrixchat-notify.py /bin/
ADD matrixchat-notify-config.json /etc/
RUN chmod +x /bin/matrixchat-notify.py

106
README.md
View File

@ -1,7 +1,14 @@
# drone-matrixchat-notify
A [drone.io] [plugin] to send notifications to Matrix chat rooms from
CI pipeline steps.
[![MIT License](https://img.shields.io/github/license/SpotlightKid/drone-matrixchat-notify?label=License)](https://github.com/SpotlightKid/drone-matrixchat-notify/blob/master/LICENSE)
[![GitHub tag (with filter)](https://img.shields.io/github/v/tag/SpotlightKid/drone-matrixchat-notify?filter=v*.*.*&logo=github&label=Latest%20version)](https://github.com/SpotlightKid/drone-matrixchat-notify/tags)
[![Docker image version](https://img.shields.io/docker/v/spotlightkid/drone-matrixchat-notify?logo=docker&label=Docker+image)](https://hub.docker.com/r/spotlightkid/drone-matrixchat-notify)
[![GitHub stars](https://img.shields.io/github/stars/SpotlightKid/drone-matrixchat-notify?logo=github&label=GitHub)](https://github.com/SpotlightKid/drone-matrixchat-notify)
[![GitLab stars](https://img.shields.io/gitlab/stars/SpotlightKid%2Fdrone-matrixchat-notify?logo=gitlab&label=GitLab)](https://gitlab.com/SpotlightKid/drone-matrixchat-notify)
[![GitHub issues](https://img.shields.io/github/issues/SpotlightKid/drone-matrixchat-notify?logo=github&label=Issues)](https://github.com/SpotlightKid/drone-matrixchat-notify/issues)
A [drone.io] [plugin] to send notifications to Matrix chat rooms from CI
pipeline steps. Supports *Jinja* message templates and *Markdown* rendering.
Example pipeline configuration:
@ -24,16 +31,54 @@ steps:
userid: '@drone-bot@matrix.org'
password:
from_secret: drone-bot-pw
template: '${DRONE_REPO} ${DRONE_COMMIT_SHA} ${DRONE_BUILD_STATUS}'
markdown: 'yes'
template: |
`${DRONE_REPO}` build #${DRONE_BUILD_NUMBER} status: **${DRONE_BUILD_STATUS}**
${DRONE_PULL_REQUEST_TITLE}](${DRONE_COMMIT_LINK})
```
## Configuration settings
* `accesstoken`
### Required
* `roomid` *(required)*
ID of matrix chat room to send messages to (ID, not alias).
* `userid` *(required)*
Matrix user ID on homeserver to send message as (ID, not username).
* `password` *(required)*
Password to use for authenticating the user set with `userid`. Either a
password or an access token is required.
* `accesstoken` *(required)*
Access token to use for authentication instead of `password`. Either an
access token or a password is required.
### Optional
* `allowed_attrs` *(default:* [`DEFAULT_ALLOWED_ATTRS`]*)*
List or string with comma-separated list of HTML attribute names or
dict mapping tag names to lists of attributes names.
See the bleach documentation on [allowed attributes] for more information.
* `allowed_tags` *(default:* [`DEFAULT_ALLOWED_TAGS`]*)*
List or set or string with comma-separated list of HTML tag names. HTML
tags not included will be stripped from the HTML output generated by
rendering a Markdown message template.
Note that the default list does not include any tags, which allow to load
external resources when the generated HTML is displayed, notably `img`
is not included.
* `deviceid`
Device ID to send with access token.
@ -46,34 +91,59 @@ steps:
The Matrix homeserver URL.
* `jinja`
If set to `yes`, `y`, `true`, `t`, `on` or `1`, the message template is
rendered with the [Jinja] templating engine (instead of performing simple
placeholder substitution). The template context is controlled by the
`pass_environment` setting, same as with non-Jinja templates, but
placeholders use a different syntax (example: `{{DRONE_REPO}}`), so the
`template` setting should be changed to be a valid Jinja template string
when this is enabled.
Using this feature requires the `jinja2` Python module to be available
(it is installed by default in the plugin's docker image).
* `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.
If set to `yes`, `y`, `true`, `t`, `on` or `1`, 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 the format set to
`org.matrix.custom.html`.
* `password`
Using this feature requires the `markdown` and `bleach` Python modules to
be available (they are installed by default in the plugin's docker image).
Password to use for authenticating the user set with `userid`. Either a
password or an access token is required.
* `markdown_extensions` *(default:* `admonition, extra, sane_lists, smarty`)
* `roomid` *(required)*
Comma-separated list of enabled Markdown extensions. See this
[list of extensions] for valid extension names. Including an invalid
extension name in this list will disable Markdown rendering.
ID of matrix chat room to send messages to (ID, not alias).
* `pass_environment` *(default:* `DRONE_*`*)*
Comma-separated white-list of environment variable names or name patterns.
Patterns are shell-glob style patterns and case-sensitive.
Only environment variables matching any of the given names or patterns will
be available as valid placeholders in the message template.
* `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.
The message template. Valid placeholders (example: `${DRONE_REPO}`) will be
substituted with the values of the matching environment variables (subject
to filtering according to the `pass_environment` setting).
See this [reference] for environment variables available drone.io in CI
See this [reference] for environment variables available in drone.io CI
pipelines.
* `userid` *(required)*
ID of user on homeserver to send message as (ID, not username).
[`DEFAULT_ALLOWED_ATTRS`]: https://github.com/SpotlightKid/drone-matrixchat-notify/blob/master/matrixchat-notify.py#L28
[`DEFAULT_ALLOWED_TAGS`]: https://github.com/SpotlightKid/drone-matrixchat-notify/blob/master/matrixchat-notify.py#L35
[allowed attributes]: https://bleach.readthedocs.io/en/latest/clean.html#allowed-attributes-attributes
[drone.io]: https://drone.io/
[jinja]: https://jinja.palletsprojects.com/
[list of extensions]: https://python-markdown.github.io/extensions/
[plugin]: https://docs.drone.io/plugins/overview/
[reference]: https://docs.drone.io/pipeline/environment/reference/

View File

@ -3,13 +3,16 @@
Requires:
* <https://github.com/poljar/matrix-nio>
* <https://pypi.org/project/matrix-nio>
* Optional: <https://pypi.org/project/bleach/>
* Optional: <https://pypi.org/project/Jinja2/>
* Optional: <https://pypi.org/project/markdown/>
"""
import argparse
import asyncio
import fnmatch
import json
import logging
import os
@ -22,14 +25,59 @@ from nio import AsyncClient, LoginResponse
PROG = "matrixchat-notify"
CONFIG_FILENAME = f"{PROG}-config.json"
DEFAULT_TEMPLATE = "${DRONE_BUILD_STATUS}"
DEFAULT_ALLOWED_ATTRS = {
"*": ["class"],
"a": ["href", "title"],
"abbr": ["title"],
"acronym": ["title"],
"img": ["alt", "src"],
}
DEFAULT_ALLOWED_TAGS = {
"a",
"abbr",
"acronym",
"b",
"blockquote",
"code",
"dd",
"div",
"dl",
"dt",
"em",
"h1",
"h2",
"h3",
"h4",
"h5",
"h6",
"i",
"li",
"ol",
"p",
"span",
"strong",
"table",
"td",
"th",
"thead",
"tr",
"ul",
}
DEFAULT_HOMESERVER = "https://matrix.org"
DEFAULT_MARKDOWN_EXTENSIONS = "admonition, extra, sane_lists, smarty"
DEFAULT_PASS_ENVIRONMENT = ["DRONE_*"]
DEFAULT_TEMPLATE = "${DRONE_BUILD_STATUS}"
SETTINGS_KEYS = (
"allowed_tags",
"allowed_attrs",
"accesstoken",
"deviceid",
"devicename",
"homeserver",
"jinja",
"markdown",
"markdown_extensions",
"pass_environment",
"password",
"roomid",
"template",
@ -40,7 +88,7 @@ log = logging.getLogger(PROG)
def tobool(s):
try:
return strtobool(s)
return strtobool(str(s))
except ValueError:
return False
@ -110,6 +158,78 @@ async def send_notification(config, message):
await client.close()
def get_template_context(config):
pass_environment = config.get("pass_environment", [])
if not isinstance(pass_environment, list):
pass_environment = [pass_environment]
patterns = []
for value in pass_environment:
# expand any comma-separated names/patterns
if "," in value:
patterns.extend([p.strip() for p in value.split(",") if p.strip()])
else:
patterns.append(value)
env_names = tuple(os.environ)
filtered_names = set()
for pattern in patterns:
filtered_names.update(fnmatch.filter(env_names, pattern))
return {name: os.environ[name] for name in tuple(filtered_names)}
def render_message(config):
context = get_template_context(config)
template = config.get("template", DEFAULT_TEMPLATE)
if tobool(config.get("jinja")):
try:
from jinja2.sandbox import SandboxedEnvironment
env = SandboxedEnvironment()
return env.from_string(template).render(context)
except Exception as exc:
log.error("Could not render Jinja2 template: %s", exc)
return template
else:
return Template(template).safe_substitute(context)
def render_markdown(message, config):
import bleach
import markdown
allowed_attrs = config.get("allowed_attrs", DEFAULT_ALLOWED_ATTRS)
allowed_tags = config.get("allowed_tags", DEFAULT_ALLOWED_TAGS)
extensions = config.get("markdown_extensions", DEFAULT_MARKDOWN_EXTENSIONS)
if isinstance(allowed_attrs, str):
allowed_attrs = [attr.strip() for attr in allowed_attrs.split(",") if attr.strip()]
if isinstance(allowed_tags, str):
allowed_tags = [tag.strip() for tag in allowed_tags.split(",") if tag.strip()]
if isinstance(extensions, str):
extensions = [ext.strip() for ext in extensions.split(",") if ext.strip()]
try:
md = markdown.Markdown(extensions=extensions)
except (AttributeError, ImportError, TypeError) as exc:
log.error("Could not instantiate Markdown formatter: %s", exc)
return message
return {
"formatted_body": bleach.clean(
md.convert(message), tags=allowed_tags, attributes=allowed_attrs, strip=True
),
"body": message,
"format": "org.matrix.custom.html",
}
def main(args=None):
ap = argparse.ArgumentParser(prog=PROG, description=__doc__.splitlines()[0])
ap.add_argument(
@ -125,6 +245,17 @@ def main(args=None):
action="store_true",
help="Don't send notification message, only print it.",
)
ap.add_argument(
"-e",
"--pass-environment",
nargs="*",
help=(
"Comma-separated white-list of environment variable names or name patterns. Only "
"environment variables matching any of the given names or patterns will be available "
"as valid placeholders in the message template. "
"Accepts shell glob patterns and may be passed more than once (default: 'DRONE_*')."
),
)
ap.add_argument(
"-m",
"--render-markdown",
@ -141,9 +272,7 @@ def main(args=None):
args = ap.parse_args(args)
logging.basicConfig(
level=getattr(
logging, "DEBUG" if args.verbose else os.environ.get("PLUGIN_LOG_LEVEL", "INFO")
),
level="DEBUG" if args.verbose else os.environ.get("PLUGIN_LOG_LEVEL", "INFO").upper(),
format=os.environ.get("PLUGIN_LOG_FORMAT", "%(levelname)s: %(message)s"),
)
@ -152,23 +281,22 @@ def main(args=None):
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 args.pass_environment is not None:
# Security feature: if any environment names/patterns are passed via -e|--pass-environment
# options, they completely replace any given via the config or environment.
config["pass_environment"] = args.pass_environment
if "pass_environment" not in config:
config["pass_environment"] = DEFAULT_PASS_ENVIRONMENT
message = render_message(config)
if tobool(config.get("markdown")) or args.render_markdown:
log.debug("Rendering markdown message to HTML.")
try:
import markdown
formatted = markdown.markdown(message)
message = render_markdown(message, config)
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"):