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>
This commit is contained in:
Christopher Arndt 2023-07-23 23:09:55 +02:00
parent db7edd5dae
commit 5c5cd14030
3 changed files with 89 additions and 6 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 markdown matrix-nio RUN python3 -m pip --no-cache-dir install bleach 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

@ -29,6 +29,23 @@ steps:
## Configuration settings ## Configuration settings
* `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.
* `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.
* `accesstoken` * `accesstoken`
Access token to use for authentication instead of `password`. Either an Access token to use for authentication instead of `password`. Either an
@ -52,6 +69,12 @@ steps:
substtution is considered to be in Markdown format and will be rendered to 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. HTML and sent as a formatted message with `org.matrix.custom.html` format.
* `markdown_extensions` *(default:* `admonition, extra, sane_lists, smarty`)
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.
* `pass_environment` *(default:* `DRONE_*`*)* * `pass_environment` *(default:* `DRONE_*`*)*
Comma-separated white-list of environment variable names or name patterns. Comma-separated white-list of environment variable names or name patterns.
@ -83,6 +106,10 @@ 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_TAGS`]: ./matrixchat-notify.py#L35
[allowed attributes]: https://bleach.readthedocs.io/en/latest/clean.html#allowed-attributes-attributes
[drone.io]: https://drone.io/ [drone.io]: https://drone.io/
[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

@ -3,7 +3,8 @@
Requires: Requires:
* <https://github.com/poljar/matrix-nio> * <https://pypi.org/project/matrix-nio>
* Optional: <https://pypi.org/project/bleach/>
* Optional: <https://pypi.org/project/markdown/> * Optional: <https://pypi.org/project/markdown/>
""" """
@ -19,19 +20,50 @@ 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.update(
{
"*": ["class"],
"img": ["alt", "src"],
}
)
DEFAULT_ALLOWED_TAGS = bleach.ALLOWED_TAGS | {
"dd",
"div",
"dl",
"dt",
"h1",
"h2",
"h3",
"h4",
"h5",
"h6",
"p",
"span",
"table",
"td",
"th",
"thead",
"tr",
}
DEFAULT_HOMESERVER = "https://matrix.org" DEFAULT_HOMESERVER = "https://matrix.org"
DEFAULT_MARKDOWN_EXTENSIONS = "admonition, extra, sane_lists, smarty"
DEFAULT_PASS_ENVIRONMENT = ["DRONE_*"] DEFAULT_PASS_ENVIRONMENT = ["DRONE_*"]
DEFAULT_TEMPLATE = "${DRONE_BUILD_STATUS}" DEFAULT_TEMPLATE = "${DRONE_BUILD_STATUS}"
SETTINGS_KEYS = ( SETTINGS_KEYS = (
"allowed_tags",
"allowed_attrs",
"accesstoken", "accesstoken",
"deviceid", "deviceid",
"devicename", "devicename",
"homeserver", "homeserver",
"markdown", "markdown",
"markdown_extensions",
"pass_environment", "pass_environment",
"password", "password",
"roomid", "roomid",
@ -138,11 +170,35 @@ def render_message(config):
return Template(template).safe_substitute(context) return Template(template).safe_substitute(context)
def render_markdown(message): def render_markdown(message, config):
import markdown import markdown
formatted = markdown.markdown(message) allowed_attrs = config.get("allowed_attrs", DEFAULT_ALLOWED_ATTRS)
return {"formatted_body": formatted, "body": message, "format": "org.matrix.custom.html"} 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): def main(args=None):
@ -209,7 +265,7 @@ def main(args=None):
if tobool(config.get("markdown")) or args.render_markdown: if tobool(config.get("markdown")) or args.render_markdown:
log.debug("Rendering markdown message to HTML.") log.debug("Rendering markdown message to HTML.")
try: try:
message = render_markdown(message) message = render_markdown(message, config)
except: ## noqa except: ## noqa
log.exception("Failed to render message with markdown.") log.exception("Failed to render message with markdown.")