Compare commits

...

2 Commits

Author SHA1 Message Date
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
3 changed files with 73 additions and 26 deletions

View File

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

View File

@ -40,7 +40,7 @@ steps:
* `allowed_tags` *(default:* [`DEFAULT_ALLOWED_TAGS`]*)* * `allowed_tags` *(default:* [`DEFAULT_ALLOWED_TAGS`]*)*
List or set or string with comma-separated list of HTML tag names. HTML 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 tags not included will be stripped from the HTML output generated by
rendering a Markdown message template. rendering a Markdown message template.
Note that the default list does not include any tags, which allow to load Note that the default list does not include any tags, which allow to load
@ -64,11 +64,28 @@ steps:
The Matrix homeserver URL. 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 Jinja2 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` * `markdown`
If set to `yes`, `y`, `true` or `on`, the message resulting from template If set to `yes`, `y`, `true`, `t`, `on` or `1`, the message resulting from
substtution is considered to be in Markdown format and will be rendered to template substtution is considered to be in Markdown format and will be
HTML and sent as a formatted message with `org.matrix.custom.html` format. rendered to HTML and sent as a formatted message with the format set to
`org.matrix.custom.html`.
Using this feature requires the `markdown` and `bleach` Python modules to
be available (they are installed by default in the plugin's docker image).
* `markdown_extensions` *(default:* `admonition, extra, sane_lists, smarty`) * `markdown_extensions` *(default:* `admonition, extra, sane_lists, smarty`)
@ -95,9 +112,9 @@ steps:
* `template` *(default:* `${DRONE_BUILD_STATUS}`*)* * `template` *(default:* `${DRONE_BUILD_STATUS}`*)*
The message template. Valid placeholders of the form `${PLACEHOLDER}` will The message template. Valid placeholders (example: `${DRONE_REPO}`) will be
be substituted with the values of the matching environment variables substituted with the values of the matching environment variables (subject
(subject to filtering according to the `pass_environment` setting). to filtering according to the `pass_environment` setting).
See this [reference] for environment variables available in drone.io CI See this [reference] for environment variables available in drone.io CI
pipelines. pipelines.
@ -107,10 +124,11 @@ steps:
ID of user on homeserver to send message as (ID, not username). ID of user on homeserver to send message as (ID, not username).
[`DEFAULT_ALLOWED_ATTRS`]: ./matrixchat-notify.py#L29 [`DEFAULT_ALLOWED_ATTRS`]: ./matrixchat-notify.py#L27
[`DEFAULT_ALLOWED_TAGS`]: ./matrixchat-notify.py#L35 [`DEFAULT_ALLOWED_TAGS`]: ./matrixchat-notify.py#L34
[allowed attributes]: https://bleach.readthedocs.io/en/latest/clean.html#allowed-attributes-attributes [allowed attributes]: https://bleach.readthedocs.io/en/latest/clean.html#allowed-attributes-attributes
[drone.io]: https://drone.io/ [drone.io]: https://drone.io/
[jinja]: https://jinja.palletsprojects.com/
[list of extensions]: https://python-markdown.github.io/extensions/ [list of extensions]: https://python-markdown.github.io/extensions/
[plugin]: https://docs.drone.io/plugins/overview/ [plugin]: https://docs.drone.io/plugins/overview/
[reference]: https://docs.drone.io/pipeline/environment/reference/ [reference]: https://docs.drone.io/pipeline/environment/reference/

View File

@ -5,6 +5,7 @@ Requires:
* <https://pypi.org/project/matrix-nio> * <https://pypi.org/project/matrix-nio>
* Optional: <https://pypi.org/project/bleach/> * Optional: <https://pypi.org/project/bleach/>
* Optional: <https://pypi.org/project/Jinja2/>
* Optional: <https://pypi.org/project/markdown/> * Optional: <https://pypi.org/project/markdown/>
""" """
@ -20,36 +21,47 @@ from distutils.util import strtobool
from os.path import exists from os.path import exists
from string import Template from string import Template
import bleach
from nio import AsyncClient, LoginResponse from nio import AsyncClient, LoginResponse
PROG = "matrixchat-notify" PROG = "matrixchat-notify"
CONFIG_FILENAME = f"{PROG}-config.json" CONFIG_FILENAME = f"{PROG}-config.json"
DEFAULT_ALLOWED_ATTRS = bleach.ALLOWED_ATTRIBUTES.copy() DEFAULT_ALLOWED_ATTRS = {
DEFAULT_ALLOWED_ATTRS.update(
{
"*": ["class"], "*": ["class"],
"a": ["href", "title"],
"abbr": ["title"],
"acronym": ["title"],
"img": ["alt", "src"], "img": ["alt", "src"],
} }
) DEFAULT_ALLOWED_TAGS = {
DEFAULT_ALLOWED_TAGS = bleach.ALLOWED_TAGS | { "a",
"abbr",
"acronym",
"b",
"blockquote",
"code",
"dd", "dd",
"div", "div",
"dl", "dl",
"dt", "dt",
"em",
"h1", "h1",
"h2", "h2",
"h3", "h3",
"h4", "h4",
"h5", "h5",
"h6", "h6",
"i",
"li",
"ol",
"p", "p",
"span", "span",
"strong",
"table", "table",
"td", "td",
"th", "th",
"thead", "thead",
"tr", "tr",
"ul",
} }
DEFAULT_HOMESERVER = "https://matrix.org" DEFAULT_HOMESERVER = "https://matrix.org"
DEFAULT_MARKDOWN_EXTENSIONS = "admonition, extra, sane_lists, smarty" DEFAULT_MARKDOWN_EXTENSIONS = "admonition, extra, sane_lists, smarty"
@ -62,6 +74,7 @@ SETTINGS_KEYS = (
"deviceid", "deviceid",
"devicename", "devicename",
"homeserver", "homeserver",
"jinja",
"markdown", "markdown",
"markdown_extensions", "markdown_extensions",
"pass_environment", "pass_environment",
@ -75,7 +88,7 @@ log = logging.getLogger(PROG)
def tobool(s): def tobool(s):
try: try:
return strtobool(s) return strtobool(str(s))
except ValueError: except ValueError:
return False return False
@ -145,15 +158,15 @@ async def send_notification(config, message):
await client.close() await client.close()
def render_message(config): def get_template_context(config):
pass_environment = config.get("pass_environment", "") pass_environment = config.get("pass_environment", [])
if not isinstance(pass_environment, list): if not isinstance(pass_environment, list):
pass_environment = [pass_environment] pass_environment = [pass_environment]
patterns = [] patterns = []
for value in pass_environment: for value in pass_environment:
# expand any comma-separetd names/patterns # expand any comma-separated names/patterns
if "," in value: if "," in value:
patterns.extend([p.strip() for p in value.split(",") if p.strip()]) patterns.extend([p.strip() for p in value.split(",") if p.strip()])
else: else:
@ -165,12 +178,28 @@ def render_message(config):
for pattern in patterns: for pattern in patterns:
filtered_names.update(fnmatch.filter(env_names, pattern)) filtered_names.update(fnmatch.filter(env_names, pattern))
context = {name: os.environ[name] for name in tuple(filtered_names)} 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) 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) return Template(template).safe_substitute(context)
def render_markdown(message, config): def render_markdown(message, config):
import bleach
import markdown import markdown
allowed_attrs = config.get("allowed_attrs", DEFAULT_ALLOWED_ATTRS) allowed_attrs = config.get("allowed_attrs", DEFAULT_ALLOWED_ATTRS)