diff --git a/changedetectionio/__init__.py b/changedetectionio/__init__.py index 3c2ee355..542c2fa5 100644 --- a/changedetectionio/__init__.py +++ b/changedetectionio/__init__.py @@ -400,6 +400,37 @@ def changedetection_app(config=None, datastore_o=None): return output + + # AJAX endpoint for sending a test + @app.route("/notification/send-test", methods=['POST']) + @login_required + def ajax_callback_send_notification_test(): + + import apprise + apobj = apprise.Apprise() + + # validate URLS + if not len(request.form['notification_urls'].strip()): + return make_response({'error': 'No Notification URLs set'}, 400) + + for server_url in request.form['notification_urls'].splitlines(): + if not apobj.add(server_url): + message = '{} is not a valid AppRise URL.'.format(server_url) + return make_response({'error': message}, 400) + + try: + n_object = {'watch_url': request.form['window_url'], + 'notification_urls': request.form['notification_urls'].splitlines(), + 'notification_title': request.form['notification_title'].strip(), + 'notification_body': request.form['notification_body'].strip(), + 'notification_format': request.form['notification_format'].strip() + } + notification_q.put(n_object) + except Exception as e: + return make_response({'error': str(e)}, 400) + + return 'OK' + @app.route("/scrub", methods=['GET', 'POST']) @login_required def scrub_page(): @@ -561,20 +592,6 @@ def changedetection_app(config=None, datastore_o=None): # Queue the watch for immediate recheck update_q.put(uuid) - if form.trigger_check.data: - if len(form.notification_urls.data): - n_object = {'watch_url': form.url.data.strip(), - 'notification_urls': form.notification_urls.data, - 'notification_title': form.notification_title.data, - 'notification_body': form.notification_body.data, - 'notification_format': form.notification_format.data, - 'uuid': uuid - } - notification_q.put(n_object) - flash('Test notification queued.') - else: - flash('No notification URLs set, cannot send test.', 'error') - # Diff page [edit] link should go back to diff page if request.args.get("next") and request.args.get("next") == 'diff' and not form.save_and_preview_button.data: return redirect(url_for('diff_history_page', uuid=uuid)) @@ -650,19 +667,6 @@ def changedetection_app(config=None, datastore_o=None): datastore.data['settings']['application']['ignore_whitespace'] = form.ignore_whitespace.data datastore.data['settings']['application']['real_browser_save_screenshot'] = form.real_browser_save_screenshot.data - if form.trigger_check.data: - if len(form.notification_urls.data): - n_object = {'watch_url': "Test from changedetection.io!", - 'notification_urls': form.notification_urls.data, - 'notification_title': form.notification_title.data, - 'notification_body': form.notification_body.data, - 'notification_format': form.notification_format.data, - } - notification_q.put(n_object) - flash('Test notification queued.') - else: - flash('No notification URLs set, cannot send test.', 'error') - if not os.getenv("SALTED_PASS", False) and form.password.encrypted_password: datastore.data['settings']['application']['password'] = form.password.encrypted_password flash("Password protection enabled.", 'notice') @@ -1027,10 +1031,14 @@ def changedetection_app(config=None, datastore_o=None): flash("Error") return redirect(url_for('index')) + @app.route("/api/delete", methods=['GET']) @login_required def api_delete(): uuid = request.args.get('uuid') + # More for testing, possible to return the first/only + if uuid == 'first': + uuid = list(datastore.data['watching'].keys()).pop() datastore.delete(uuid) flash('Deleted.') diff --git a/changedetectionio/forms.py b/changedetectionio/forms.py index ae73cb85..4dc7921a 100644 --- a/changedetectionio/forms.py +++ b/changedetectionio/forms.py @@ -306,7 +306,6 @@ class commonSettingsForm(Form): notification_title = StringField('Notification Title', default=default_notification_title, validators=[validators.Optional(), ValidateTokensList()]) notification_body = TextAreaField('Notification Body', default=default_notification_body, validators=[validators.Optional(), ValidateTokensList()]) notification_format = SelectField('Notification Format', choices=valid_notification_formats.keys(), default=default_notification_format) - 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/static/js/notifications.js b/changedetectionio/static/js/notifications.js new file mode 100644 index 00000000..2dd09dec --- /dev/null +++ b/changedetectionio/static/js/notifications.js @@ -0,0 +1,42 @@ +$(document).ready(function() { + $('#send-test-notification').click(function (e) { + e.preventDefault(); + + // this can be global + var csrftoken = $('input[name=csrf_token]').val(); + $.ajaxSetup({ + beforeSend: function(xhr, settings) { + if (!/^(GET|HEAD|OPTIONS|TRACE)$/i.test(settings.type) && !this.crossDomain) { + xhr.setRequestHeader("X-CSRFToken", csrftoken) + } + } + }) + + data = { + window_url : window.location.href, + notification_urls : $('#notification_urls').val(), + notification_title : $('#notification_title').val(), + notification_body : $('#notification_body').val(), + notification_format : $('#notification_format').val(), + } + for (key in data) { + if (!data[key].length) { + alert(key+" is empty, cannot send test.") + return; + } + } + + $.ajax({ + type: "POST", + url: notification_base_url, + data : data + }).done(function(data){ + console.log(data); + alert('Sent'); + }).fail(function(data){ + console.log(data); + alert('Error: '+data.responseJSON.error); + }) + }); +}); + diff --git a/changedetectionio/templates/_common_fields.jinja b/changedetectionio/templates/_common_fields.jinja index 1f6df851..c4ae0506 100644 --- a/changedetectionio/templates/_common_fields.jinja +++ b/changedetectionio/templates/_common_fields.jinja @@ -18,6 +18,8 @@ <li>Go here for <a href="{{url_for('notification_logs')}}">Notification debug logs</a></li> </ul> </div> + <br/> + <a id="send-test-notification" class="pure-button button-secondary button-xsmall" style="font-size: 70%">Send test notification</a> </div> <div id="notification-customisation"> <div class="pure-control-group"> @@ -93,7 +95,4 @@ </span> </div> </div> - <div class="pure-control-group"> - {{ render_field(form.trigger_check) }} - </div> {% endmacro %} diff --git a/changedetectionio/templates/base.html b/changedetectionio/templates/base.html index d003feac..0b083c6c 100644 --- a/changedetectionio/templates/base.html +++ b/changedetectionio/templates/base.html @@ -17,6 +17,7 @@ background-image: url({{url_for('static_content', group='images', filename='gradient-border.png')}}); } </style> + <script type="text/javascript" src="{{url_for('static_content', group='js', filename='jquery-3.6.0.min.js')}}"></script> </head> <body> diff --git a/changedetectionio/templates/edit.html b/changedetectionio/templates/edit.html index b6e6e0d8..c82fff21 100644 --- a/changedetectionio/templates/edit.html +++ b/changedetectionio/templates/edit.html @@ -4,6 +4,10 @@ {% from '_helpers.jinja' import render_button %} {% from '_common_fields.jinja' import render_common_settings_form %} <script type="text/javascript" src="{{url_for('static_content', group='js', filename='tabs.js')}}" defer></script> +<script type="text/javascript" src="{{url_for('static_content', group='js', filename='notifications.js')}}" defer></script> +<script> + var notification_base_url="{{url_for('ajax_callback_send_notification_test')}}"; +</script> <div class="edit-form monospaced-textarea"> diff --git a/changedetectionio/templates/settings.html b/changedetectionio/templates/settings.html index e3145de3..8e0bd48c 100644 --- a/changedetectionio/templates/settings.html +++ b/changedetectionio/templates/settings.html @@ -6,7 +6,10 @@ <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> - +<script type="text/javascript" src="{{url_for('static_content', group='js', filename='notifications.js')}}" defer></script> +<script> + var notification_base_url="{{url_for('ajax_callback_send_notification_test')}}"; +</script> <div class="edit-form"> <div class="tabs collapsable"> <ul> diff --git a/changedetectionio/tests/test_notification.py b/changedetectionio/tests/test_notification.py index ac5591e9..c590f762 100644 --- a/changedetectionio/tests/test_notification.py +++ b/changedetectionio/tests/test_notification.py @@ -2,15 +2,17 @@ import os import time import re from flask import url_for -from . util import set_original_response, set_modified_response, live_server_setup +from . util import set_original_response, set_modified_response, set_more_modified_response, live_server_setup import logging from changedetectionio.notification import default_notification_body, default_notification_title +def test_setup(live_server): + live_server_setup(live_server) + # Hard to just add more live server URLs when one test is already running (I think) # So we add our test here (was in a different file) def test_check_notification(client, live_server): - live_server_setup(live_server) set_original_response() # Give the endpoint time to spin up @@ -49,84 +51,76 @@ def test_check_notification(client, live_server): notification_url = url.replace('http', 'json') print (">>>> Notification URL: "+notification_url) + + notification_form_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" + "Diff: {diff}\n" + "Diff Full: {diff_full}\n" + ":-)", + "notification_format": "Text"} + + notification_form_data.update({ + "url": test_url, + "tag": "my tag", + "title": "my title", + "headers": "", + "fetch_backend": "html_requests"}) + 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" - "Diff: {diff}\n" - "Diff Full: {diff_full}\n" - ":-)", - "notification_format": "Text", - "url": test_url, - "tag": "my tag", - "title": "my title", - "headers": "", - "fetch_backend": "html_requests", - "trigger_check": "y"}, + data=notification_form_data, follow_redirects=True ) assert b"Updated watch." in res.data - assert b"Test notification queued" in res.data + # Hit the edit page, be sure that we saved it + # Re #242 - wasnt saving? res = client.get( url_for("edit_page", uuid="first")) assert bytes(notification_url.encode('utf-8')) in res.data - - # Re #242 - wasnt saving? assert bytes("New ChangeDetection.io Notification".encode('utf-8')) in res.data - # Because we hit 'send test notification on save' + ## Now recheck, and it should have sent the notification time.sleep(3) + set_modified_response() notification_submission = None - # Verify what was sent as a notification, this file should exist - with open("test-datastore/notification.txt", "r") as f: - notification_submission = f.read() - # Did we see the URL that had a change, in the notification? - - assert test_url in notification_submission - - os.unlink("test-datastore/notification.txt") - - set_modified_response() - # Trigger a check client.get(url_for("api_watch_checknow"), follow_redirects=True) - - # Give the thread time to pick it up time.sleep(3) - - # Did the front end see it? - res = client.get( - url_for("index")) - - assert bytes("just now".encode('utf-8')) in res.data - - notification_submission=None - # Verify what was sent as a notification + # Verify what was sent as a notification, this file should exist with open("test-datastore/notification.txt", "r") as f: notification_submission = f.read() - # Did we see the URL that had a change, in the notification? - - assert test_url in notification_submission + os.unlink("test-datastore/notification.txt") + # Did we see the URL that had a change, in the notification? # Diff was correctly executed + assert test_url in notification_submission + assert ':-)' in notification_submission assert "Diff Full: Some initial text" in notification_submission assert "Diff: (changed) Which is across multiple lines" in notification_submission assert "(into ) which has this one new line" in notification_submission - + # Re #342 - check for accidental python byte encoding of non-utf8/string + assert "b'" not in notification_submission + assert re.search('Watch UUID: [0-9a-f]{8}(-[0-9a-f]{4}){3}-[0-9a-f]{12}', notification_submission, re.IGNORECASE) + assert "Watch title: my title" in notification_submission + assert "Watch tag: my tag" in notification_submission + assert "diff/" in notification_submission + assert "preview/" in notification_submission + assert ":-)" in notification_submission + assert "New ChangeDetection.io Notification - {}".format(test_url) in notification_submission if env_base_url: # Re #65 - did we see our BASE_URl ? @@ -135,50 +129,17 @@ def test_check_notification(client, live_server): else: logging.debug(">>> Skipping BASE_URL check") - ## Now configure something clever, we go into custom config (non-default) mode, this is returned by the endpoint - with open("test-datastore/endpoint-content.txt", "w") as f: - f.write(";jasdhflkjadshf kjhsdfkjl ahslkjf haslkjd hfaklsj hf\njl;asdhfkasj stuff we will detect\n") - res = client.post( - url_for("settings_page"), - 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", - }, - follow_redirects=True - ) - assert b"Settings updated." in res.data - # Re #143 - should not see this if we didnt hit the test box - assert b"Test notification queued" not in res.data - # Trigger a check + # This should insert the {current_snapshot} + set_more_modified_response() client.get(url_for("api_watch_checknow"), follow_redirects=True) - - # Give the thread time to pick it up time.sleep(3) - - # Did the front end see it? - res = client.get( - url_for("index")) - - assert bytes("just now".encode('utf-8')) in res.data - + # Verify what was sent as a notification, this file should exist with open("test-datastore/notification.txt", "r") as f: notification_submission = f.read() - print ("Notification submission was:", notification_submission) - # Re #342 - check for accidental python byte encoding of non-utf8/string - assert "b'" not in notification_submission - - assert re.search('Watch UUID: [0-9a-f]{8}(-[0-9a-f]{4}){3}-[0-9a-f]{12}', notification_submission, re.IGNORECASE) - assert "Watch title: my title" in notification_submission - assert "Watch tag: my tag" in notification_submission - assert "diff/" in notification_submission - assert "preview/" in notification_submission - assert ":-)" in notification_submission - assert "New ChangeDetection.io Notification - {}".format(test_url) in notification_submission - # This should insert the {current_snapshot} - assert "stuff we will detect" in notification_submission + assert "Ohh yeah awesome" in notification_submission + # Prove that "content constantly being marked as Changed with no Updating causes notification" is not a thing # https://github.com/dgtlmoon/changedetection.io/discussions/192 @@ -186,33 +147,39 @@ def test_check_notification(client, live_server): # Trigger a check client.get(url_for("api_watch_checknow"), follow_redirects=True) - time.sleep(3) + time.sleep(1) client.get(url_for("api_watch_checknow"), follow_redirects=True) - time.sleep(3) + time.sleep(1) client.get(url_for("api_watch_checknow"), follow_redirects=True) - time.sleep(3) + time.sleep(1) assert os.path.exists("test-datastore/notification.txt") == False + # cleanup for the next + client.get( + url_for("api_delete", uuid="first"), + follow_redirects=True + ) + - # Now adding a wrong token should give us an error +def test_notification_validation(client, live_server): + #live_server_setup(live_server) + time.sleep(3) + # re #242 - when you edited an existing new entry, it would not correctly show the notification settings + # Add our URL to the import page + test_url = url_for('test_endpoint', _external=True) res = client.post( - url_for("settings_page"), - data={"notification_title": "New ChangeDetection.io Notification - {watch_url}", - "notification_body": "Rubbish: {rubbish}\n", - "notification_format": "Text", - "notification_urls": "json://foobar.com", - "minutes_between_check": 180, - "fetch_backend": "html_requests" - }, + url_for("api_watch_add"), + data={"url": test_url, "tag": 'nice one'}, follow_redirects=True ) - - assert bytes("is not a valid token".encode('utf-8')) in res.data + with open("xxx.bin", "wb") as f: + f.write(res.data) + assert b"Watch added" in res.data # Re #360 some validation res = client.post( url_for("edit_page", uuid="first"), - data={"notification_urls": notification_url, + data={"notification_urls": 'json://localhost/foobar', "notification_title": "", "notification_body": "", "notification_format": "Text", @@ -220,8 +187,28 @@ def test_check_notification(client, live_server): "tag": "my tag", "title": "my title", "headers": "", - "fetch_backend": "html_requests", - "trigger_check": "y"}, + "fetch_backend": "html_requests"}, follow_redirects=True ) assert b"Notification Body and Title is required when a Notification URL is used" in res.data + + # 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", + "notification_format": "Text", + "notification_urls": "json://localhost/foobar", + "time_between_check": {'seconds': 180}, + "fetch_backend": "html_requests" + }, + follow_redirects=True + ) + + assert bytes("is not a valid token".encode('utf-8')) in res.data + + # cleanup for the next + client.get( + url_for("api_delete", uuid="first"), + follow_redirects=True + ) diff --git a/changedetectionio/tests/util.py b/changedetectionio/tests/util.py index 05fcca44..3c7e89e4 100644 --- a/changedetectionio/tests/util.py +++ b/changedetectionio/tests/util.py @@ -35,6 +35,24 @@ def set_modified_response(): return None +def set_more_modified_response(): + test_return_data = """<html> + <head><title>modified head title + + Some initial text
+

which has this one new line

+
+ So let's see what happens.
+ Ohh yeah awesome
+ + + """ + + with open("test-datastore/endpoint-content.txt", "w") as f: + f.write(test_return_data) + + return None + def live_server_setup(live_server): @@ -82,7 +100,7 @@ def live_server_setup(live_server): if data != None: f.write(data) - print("\n>> Test notification endpoint was hit.\n") + print("\n>> Test notification endpoint was hit.\n", data) return "Text was set"