diff --git a/README.md b/README.md index 395e3ac8..839a5c13 100644 --- a/README.md +++ b/README.md @@ -90,10 +90,11 @@ Just some examples Self-hosted web page change monitoring notifications +Now you can also customise your notification content! + ### JSON API Monitoring -Detect changes and monitor data in JSON API's by using the built-in JSONPath selectors as a filter. - +Detect changes and monitor data in JSON API's by using the built-in JSONPath selectors as a filter / selector. ![image](https://user-images.githubusercontent.com/275001/125165842-0ce01980-e1dc-11eb-9e73-d8137dd162dc.png) @@ -101,7 +102,6 @@ This will re-parse the JSON and apply indent to the text, making it super easy t ![image](https://user-images.githubusercontent.com/275001/125165995-d9ea5580-e1dc-11eb-8030-f0deced2661a.png) - ### Proxy A proxy for ChangeDetection.io can be configured by setting environment the diff --git a/backend/__init__.py b/backend/__init__.py index 1b668087..3c9b650e 100644 --- a/backend/__init__.py +++ b/backend/__init__.py @@ -422,7 +422,8 @@ 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, + 'uuid': uuid} notification_q.put(n_object) flash('Notifications queued.') @@ -463,6 +464,8 @@ def changedetection_app(config=None, datastore_o=None): form.minutes_between_check.data = int(datastore.data['settings']['requests']['minutes_between_check']) form.notification_urls.data = datastore.data['settings']['application']['notification_urls'] form.extract_title_as_title.data = datastore.data['settings']['application']['extract_title_as_title'] + form.notification_title.data = datastore.data['settings']['application']['notification_title'] + form.notification_body.data = datastore.data['settings']['application']['notification_body'] # Password unset is a GET if request.values.get('removepassword') == 'true': @@ -476,6 +479,8 @@ def changedetection_app(config=None, datastore_o=None): datastore.data['settings']['application']['notification_urls'] = form.notification_urls.data datastore.data['settings']['requests']['minutes_between_check'] = form.minutes_between_check.data datastore.data['settings']['application']['extract_title_as_title'] = form.extract_title_as_title.data + datastore.data['settings']['application']['notification_title'] = form.notification_title.data + datastore.data['settings']['application']['notification_body'] = form.notification_body.data if len(form.notification_urls.data): import apprise @@ -823,39 +828,22 @@ def check_for_new_version(): app.config.exit.wait(86400) def notification_runner(): - while not app.config.exit.is_set(): try: # At the moment only one thread runs (single runner) n_object = notification_q.get(block=False) except queue.Empty: time.sleep(1) - pass else: - import apprise - - # Create an Apprise instance + # Process notifications try: - apobj = apprise.Apprise() - for url in n_object['notification_urls']: - apobj.add(url.strip()) - - n_body = n_object['watch_url'] - - # 65 - Append URL of instance to the notification if it is set. - base_url = os.getenv('BASE_URL') - if base_url != None: - n_body += "\n" + base_url - - apobj.notify( - body=n_body, - # @todo This should be configurable. - title="ChangeDetection.io Notification - {}".format(n_object['watch_url']) - ) + from backend import notification + notification.process_notification(n_object, datastore) except Exception as e: - print("Watch URL: {} Error {}".format(n_object['watch_url'],e)) + print("Watch URL: {} Error {}".format(n_object['watch_url'], e)) + # Thread runner to check every minute, look for new watches to feed into the Queue. diff --git a/backend/forms.py b/backend/forms.py index 72bda422..a2739030 100644 --- a/backend/forms.py +++ b/backend/forms.py @@ -152,3 +152,6 @@ class globalSettingsForm(Form): notification_urls = StringListField('Notification URL List') 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') + notification_body = TextAreaField('Notification Body') diff --git a/backend/notification.py b/backend/notification.py new file mode 100644 index 00000000..39579a43 --- /dev/null +++ b/backend/notification.py @@ -0,0 +1,54 @@ +import os +import apprise + +def process_notification(n_object, datastore): + apobj = apprise.Apprise() + for url in n_object['notification_urls']: + apobj.add(url.strip()) + + # 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'] + + # Insert variables into the notification content + notification_parameters = create_notification_parameters(n_object) + raw_notification_text = [n_body, n_title] + + parameterised_notification_text = dict( + [ + (i, n.replace(n, n.format(**notification_parameters))) + for i, n in zip(['body', 'title'], raw_notification_text) + ] + ) + + apobj.notify( + body=parameterised_notification_text["body"], + title=parameterised_notification_text["title"] + ) + + +# Notification title + body content parameters get created here. +def create_notification_parameters(n_object): + + # in the case we send a test notification from the main settings, there is no UUID. + uuid = n_object['uuid'] if 'uuid' in n_object else '' + + # Create URLs to customise the notification with + base_url = os.getenv('BASE_URL', '').strip('"') + watch_url = n_object['watch_url'] + + if base_url != '': + diff_url = "{}/diff/{}".format(base_url, uuid) + preview_url = "{}/preview/{}".format(base_url, uuid) + else: + diff_url = preview_url = '' + + return { + 'base_url': base_url, + 'watch_url': watch_url, + 'diff_url': diff_url, + 'preview_url': preview_url, + 'current_snapshot': n_object['current_snapshot'] if 'current_snapshot' in n_object else '' + } + diff --git a/backend/static/js/settings.js b/backend/static/js/settings.js new file mode 100644 index 00000000..c6eba772 --- /dev/null +++ b/backend/static/js/settings.js @@ -0,0 +1,16 @@ +window.addEventListener("load", (event) => { + // just an example for now + function toggleVisible(elem) { + // theres better ways todo this + var x = document.getElementById(elem); + if (x.style.display === "block") { + x.style.display = "none"; + } else { + x.style.display = "block"; + } + } + + document.getElementById("toggle-customise-notifications").onclick = function () { + toggleVisible("notification-customisation"); + }; +}); diff --git a/backend/static/styles/styles.css b/backend/static/styles/styles.css index 21e0e330..e1b1d831 100644 --- a/backend/static/styles/styles.css +++ b/backend/static/styles/styles.css @@ -102,6 +102,24 @@ body:after, body:before { -webkit-clip-path: polygon(100% 0, 0 0, 0 77.5%, 1% 77.4%, 2% 77.1%, 3% 76.6%, 4% 75.9%, 5% 75.05%, 6% 74.05%, 7% 72.95%, 8% 71.75%, 9% 70.55%, 10% 69.3%, 11% 68.05%, 12% 66.9%, 13% 65.8%, 14% 64.8%, 15% 64%, 16% 63.35%, 17% 62.85%, 18% 62.6%, 19% 62.5%, 20% 62.65%, 21% 63%, 22% 63.5%, 23% 64.2%, 24% 65.1%, 25% 66.1%, 26% 67.2%, 27% 68.4%, 28% 69.65%, 29% 70.9%, 30% 72.15%, 31% 73.3%, 32% 74.35%, 33% 75.3%, 34% 76.1%, 35% 76.75%, 36% 77.2%, 37% 77.45%, 38% 77.5%, 39% 77.3%, 40% 76.95%, 41% 76.4%, 42% 75.65%, 43% 74.75%, 44% 73.75%, 45% 72.6%, 46% 71.4%, 47% 70.15%, 48% 68.9%, 49% 67.7%, 50% 66.55%, 51% 65.5%, 52% 64.55%, 53% 63.75%, 54% 63.15%, 55% 62.75%, 56% 62.55%, 57% 62.5%, 58% 62.7%, 59% 63.1%, 60% 63.7%, 61% 64.45%, 62% 65.4%, 63% 66.45%, 64% 67.6%, 65% 68.8%, 66% 70.05%, 67% 71.3%, 68% 72.5%, 69% 73.6%, 70% 74.65%, 71% 75.55%, 72% 76.35%, 73% 76.9%, 74% 77.3%, 75% 77.5%, 76% 77.45%, 77% 77.25%, 78% 76.8%, 79% 76.2%, 80% 75.4%, 81% 74.45%, 82% 73.4%, 83% 72.25%, 84% 71.05%, 85% 69.8%, 86% 68.55%, 87% 67.35%, 88% 66.2%, 89% 65.2%, 90% 64.3%, 91% 63.55%, 92% 63%, 93% 62.65%, 94% 62.5%, 95% 62.55%, 96% 62.8%, 97% 63.3%, 98% 63.9%, 99% 64.75%, 100% 65.7%); clip-path: polygon(100% 0, 0 0, 0 77.5%, 1% 77.4%, 2% 77.1%, 3% 76.6%, 4% 75.9%, 5% 75.05%, 6% 74.05%, 7% 72.95%, 8% 71.75%, 9% 70.55%, 10% 69.3%, 11% 68.05%, 12% 66.9%, 13% 65.8%, 14% 64.8%, 15% 64%, 16% 63.35%, 17% 62.85%, 18% 62.6%, 19% 62.5%, 20% 62.65%, 21% 63%, 22% 63.5%, 23% 64.2%, 24% 65.1%, 25% 66.1%, 26% 67.2%, 27% 68.4%, 28% 69.65%, 29% 70.9%, 30% 72.15%, 31% 73.3%, 32% 74.35%, 33% 75.3%, 34% 76.1%, 35% 76.75%, 36% 77.2%, 37% 77.45%, 38% 77.5%, 39% 77.3%, 40% 76.95%, 41% 76.4%, 42% 75.65%, 43% 74.75%, 44% 73.75%, 45% 72.6%, 46% 71.4%, 47% 70.15%, 48% 68.9%, 49% 67.7%, 50% 66.55%, 51% 65.5%, 52% 64.55%, 53% 63.75%, 54% 63.15%, 55% 62.75%, 56% 62.55%, 57% 62.5%, 58% 62.7%, 59% 63.1%, 60% 63.7%, 61% 64.45%, 62% 65.4%, 63% 66.45%, 64% 67.6%, 65% 68.8%, 66% 70.05%, 67% 71.3%, 68% 72.5%, 69% 73.6%, 70% 74.65%, 71% 75.55%, 72% 76.35%, 73% 76.9%, 74% 77.3%, 75% 77.5%, 76% 77.45%, 77% 77.25%, 78% 76.8%, 79% 76.2%, 80% 75.4%, 81% 74.45%, 82% 73.4%, 83% 72.25%, 84% 71.05%, 85% 69.8%, 86% 68.55%, 87% 67.35%, 88% 66.2%, 89% 65.2%, 90% 64.3%, 91% 63.55%, 92% 63%, 93% 62.65%, 94% 62.5%, 95% 62.55%, 96% 62.8%, 97% 63.3%, 98% 63.9%, 99% 64.75%, 100% 65.7%); } +.arrow { + border: solid black; + border-width: 0 3px 3px 0; + display: inline-block; + padding: 3px; } + .arrow.right { + transform: rotate(-45deg); + -webkit-transform: rotate(-45deg); } + .arrow.left { + transform: rotate(135deg); + -webkit-transform: rotate(135deg); } + .arrow.up { + transform: rotate(-135deg); + -webkit-transform: rotate(-135deg); } + .arrow.down { + transform: rotate(45deg); + -webkit-transform: rotate(45deg); } + .button-small { font-size: 85%; } @@ -166,6 +184,18 @@ body:after, body:before { .messages li.notice { background: rgba(255, 255, 255, 0.5); } +#notification-customisation { + display: block; + border: 1px solid #ccc; + padding: 1rem; + border-radius: 5px; } + +#toggle-customise-notifications { + cursor: pointer; } + +#token-table.pure-table td, #token-table.pure-table th { + font-size: 80%; } + #new-watch-form { background: rgba(0, 0, 0, 0.05); padding: 1em; @@ -260,8 +290,7 @@ footer { .pure-form input[type=url] { width: 100%; } .pure-form textarea { - width: 100%; - font-size: 14px; } + width: 100%; } @media only screen and (max-width: 760px), (min-device-width: 768px) and (max-device-width: 1024px) { .box { diff --git a/backend/static/styles/styles.scss b/backend/static/styles/styles.scss index 33c1de38..8042dd1f 100644 --- a/backend/static/styles/styles.scss +++ b/backend/static/styles/styles.scss @@ -72,7 +72,6 @@ section.content { } } - .watch-tag-list { color: #e70069; white-space: nowrap; @@ -137,12 +136,33 @@ body:after, body:before { clip-path: polygon(100% 0, 0 0, 0 77.5%, 1% 77.4%, 2% 77.1%, 3% 76.6%, 4% 75.9%, 5% 75.05%, 6% 74.05%, 7% 72.95%, 8% 71.75%, 9% 70.55%, 10% 69.3%, 11% 68.05%, 12% 66.9%, 13% 65.8%, 14% 64.8%, 15% 64%, 16% 63.35%, 17% 62.85%, 18% 62.6%, 19% 62.5%, 20% 62.65%, 21% 63%, 22% 63.5%, 23% 64.2%, 24% 65.1%, 25% 66.1%, 26% 67.2%, 27% 68.4%, 28% 69.65%, 29% 70.9%, 30% 72.15%, 31% 73.3%, 32% 74.35%, 33% 75.3%, 34% 76.1%, 35% 76.75%, 36% 77.2%, 37% 77.45%, 38% 77.5%, 39% 77.3%, 40% 76.95%, 41% 76.4%, 42% 75.65%, 43% 74.75%, 44% 73.75%, 45% 72.6%, 46% 71.4%, 47% 70.15%, 48% 68.9%, 49% 67.7%, 50% 66.55%, 51% 65.5%, 52% 64.55%, 53% 63.75%, 54% 63.15%, 55% 62.75%, 56% 62.55%, 57% 62.5%, 58% 62.7%, 59% 63.1%, 60% 63.7%, 61% 64.45%, 62% 65.4%, 63% 66.45%, 64% 67.6%, 65% 68.8%, 66% 70.05%, 67% 71.3%, 68% 72.5%, 69% 73.6%, 70% 74.65%, 71% 75.55%, 72% 76.35%, 73% 76.9%, 74% 77.3%, 75% 77.5%, 76% 77.45%, 77% 77.25%, 78% 76.8%, 79% 76.2%, 80% 75.4%, 81% 74.45%, 82% 73.4%, 83% 72.25%, 84% 71.05%, 85% 69.8%, 86% 68.55%, 87% 67.35%, 88% 66.2%, 89% 65.2%, 90% 64.3%, 91% 63.55%, 92% 63%, 93% 62.65%, 94% 62.5%, 95% 62.55%, 96% 62.8%, 97% 63.3%, 98% 63.9%, 99% 64.75%, 100% 65.7%) } +.arrow { + border: solid black; + border-width: 0 3px 3px 0; + display: inline-block; + padding: 3px; + &.right { + transform: rotate(-45deg); + -webkit-transform: rotate(-45deg); + } + &.left { + transform: rotate(135deg); + -webkit-transform: rotate(135deg); + } + &.up { + transform: rotate(-135deg); + -webkit-transform: rotate(-135deg); + } + &.down { + transform: rotate(45deg); + -webkit-transform: rotate(45deg); + } +} .button-small { font-size: 85%; } - .fetch-error { padding-top: 1em; font-size: 60%; @@ -221,7 +241,24 @@ body:after, body:before { background: rgba(255, 255, 255, .5); } } +} +#notification-customisation { + display: block; + border: 1px solid #ccc; + padding: 1rem; + border-radius: 5px; +} + +#toggle-customise-notifications { + cursor: pointer; +} + + +#token-table { + &.pure-table td, &.pure-table th { + font-size: 80%; + } } #new-watch-form { @@ -351,7 +388,6 @@ footer { textarea { width: 100%; - font-size: 14px; } } @@ -441,5 +477,3 @@ and also iPads specifically. } } - - diff --git a/backend/store.py b/backend/store.py index 7ddabb76..ab38c543 100644 --- a/backend/store.py +++ b/backend/store.py @@ -39,7 +39,10 @@ class ChangeDetectionStore: 'application': { 'password': False, 'extract_title_as_title': False, - 'notification_urls': [] # Apprise URL list + 'notification_urls': [], # Apprise URL list + # Custom notification content + 'notification_title': 'ChangeDetection.io Notification - {watch_url}', + 'notification_body': '{base_url}' } } } @@ -176,7 +179,6 @@ class ChangeDetectionStore: @property def data(self): - has_unviewed = False for uuid, v in self.__data['watching'].items(): self.__data['watching'][uuid]['newest_history_key'] = self.get_newest_history_key(uuid) @@ -191,8 +193,6 @@ class ChangeDetectionStore: if not self.__data['watching'][uuid]['title']: self.__data['watching'][uuid]['title'] = None - - self.__data['has_unviewed'] = has_unviewed return self.__data @@ -355,7 +355,7 @@ class ChangeDetectionStore: if self.stop_thread: print("Shutting down datastore thread") return - + if self.needs_write: self.sync_to_json() time.sleep(3) diff --git a/backend/templates/edit.html b/backend/templates/edit.html index cdeb139b..7212c087 100644 --- a/backend/templates/edit.html +++ b/backend/templates/edit.html @@ -50,7 +50,8 @@ User-Agent: wonderbra 1.0") }} </fieldset> <div class="pure-control-group"> - {{ render_field(form.notification_urls, rows=5, placeholder="Gitter - gitter://token/room + {{ 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 diff --git a/backend/templates/settings.html b/backend/templates/settings.html index 177b834c..2b6d9422 100644 --- a/backend/templates/settings.html +++ b/backend/templates/settings.html @@ -2,45 +2,100 @@ {% block content %} {% from '_helpers.jinja' import render_field %} - +<script type="text/javascript" src="static/js/settings.js"></script> <div class="edit-form"> <form class="pure-form pure-form-stacked settings" action="/settings" method="POST"> <fieldset> <div class="pure-control-group"> {{ render_field(form.minutes_between_check, size=5) }} - <span class="pure-form-message-inline">Default time for all watches, when the watch does not have a specific time setting.</span> + <span class="pure-form-message-inline">Default time for all watches, when the watch does not have a specific time setting.</span> </div> <div class="pure-control-group"> {% if current_user.is_authenticated %} - <a href="/settings?removepassword=true" class="pure-button pure-button-primary">Remove password</a> + <a href="/settings?removepassword=true" class="pure-button pure-button-primary">Remove password</a> {% else %} - {{ render_field(form.password, size=10) }} - <span class="pure-form-message-inline">Password protection for your changedetection.io application.</span> + {{ render_field(form.password, size=10) }} + <span class="pure-form-message-inline">Password protection for your changedetection.io application.</span> {% endif %} </div> <div class="pure-control-group"> {{ render_field(form.extract_title_as_title) }} - <span class="pure-form-message-inline">Note: This will automatically apply to all existing watches.</span> + <span class="pure-form-message-inline">Note: This will automatically apply to all existing watches.</span> </div> - <div class="pure-control-group"> - {{ render_field(form.notification_urls, rows=5, placeholder="Gitter - gitter://token/room + + <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 -") }} - <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> - </div> - <div class="pure-controls"> - <span class="pure-form-message-inline"><label for="trigger-test-notification" class="pure-checkbox"> - <input type="checkbox" id="trigger-test-notification" name="trigger-test-notification"> Send test notification on save.</label></span> +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! + <a id="toggle-customise-notifications">Customise notification body: <i + class="arrow down"></i></a> + </div> + </div> + <div id="notification-customisation" style="display:none;"> + + <div class="pure-control-group"> + {{ render_field(form.notification_title, size=80) }} + <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>{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. + </span> + </div> </div> - <br/> + <span class="pure-form-message-inline"><label for="trigger-test-notification" class="pure-checkbox"> + <input type="checkbox" id="trigger-test-notification" name="trigger-test-notification"> Send test notification on save.</label> + </span> + + </div> <div class="pure-control-group"> <button type="submit" class="pure-button pure-button-primary">Save</button> </div> <br/> - <div class="pure-control-group"> <a href="/" class="pure-button button-small button-cancel">Back</a> <a href="/scrub" class="pure-button button-small button-cancel">Delete History Snapshot Data</a> diff --git a/backend/tests/conftest.py b/backend/tests/conftest.py index 1daf7d74..28414553 100644 --- a/backend/tests/conftest.py +++ b/backend/tests/conftest.py @@ -22,10 +22,18 @@ def app(request): except FileExistsError: pass - try: - os.unlink("{}/url-watches.json".format(datastore_path)) - except FileNotFoundError: - pass + # Enable a BASE_URL for notifications to work (so we can look for diff/ etc URLs) + os.environ["BASE_URL"] = "http://mysite.com/" + + # Unlink test output files + files = ['test-datastore/output.txt', + "{}/url-watches.json".format(datastore_path), + 'test-datastore/notification.txt'] + for file in files: + try: + os.unlink(file) + except FileNotFoundError: + pass app_config = {'datastore_path': datastore_path} datastore = store.ChangeDetectionStore(datastore_path=app_config['datastore_path'], include_default_watches=False) diff --git a/backend/tests/test_notification.py b/backend/tests/test_notification.py index 62a70260..83609804 100644 --- a/backend/tests/test_notification.py +++ b/backend/tests/test_notification.py @@ -1,4 +1,4 @@ - +import os import time from flask import url_for from . util import set_original_response, set_modified_response, live_server_setup @@ -22,7 +22,7 @@ def test_check_notification(client, live_server): ) assert b"1 Imported" in res.data - # Give the thread time to pick it up + # Give the thread time to pick up the first version time.sleep(3) # Goto the edit page, add our ignore text @@ -33,16 +33,34 @@ def test_check_notification(client, live_server): print (">>>> Notification URL: "+notification_url) res = client.post( url_for("edit_page", uuid="first"), - data={"notification_urls": notification_url, "url": test_url, "tag": "", "headers": ""}, + data={"notification_urls": notification_url, + "url": test_url, + "tag": "", + "headers": "", + "trigger_check": "y"}, follow_redirects=True ) assert b"Updated watch." in res.data + assert b"Notifications queued" in res.data # Hit the edit page, be sure that we saved it res = client.get( url_for("edit_page", uuid="first")) assert bytes(notification_url.encode('utf-8')) in res.data + + # Because we hit 'send test notification on save' + time.sleep(3) + + # 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 @@ -57,16 +75,48 @@ def test_check_notification(client, live_server): assert bytes("just now".encode('utf-8')) in res.data + # Verify what was sent as a notification + 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 - # Check it triggered - res = client.get( - url_for("test_notification_counter"), + # Re #65 - did we see our foobar.com BASE_URL ? + #assert bytes("https://foobar.com".encode('utf-8')) in notification_submission + + + ## Now configure something clever, we go into custom config (non-default) mode + + with open("test-datastore/output.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_body": "{base_url}\n{watch_url}\n{preview_url}\n{diff_url}\n{current_snapshot}\n:-)", + "minutes_between_check": 180}, + follow_redirects=True ) + assert b"Settings updated." in res.data - assert bytes("we hit it".encode('utf-8')) in res.data + # Trigger a check + client.get(url_for("api_watch_checknow"), follow_redirects=True) - # Did we see the URL that had a change, in the notification? - assert bytes("test-endpoint".encode('utf-8')) in res.data + # Give the thread time to pick it up + time.sleep(3) - # Re #65 - did we see our foobar.com BASE_URL ? - assert bytes("https://foobar.com".encode('utf-8')) in res.data + # Did the front end see it? + res = client.get( + url_for("index")) + + assert bytes("just now".encode('utf-8')) in res.data + + with open("test-datastore/notification.txt", "r") as f: + notification_submission = f.read() + + 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 diff --git a/backend/tests/util.py b/backend/tests/util.py index 05a63445..7aafa745 100644 --- a/backend/tests/util.py +++ b/backend/tests/util.py @@ -43,27 +43,18 @@ def live_server_setup(live_server): with open("test-datastore/output.txt", "r") as f: return f.read() + # Where we POST to as a notification @live_server.app.route('/test_notification_endpoint', methods=['POST']) def test_notification_endpoint(): from flask import request - with open("test-datastore/count.txt", "w") as f: - f.write("we hit it\n") + with open("test-datastore/notification.txt", "wb") as f: # Debug method, dump all POST to file also, used to prove #65 data = request.stream.read() if data != None: - f.write(str(data)) + f.write(data) print("\n>> Test notification endpoint was hit.\n") return "Text was set" - # And this should return not zero. - @live_server.app.route('/test_notification_counter') - def test_notification_counter(): - try: - with open("test-datastore/count.txt", "r") as f: - return f.read() - except FileNotFoundError: - return "nope :(" - - live_server.start() \ No newline at end of file + live_server.start() diff --git a/backend/update_worker.py b/backend/update_worker.py index 9f02f95c..7ae9f67f 100644 --- a/backend/update_worker.py +++ b/backend/update_worker.py @@ -40,26 +40,35 @@ class update_worker(threading.Thread): try: self.datastore.update_watch(uuid=uuid, update_obj=result) if changed_detected: - # A change was detected - self.datastore.save_history_text(uuid=uuid, contents=contents, result_obj=result) + # A change was detected + newest_version_file_contents = "" + self.datastore.save_history_text(uuid=uuid, contents=contents, result_obj=result) watch = self.datastore.data['watching'][uuid] + newest_key = self.datastore.get_newest_history_key(uuid) + if newest_key: + with open(watch['history'][newest_key], 'r') as f: + newest_version_file_contents = f.read().strip() + + n_object = { + 'watch_url': self.datastore.data['watching'][uuid]['url'], + 'uuid': uuid, + 'current_snapshot': newest_version_file_contents + } + # Did it have any notification alerts to hit? if len(watch['notification_urls']): print("Processing notifications for UUID: {}".format(uuid)) - n_object = {'watch_url': self.datastore.data['watching'][uuid]['url'], - 'notification_urls': watch['notification_urls']} + n_object['notification_urls'] = watch['notification_urls'] 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("Processing GLOBAL notifications for UUID: {}".format(uuid)) - n_object = {'watch_url': self.datastore.data['watching'][uuid]['url'], - 'notification_urls': self.datastore.data['settings']['application'][ - 'notification_urls']} + n_object['notification_urls'] = self.datastore.data['settings']['application']['notification_urls'] self.notification_q.put(n_object) + except Exception as e: print("!!!! Exception in update_worker !!!\n", e)