diff --git a/README.md b/README.md index a6f9f0ec..6939f4f1 100644 --- a/README.md +++ b/README.md @@ -111,7 +111,7 @@ Just some examples Self-hosted web page change monitoring notifications -Now you can also customise your notification content! +Now you can also customise your notification content and use Jinja2 templating for their title and body! ### JSON API Monitoring diff --git a/changedetectionio/forms.py b/changedetectionio/forms.py index 99c2c53b..46b6977c 100644 --- a/changedetectionio/forms.py +++ b/changedetectionio/forms.py @@ -133,7 +133,7 @@ class ValidateAppRiseServers(object): message = field.gettext('\'%s\' is not a valid AppRise URL.' % (server_url)) raise ValidationError(message) -class ValidateTokensList(object): +class ValidateJinja2Template(object): """ Validates that a {token} is from a valid set """ @@ -142,11 +142,22 @@ class ValidateTokensList(object): def __call__(self, form, field): from changedetectionio import notification - regex = re.compile('{.*?}') - for p in re.findall(regex, field.data): - if not p.strip('{}') in notification.valid_tokens: - message = field.gettext('Token \'%s\' is not a valid token.') - raise ValidationError(message % (p)) + from jinja2 import Environment, BaseLoader, TemplateSyntaxError + from jinja2.meta import find_undeclared_variables + + try: + jinja2_env = Environment(loader=BaseLoader) + jinja2_env.globals.update(notification.valid_tokens) + rendered = jinja2_env.from_string(field.data).render() + except TemplateSyntaxError as e: + raise ValidationError(f"This is not a valid Jinja2 template: {e}") from e + + ast = jinja2_env.parse(field.data) + undefined = ", ".join(find_undeclared_variables(ast)) + if undefined: + raise ValidationError( + f"The following tokens used in the notification are not valid: {undefined}" + ) class ValidateListRegex(object): """ @@ -201,8 +212,8 @@ class quickWatchForm(Form): class commonSettingsForm(Form): notification_urls = StringListField('Notification URL List', validators=[validators.Optional(), ValidateAppRiseServers()]) - notification_title = StringField('Notification Title', default='ChangeDetection.io Notification - {watch_url}', validators=[validators.Optional(), ValidateTokensList()]) - notification_body = TextAreaField('Notification Body', default='{watch_url} had a change.', validators=[validators.Optional(), ValidateTokensList()]) + notification_title = StringField('Notification Title', default='ChangeDetection.io Notification - {{ watch_url }}', validators=[validators.Optional(), ValidateJinja2Template()]) + notification_body = TextAreaField('Notification Body', default='{{ watch_url }} had a change.', validators=[validators.Optional(), ValidateJinja2Template()]) trigger_check = BooleanField('Send test notification on save') fetch_backend = RadioField(u'Fetch Method', choices=content_fetcher.available_fetchers(), validators=[ValidateContentFetcherIsReady()]) extract_title_as_title = BooleanField('Extract from document and use as watch title', default=False) diff --git a/changedetectionio/notification.py b/changedetectionio/notification.py index 0a18fdba..7eb22428 100644 --- a/changedetectionio/notification.py +++ b/changedetectionio/notification.py @@ -1,5 +1,5 @@ -import os import apprise +from jinja2 import Environment, BaseLoader valid_tokens = { 'base_url': '', @@ -24,18 +24,13 @@ def process_notification(n_object, datastore): print (">> Process Notification: AppRise notifying {}".format(url)) apobj.add(url) - # Get the notification body from datastore - n_body = n_object['notification_body'] - n_title = n_object['notification_title'] - # Insert variables into the notification content notification_parameters = create_notification_parameters(n_object, datastore) - for n_k in notification_parameters: - token = '{' + n_k + '}' - val = notification_parameters[n_k] - n_title = n_title.replace(token, val) - n_body = n_body.replace(token, val) + # Get the notification body from datastore + jinja2_env = Environment(loader=BaseLoader) + n_body = jinja2_env.from_string(n_object['notification_body']).render(**notification_parameters) + n_title = jinja2_env.from_string(n_object['notification_title']).render(**notification_parameters) apobj.notify( body=n_body, @@ -61,7 +56,7 @@ def create_notification_parameters(n_object, datastore): watch_url = n_object['watch_url'] - # Re #148 - Some people have just {base_url} in the body or title, but this may break some notification services + # Re #148 - Some people have just {{ base_url }} in the body or title, but this may break some notification services # like 'Join', so it's always best to atleast set something obvious so that they are not broken. if base_url == '': base_url = "<base-url-env-var-not-set>" @@ -85,4 +80,4 @@ def create_notification_parameters(n_object, datastore): 'current_snapshot': n_object['current_snapshot'] if 'current_snapshot' in n_object else '' }) - return tokens \ No newline at end of file + return tokens diff --git a/changedetectionio/templates/_common_fields.jinja b/changedetectionio/templates/_common_fields.jinja index 55573781..1e8dda18 100644 --- a/changedetectionio/templates/_common_fields.jinja +++ b/changedetectionio/templates/_common_fields.jinja @@ -26,8 +26,10 @@ </div> <div class="pure-controls"> <span class="pure-form-message-inline"> - These tokens can be used in the notification body and title to - customise the notification text. + You can use + <a target="_new" href="https://jinja.palletsprojects.com/en/3.0.x/templates/">Jinja2</a> + templating for the notification title and body.<br /> + These tokens can be used to customise them. </span> <table class="pure-table" id="token-table"> <thead> @@ -38,42 +40,42 @@ </thead> <tbody> <tr> - <td><code>{base_url}</code></td> + <td><code>{{ '{{ base_url }}' }}</code></td> <td>The URL of the changedetection.io instance you are running.</td> </tr> <tr> - <td><code>{watch_url}</code></td> + <td><code>{{ '{{ watch_url }}' }}</code></td> <td>The URL being watched.</td> </tr> <tr> - <td><code>{watch_uuid}</code></td> + <td><code>{{ '{{ watch_uuid }}' }}</code></td> <td>The UUID of the watch.</td> </tr> <tr> - <td><code>{watch_title}</code></td> + <td><code>{{ '{{ watch_title }}' }}</code></td> <td>The title of the watch.</td> </tr> <tr> - <td><code>{watch_tag}</code></td> + <td><code>{{ '{{ watch_tag }}' }}</code></td> <td>The tag of the watch.</td> </tr> <tr> - <td><code>{preview_url}</code></td> + <td><code>{{ '{{ preview_url }}' }}</code></td> <td>The URL of the preview page generated by changedetection.io.</td> </tr> <tr> - <td><code>{diff_url}</code></td> + <td><code>{{ '{{ diff_url }}' }}</code></td> <td>The URL of the diff page generated by changedetection.io.</td> </tr> <tr> - <td><code>{current_snapshot}</code></td> + <td><code>{{ '{{ current_snapshot }}' }}</code></td> <td>The current snapshot value, useful when combined with JSON or CSS filters </td> </tr> </tbody> </table> <span class="pure-form-message-inline"> - URLs generated by changedetection.io (such as <code>{diff_url}</code>) require the <code>BASE_URL</code> environment variable set.<br/> + URLs generated by changedetection.io (such as <code>{{ '{{ diff_url }}' }}</code>) require the <code>BASE_URL</code> environment variable set.<br/> Your <code>BASE_URL</code> var is currently "{{base_url}}" </span> </div> diff --git a/changedetectionio/templates/settings.html b/changedetectionio/templates/settings.html index 856f9539..9f2eaf26 100644 --- a/changedetectionio/templates/settings.html +++ b/changedetectionio/templates/settings.html @@ -36,7 +36,7 @@ {{ render_field(form.base_url, placeholder="http://yoursite.com:5000/", class="m-d") }} <span class="pure-form-message-inline"> - Base URL used for the {base_url} token in notifications, default value is the ENV var 'base_url', + Base URL used for the {{ '{{ base_url }}' }} token in notifications, default value is the ENV var 'base_url', <a href="https://github.com/dgtlmoon/changedetection.io/wiki/Configurable-BASE_URL-setting">read more here</a>. </span> </div> diff --git a/changedetectionio/tests/test_notification.py b/changedetectionio/tests/test_notification.py index 95be9a10..f0598915 100644 --- a/changedetectionio/tests/test_notification.py +++ b/changedetectionio/tests/test_notification.py @@ -46,15 +46,15 @@ def test_check_notification(client, live_server): res = client.post( url_for("edit_page", uuid="first"), data={"notification_urls": notification_url, - "notification_title": "New ChangeDetection.io Notification - {watch_url}", - "notification_body": "BASE URL: {base_url}\n" - "Watch URL: {watch_url}\n" - "Watch UUID: {watch_uuid}\n" - "Watch title: {watch_title}\n" - "Watch tag: {watch_tag}\n" - "Preview: {preview_url}\n" - "Diff URL: {diff_url}\n" - "Snapshot: {current_snapshot}\n" + "notification_title": "New ChangeDetection.io Notification - {{ watch_url }}", + "notification_body": "BASE URL: {{ base_url }}\n" + "Watch URL: {{ watch_url }}\n" + "Watch UUID: {{ watch_uuid }}\n" + "Watch title: {{ watch_title }}\n" + "Watch tag: {{ watch_tag }}\n" + "Preview: {{ preview_url }}\n" + "Diff URL: {{ diff_url }}\n" + "Snapshot: {{ current_snapshot }}\n" ":-)", "url": test_url, "tag": "my tag", @@ -126,7 +126,7 @@ def test_check_notification(client, live_server): res = client.post( url_for("settings_page"), - data={"notification_title": "New ChangeDetection.io Notification - {watch_url}", + data={"notification_title": "New ChangeDetection.io Notification - {{ watch_url }}", "notification_urls": "json://foobar.com", #Re #143 should not see that it sent without [test checkbox] "minutes_between_check": 180, "fetch_backend": "html_requests", @@ -179,13 +179,27 @@ def test_check_notification(client, live_server): # Now adding a wrong token should give us an error res = client.post( url_for("settings_page"), - data={"notification_title": "New ChangeDetection.io Notification - {watch_url}", - "notification_body": "Rubbish: {rubbish}\n", + data={"notification_title": "New ChangeDetection.io Notification - {{ watch_url }}", + "notification_body": "Rubbish: {{ rubbish }}\n", "notification_urls": "json://foobar.com", "minutes_between_check": 180, "fetch_backend": "html_requests" }, follow_redirects=True ) + assert bytes("The following tokens used in the notification are not valid".encode('utf-8')) in res.data + + + # And trying to define an invalid Jinja2 template should also throw an error + res = client.post( + url_for("settings_page"), + data={"notification_title": "New ChangeDetection.io Notification - {{ watch_url }}", + "notification_body": "Rubbish: {{ rubbish }\n", + "notification_urls": "json://foobar.com", + "minutes_between_check": 180, + "fetch_backend": "html_requests" + }, + follow_redirects=True + ) + assert bytes("This is not a valid Jinja2 template".encode('utf-8')) in res.data - assert bytes("is not a valid token".encode('utf-8')) in res.data \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index c699fa80..c3c64fa1 100644 --- a/requirements.txt +++ b/requirements.txt @@ -16,8 +16,9 @@ chardet > 2.3.0 wtforms ~= 2.3.3 jsonpath-ng ~= 1.5.3 -# Notification library +# Notification libraries apprise ~= 0.9 +jinja2 ~= 2.11.3 # Pinned version of cryptography otherwise # ERROR: Could not build wheels for cryptography which use PEP 517 and cannot be installed directly