diff --git a/changedetectionio/__init__.py b/changedetectionio/__init__.py index 1370e5e2..89ccc786 100644 --- a/changedetectionio/__init__.py +++ b/changedetectionio/__init__.py @@ -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,8 +444,10 @@ 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) flash('Notifications queued.') @@ -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.') diff --git a/changedetectionio/forms.py b/changedetectionio/forms.py index f67d851e..4bc39215 100644 --- a/changedetectionio/forms.py +++ b/changedetectionio/forms.py @@ -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 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()]) diff --git a/changedetectionio/notification.py b/changedetectionio/notification.py index bd75359e..da015075 100644 --- a/changedetectionio/notification.py +++ b/changedetectionio/notification.py @@ -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 diff --git a/changedetectionio/store.py b/changedetectionio/store.py index 8a28f6b6..45fc9c95 100644 --- a/changedetectionio/store.py +++ b/changedetectionio/store.py @@ -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, diff --git a/changedetectionio/templates/_common_fields.jinja b/changedetectionio/templates/_common_fields.jinja new file mode 100644 index 00000000..184e6011 --- /dev/null +++ b/changedetectionio/templates/_common_fields.jinja @@ -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 %} \ No newline at end of file diff --git a/changedetectionio/templates/_helpers.jinja b/changedetectionio/templates/_helpers.jinja index 08a8815a..734fc7e0 100644 --- a/changedetectionio/templates/_helpers.jinja +++ b/changedetectionio/templates/_helpers.jinja @@ -22,4 +22,6 @@ </ul> {% endif %} </span> -{% endmacro %} \ No newline at end of file +{% endmacro %} + + diff --git a/changedetectionio/templates/edit.html b/changedetectionio/templates/edit.html index cd55da0c..4a3c02f9 100644 --- a/changedetectionio/templates/edit.html +++ b/changedetectionio/templates/edit.html @@ -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> - </div> - <div class="pure-controls"> - {{ render_field(form.trigger_check, rows=5) }} - </div> - </fieldset> + <div class="tab-pane-inner" id="notifications"> + <strong>Note: <i>These settings override the global settings.</i></strong> + {{ render_notifications_field(form) }} </div> + <div class="tab-pane-inner" id="filters"> <fieldset> <div class="pure-control-group"> diff --git a/changedetectionio/templates/settings.html b/changedetectionio/templates/settings.html index 94884435..f9850a55 100644 --- a/changedetectionio/templates/settings.html +++ b/changedetectionio/templates/settings.html @@ -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> - </div> - </fieldset> + <div class="tab-pane-inner" id="notifications"> + {{ render_notifications_field(form) }} </div> + <div class="tab-pane-inner" id="fetching"> <div class="pure-control-group"> {{ render_field(form.fetch_backend) }} diff --git a/changedetectionio/tests/test_notification.py b/changedetectionio/tests/test_notification.py index fcebb9f7..2feb36da 100644 --- a/changedetectionio/tests/test_notification.py +++ b/changedetectionio/tests/test_notification.py @@ -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 ) diff --git a/changedetectionio/tests/test_watch_fields_storage.py b/changedetectionio/tests/test_watch_fields_storage.py index 12f6474c..a2929b79 100644 --- a/changedetectionio/tests/test_watch_fields_storage.py +++ b/changedetectionio/tests/test_watch_fields_storage.py @@ -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 diff --git a/changedetectionio/tests/util.py b/changedetectionio/tests/util.py index 38e1c2b6..a24e6ae3 100644 --- a/changedetectionio/tests/util.py +++ b/changedetectionio/tests/util.py @@ -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 diff --git a/changedetectionio/update_worker.py b/changedetectionio/update_worker.py index a5f36bda..7e38d531 100644 --- a/changedetectionio/update_worker.py +++ b/changedetectionio/update_worker.py @@ -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.")