Re #185 - [feature] Custom notifications templates per watch (#226)

* Re #185 - [feature] Custom text templates for the notification per monitored entry as override.
Bonus points: Adding validation for apprise URLs
pull/227/head
dgtlmoon 3 years ago committed by GitHub
parent 5b60314e8b
commit b903280cd0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -403,14 +403,15 @@ def changedetection_app(config=None, datastore_o=None):
if form.fetch_backend.data == datastore.data['settings']['application']['fetch_backend']:
form.fetch_backend.data = None
update_obj = {'url': form.url.data.strip(),
'minutes_between_check': form.minutes_between_check.data,
'tag': form.tag.data.strip(),
'title': form.title.data.strip(),
'headers': form.headers.data,
'fetch_backend': form.fetch_backend.data,
'trigger_text': form.trigger_text.data
'trigger_text': form.trigger_text.data,
'notification_title': form.notification_title.data,
'notification_body': form.notification_body.data
}
# Notification URLs
@ -443,7 +444,9 @@ def changedetection_app(config=None, datastore_o=None):
if form.trigger_check.data:
n_object = {'watch_url': form.url.data.strip(),
'notification_urls': form.notification_urls.data
'notification_urls': form.notification_urls.data,
'notification_title': form.notification_title.data,
'notification_body' : form.notification_body.data
}
notification_q.put(n_object)
@ -513,7 +516,10 @@ def changedetection_app(config=None, datastore_o=None):
if form.trigger_check.data and len(form.notification_urls.data):
n_object = {'watch_url': "Test from changedetection.io!",
'notification_urls': form.notification_urls.data}
'notification_urls': form.notification_urls.data,
'notification_title': form.notification_title.data,
'notification_body': form.notification_body.data
}
notification_q.put(n_object)
flash('Notifications queued.')

@ -116,6 +116,23 @@ class ValidateContentFetcherIsReady(object):
raise ValidationError(message % (field.data, e))
class ValidateAppRiseServers(object):
"""
Validates that each URL given is compatible with AppRise
"""
def __init__(self, message=None):
self.message = message
def __call__(self, form, field):
import apprise
apobj = apprise.Apprise()
for server_url in field.data:
if not apobj.add(server_url):
message = field.gettext('\'%s\' is not a valid AppRise URL.' % (server_url))
raise ValidationError(message)
class ValidateTokensList(object):
"""
Validates that a {token} is from a valid set
@ -172,42 +189,40 @@ class ValidateCSSJSONInput(object):
message = field.gettext('\'%s\' is not a valid JSONPath expression. (%s)')
raise ValidationError(message % (input, str(e)))
class quickWatchForm(Form):
# https://wtforms.readthedocs.io/en/2.3.x/fields/#module-wtforms.fields.html5
# `require_tld` = False is needed even for the test harness "http://localhost:5005.." to run
url = html5.URLField('URL', [validators.URL(require_tld=False)])
tag = StringField('Group tag', [validators.Optional(), validators.Length(max=35)])
class watchForm(quickWatchForm):
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()])
trigger_check = BooleanField('Send test notification on save')
fetch_backend = RadioField(u'Fetch Method', choices=content_fetcher.available_fetchers(), validators=[ValidateContentFetcherIsReady()])
class watchForm(commonSettingsForm):
url = html5.URLField('URL', [validators.URL(require_tld=False)])
tag = StringField('Group tag', [validators.Optional(), validators.Length(max=35)])
minutes_between_check = html5.IntegerField('Maximum time in minutes until recheck',
[validators.Optional(), validators.NumberRange(min=1)])
css_filter = StringField('CSS/JSON Filter', [ValidateCSSJSONInput()])
title = StringField('Title')
fetch_backend = RadioField(u'Fetch Method', choices=content_fetcher.available_fetchers(), validators=[ValidateContentFetcherIsReady()])
ignore_text = StringListField('Ignore Text', [ValidateListRegex()])
notification_urls = StringListField('Notification URL List')
headers = StringDictKeyValue('Request Headers')
trigger_check = BooleanField('Send test notification on save')
trigger_text = StringListField('Trigger/wait for text', [validators.Optional(), ValidateListRegex()])
class globalSettingsForm(Form):
class globalSettingsForm(commonSettingsForm):
password = SaltyPasswordField()
minutes_between_check = html5.IntegerField('Maximum time in minutes until recheck',
[validators.NumberRange(min=1)])
notification_urls = StringListField('Notification URL List')
fetch_backend = RadioField(u'Fetch Method', choices=content_fetcher.available_fetchers(), validators=[ValidateContentFetcherIsReady()])
extract_title_as_title = BooleanField('Extract <title> from document and use as watch title')
trigger_check = BooleanField('Send test notification on save')
notification_title = StringField('Notification Title', validators=[validators.Optional(), ValidateTokensList()])
notification_body = TextAreaField('Notification Body', validators=[validators.Optional(), ValidateTokensList()])

@ -20,13 +20,13 @@ def process_notification(n_object, datastore):
apobj = apprise.Apprise(debug=True)
for url in n_object['notification_urls']:
print (">> Process Notification: AppRise notifying {}".format(url.strip()))
apobj.add(url.strip())
url = url.strip()
print (">> Process Notification: AppRise notifying {}".format(url))
apobj.add(url)
# Get the notification body from datastore
n_body = datastore.data['settings']['application']['notification_body']
# Get the notification title from the datastore
n_title = datastore.data['settings']['application']['notification_title']
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)
@ -37,13 +37,11 @@ def process_notification(n_object, datastore):
n_title = n_title.replace(token, val)
n_body = n_body.replace(token, val)
apobj.notify(
body=n_body,
title=n_title
)
# Notification title + body content parameters get created here.
def create_notification_parameters(n_object, datastore):
from copy import deepcopy

@ -42,8 +42,8 @@ class ChangeDetectionStore:
'fetch_backend': 'html_requests',
'notification_urls': [], # Apprise URL list
# Custom notification content
'notification_title': 'ChangeDetection.io Notification - {watch_url}',
'notification_body': '{watch_url} had a change.'
'notification_title': None,
'notification_body': None,
}
}
}
@ -66,7 +66,10 @@ class ChangeDetectionStore:
'headers': {}, # Extra headers to send
'history': {}, # Dict of timestamp and output stripped filename
'ignore_text': [], # List of text to ignore when calculating the comparison checksum
# Custom notification content
'notification_urls': [], # List of URLs to add to the notification Queue (Usually AppRise)
'notification_title': None,
'notification_body': None,
'css_filter': "",
'trigger_text': [], # List of text or regex to wait for until a change is detected
'fetch_backend': None,

@ -0,0 +1,90 @@
{% from '_helpers.jinja' import render_field %}
{% macro render_notifications_field(form) %}
<fieldset>
<div class="field-group">
<div class="pure-control-group">
{{ render_field(form.notification_urls, rows=5, placeholder="Examples:
Gitter - gitter://token/room
Office365 - o365://TenantID:AccountEmail/ClientID/ClientSecret/TargetEmail
AWS SNS - sns://AccessKeyID/AccessSecretKey/RegionName/+PhoneNo
SMTPS - mailtos://user:pass@mail.domain.com?to=receivingAddress@example.com")
}}
<div class="pure-form-message-inline">Use <a target=_new
href="https://github.com/caronc/apprise">AppRise
URLs</a> for notification to just about any service!
</div>
</div>
<div id="notification-customisation">
<div class="pure-control-group">
{{ render_field(form.notification_title, class="m-d") }}
<span class="pure-form-message-inline">Title for all notifications</span>
</div>
<div class="pure-control-group">
{{ render_field(form.notification_body , rows=5) }}
<span class="pure-form-message-inline">Body for all notifications</span>
</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.
</span>
<table class="pure-table" id="token-table">
<thead>
<tr>
<th>Token</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<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>The URL being watched.</td>
</tr>
<tr>
<td><code>{watch_uuid}</code></td>
<td>The UUID of the watch.</td>
</tr>
<tr>
<td><code>{watch_title}</code></td>
<td>The title of the watch.</td>
</tr>
<tr>
<td><code>{watch_tag}</code></td>
<td>The tag of the watch.</td>
</tr>
<tr>
<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>The URL of the diff page generated by changedetection.io.</td>
</tr>
<tr>
<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/>
Your <code>BASE_URL</code> var is currently "{{base_url}}"
</span>
</div>
</div>
<div class="pure-control-group">
{{ render_field(form.trigger_check) }}
</div>
</div>
</fieldset>
{% endmacro %}

@ -23,3 +23,5 @@
{% endif %}
</span>
{% endmacro %}

@ -1,6 +1,7 @@
{% extends 'base.html' %}
{% block content %}
{% from '_helpers.jinja' import render_field %}
{% from '_common_fields.jinja' import render_notifications_field %}
<script type="text/javascript" src="{{url_for('static_content', group='js', filename='tabs.js')}}" defer></script>
<div class="edit-form monospaced-textarea">
@ -57,25 +58,12 @@ User-Agent: wonderbra 1.0") }}
</div>
</fieldset>
</div>
<div class="tab-pane-inner" id="notifications">
<fieldset>
<div class="pure-control-group">
{{ render_field(form.notification_urls, rows=5, placeholder="Examples:
Gitter - gitter://token/room
Office365 - o365://TenantID:AccountEmail/ClientID/ClientSecret/TargetEmail
AWS SNS - sns://AccessKeyID/AccessSecretKey/RegionName/+PhoneNo
SMTPS - mailtos://user:pass@mail.domain.com?to=receivingAddress@example.com
") }}
<span class="pure-form-message-inline">Use <a target=_new
href="https://github.com/caronc/apprise">AppRise URLs</a> for notification to just about any service!</span>
<span class="pure-form-message-inline">Note: This overrides any global settings notification URLs</span>
<strong>Note: <i>These settings override the global settings.</i></strong>
{{ render_notifications_field(form) }}
</div>
<div class="pure-controls">
{{ render_field(form.trigger_check, rows=5) }}
</div>
</fieldset>
</div>
<div class="tab-pane-inner" id="filters">
<fieldset>
<div class="pure-control-group">

@ -2,6 +2,7 @@
{% block content %}
{% from '_helpers.jinja' import render_field %}
{% from '_common_fields.jinja' import render_notifications_field %}
<script type="text/javascript" src="{{url_for('static_content', group='js', filename='settings.js')}}" defer></script>
<script type="text/javascript" src="{{url_for('static_content', group='js', filename='tabs.js')}}" defer></script>
@ -37,91 +38,11 @@
</div>
</fieldset>
</div>
<div class="tab-pane-inner" id="notifications">
<fieldset>
<div class="field-group">
<div class="pure-control-group">
{{ render_field(form.notification_urls, rows=5, placeholder="Examples:
Gitter - gitter://token/room
Office365 - o365://TenantID:AccountEmail/ClientID/ClientSecret/TargetEmail
AWS SNS - sns://AccessKeyID/AccessSecretKey/RegionName/+PhoneNo
SMTPS - mailtos://user:pass@mail.domain.com?to=receivingAddress@example.com")
}}
<div class="pure-form-message-inline">Use <a target=_new
href="https://github.com/caronc/apprise">AppRise
URLs</a> for notification to just about any service!
</div>
</div>
<div id="notification-customisation">
<div class="pure-control-group">
{{ render_field(form.notification_title, class="m-d") }}
<span class="pure-form-message-inline">Title for all notifications</span>
</div>
<div class="pure-control-group">
{{ render_field(form.notification_body , rows=5) }}
<span class="pure-form-message-inline">Body for all notifications</span>
</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.
</span>
<table class="pure-table" id="token-table">
<thead>
<tr>
<th>Token</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<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>The URL being watched.</td>
</tr>
<tr>
<td><code>{watch_uuid}</code></td>
<td>The UUID of the watch.</td>
</tr>
<tr>
<td><code>{watch_title}</code></td>
<td>The title of the watch.</td>
</tr>
<tr>
<td><code>{watch_tag}</code></td>
<td>The tag of the watch.</td>
</tr>
<tr>
<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>The URL of the diff page generated by changedetection.io.</td>
</tr>
<tr>
<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/>
Your <code>BASE_URL</code> var is currently "{{base_url}}"
</span>
</div>
</div>
<div class="pure-control-group">
{{ render_field(form.trigger_check) }}
</div>
{{ render_notifications_field(form) }}
</div>
</fieldset>
</div>
<div class="tab-pane-inner" id="fetching">
<div class="pure-control-group">
{{ render_field(form.fetch_backend) }}

@ -35,6 +35,16 @@ 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"
":-)",
"url": test_url,
"tag": "my tag",
"title": "my title",
@ -66,7 +76,6 @@ def test_check_notification(client, live_server):
os.unlink("test-datastore/notification.txt")
set_modified_response()
# Trigger a check
@ -100,18 +109,9 @@ def test_check_notification(client, live_server):
res = client.post(
url_for("settings_page"),
data={"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_urls": "json://foobar.com", #Re #143 should not see that it sent without [test checkbox]
"minutes_between_check": 180,
"fetch_backend": "html_requests"
"fetch_backend": "html_requests",
},
follow_redirects=True
)

@ -20,7 +20,7 @@ def test_check_watch_field_storage(client, live_server):
res = client.post(
url_for("edit_page", uuid="first"),
data={ "notification_urls": "http://myapi.com",
data={ "notification_urls": "json://myapi.com",
"minutes_between_check": 126,
"css_filter" : ".fooclass",
"title" : "My title",
@ -39,7 +39,7 @@ def test_check_watch_field_storage(client, live_server):
follow_redirects=True
)
assert b"http://myapi.com" in res.data
assert b"json://myapi.com" in res.data
assert b"126" in res.data
assert b".fooclass" in res.data
assert b"My title" in res.data

@ -58,7 +58,7 @@ def live_server_setup(live_server):
# Where we POST to as a notification
@live_server.app.route('/test_notification_endpoint', methods=['POST'])
@live_server.app.route('/test_notification_endpoint', methods=['POST', 'GET'])
def test_notification_endpoint():
from flask import request

@ -85,12 +85,16 @@ class update_worker(threading.Thread):
if len(watch['notification_urls']):
print(">>> Notifications queued for UUID from watch {}".format(uuid))
n_object['notification_urls'] = watch['notification_urls']
n_object['notification_title'] = watch['notification_title']
n_object['notification_body'] = watch['notification_body']
self.notification_q.put(n_object)
# No? maybe theres a global setting, queue them all
elif len(self.datastore.data['settings']['application']['notification_urls']):
print(">>> Watch notification URLs were empty, using GLOBAL notifications for UUID: {}".format(uuid))
n_object['notification_urls'] = self.datastore.data['settings']['application']['notification_urls']
n_object['notification_title'] = self.datastore.data['settings']['application']['notification_title']
n_object['notification_body'] = self.datastore.data['settings']['application']['notification_body']
self.notification_q.put(n_object)
else:
print(">>> NO notifications queued, watch and global notification URLs were empty.")

Loading…
Cancel
Save