Customisable notifications (#123)

* Customisable notifications (#121)
* Test improvements
* Setup BASE_URL environment in test

Co-authored-by: dtomlinson91 <53234158+dtomlinson91@users.noreply.github.com>
pull/91/head
dgtlmoon 3 years ago committed by GitHub
parent 655a350f50
commit dad48402f1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -90,10 +90,11 @@ Just some examples
<img src="https://raw.githubusercontent.com/dgtlmoon/changedetection.io/master/screenshot-notifications.png" style="max-width:100%;" alt="Self-hosted web page change monitoring notifications" title="Self-hosted web page change monitoring notifications" /> <img src="https://raw.githubusercontent.com/dgtlmoon/changedetection.io/master/screenshot-notifications.png" style="max-width:100%;" alt="Self-hosted web page change monitoring notifications" title="Self-hosted web page change monitoring notifications" />
### JSON API Monitoring Now you can also customise your notification content!
Detect changes and monitor data in JSON API's by using the built-in JSONPath selectors as a filter. ### JSON API Monitoring
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) ![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) ![image](https://user-images.githubusercontent.com/275001/125165995-d9ea5580-e1dc-11eb-8030-f0deced2661a.png)
### Proxy ### Proxy
A proxy for ChangeDetection.io can be configured by setting environment the A proxy for ChangeDetection.io can be configured by setting environment the

@ -422,7 +422,8 @@ def changedetection_app(config=None, datastore_o=None):
if form.trigger_check.data: if form.trigger_check.data:
n_object = {'watch_url': form.url.data.strip(), 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) notification_q.put(n_object)
flash('Notifications queued.') 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.minutes_between_check.data = int(datastore.data['settings']['requests']['minutes_between_check'])
form.notification_urls.data = datastore.data['settings']['application']['notification_urls'] 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.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 # Password unset is a GET
if request.values.get('removepassword') == 'true': 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']['application']['notification_urls'] = form.notification_urls.data
datastore.data['settings']['requests']['minutes_between_check'] = form.minutes_between_check.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']['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): if len(form.notification_urls.data):
import apprise import apprise
@ -823,39 +828,22 @@ def check_for_new_version():
app.config.exit.wait(86400) app.config.exit.wait(86400)
def notification_runner(): def notification_runner():
while not app.config.exit.is_set(): while not app.config.exit.is_set():
try: try:
# At the moment only one thread runs (single runner) # At the moment only one thread runs (single runner)
n_object = notification_q.get(block=False) n_object = notification_q.get(block=False)
except queue.Empty: except queue.Empty:
time.sleep(1) time.sleep(1)
pass
else: else:
import apprise # Process notifications
# Create an Apprise instance
try: try:
apobj = apprise.Apprise() from backend import notification
for url in n_object['notification_urls']: notification.process_notification(n_object, datastore)
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'])
)
except Exception as e: 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. # Thread runner to check every minute, look for new watches to feed into the Queue.

@ -152,3 +152,6 @@ class globalSettingsForm(Form):
notification_urls = StringListField('Notification URL List') notification_urls = StringListField('Notification URL List')
extract_title_as_title = BooleanField('Extract <title> from document and use as watch title') extract_title_as_title = BooleanField('Extract <title> from document and use as watch title')
trigger_check = BooleanField('Send test notification on save') trigger_check = BooleanField('Send test notification on save')
notification_title = StringField('Notification Title')
notification_body = TextAreaField('Notification Body')

@ -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 ''
}

@ -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");
};
});

@ -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%); -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%); } 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 { .button-small {
font-size: 85%; } font-size: 85%; }
@ -166,6 +184,18 @@ body:after, body:before {
.messages li.notice { .messages li.notice {
background: rgba(255, 255, 255, 0.5); } 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 { #new-watch-form {
background: rgba(0, 0, 0, 0.05); background: rgba(0, 0, 0, 0.05);
padding: 1em; padding: 1em;
@ -260,8 +290,7 @@ footer {
.pure-form input[type=url] { .pure-form input[type=url] {
width: 100%; } width: 100%; }
.pure-form textarea { .pure-form textarea {
width: 100%; width: 100%; }
font-size: 14px; }
@media only screen and (max-width: 760px), (min-device-width: 768px) and (max-device-width: 1024px) { @media only screen and (max-width: 760px), (min-device-width: 768px) and (max-device-width: 1024px) {
.box { .box {

@ -72,7 +72,6 @@ section.content {
} }
} }
.watch-tag-list { .watch-tag-list {
color: #e70069; color: #e70069;
white-space: nowrap; 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%) 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 { .button-small {
font-size: 85%; font-size: 85%;
} }
.fetch-error { .fetch-error {
padding-top: 1em; padding-top: 1em;
font-size: 60%; font-size: 60%;
@ -221,7 +241,24 @@ body:after, body:before {
background: rgba(255, 255, 255, .5); 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 { #new-watch-form {
@ -351,7 +388,6 @@ footer {
textarea { textarea {
width: 100%; width: 100%;
font-size: 14px;
} }
} }
@ -441,5 +477,3 @@ and also iPads specifically.
} }
} }

@ -39,7 +39,10 @@ class ChangeDetectionStore:
'application': { 'application': {
'password': False, 'password': False,
'extract_title_as_title': 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 @property
def data(self): def data(self):
has_unviewed = False has_unviewed = False
for uuid, v in self.__data['watching'].items(): for uuid, v in self.__data['watching'].items():
self.__data['watching'][uuid]['newest_history_key'] = self.get_newest_history_key(uuid) 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']: if not self.__data['watching'][uuid]['title']:
self.__data['watching'][uuid]['title'] = None self.__data['watching'][uuid]['title'] = None
self.__data['has_unviewed'] = has_unviewed self.__data['has_unviewed'] = has_unviewed
return self.__data return self.__data

@ -50,7 +50,8 @@ User-Agent: wonderbra 1.0") }}
</fieldset> </fieldset>
<div class="pure-control-group"> <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 Office365 - o365://TenantID:AccountEmail/ClientID/ClientSecret/TargetEmail
AWS SNS - sns://AccessKeyID/AccessSecretKey/RegionName/+PhoneNo AWS SNS - sns://AccessKeyID/AccessSecretKey/RegionName/+PhoneNo
SMTPS - mailtos://user:pass@mail.domain.com?to=receivingAddress@example.com SMTPS - mailtos://user:pass@mail.domain.com?to=receivingAddress@example.com

@ -2,45 +2,100 @@
{% block content %} {% block content %}
{% from '_helpers.jinja' import render_field %} {% from '_helpers.jinja' import render_field %}
<script type="text/javascript" src="static/js/settings.js"></script>
<div class="edit-form"> <div class="edit-form">
<form class="pure-form pure-form-stacked settings" action="/settings" method="POST"> <form class="pure-form pure-form-stacked settings" action="/settings" method="POST">
<fieldset> <fieldset>
<div class="pure-control-group"> <div class="pure-control-group">
{{ render_field(form.minutes_between_check, size=5) }} {{ 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>
<div class="pure-control-group"> <div class="pure-control-group">
{% if current_user.is_authenticated %} {% 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 %} {% else %}
{{ render_field(form.password, size=10) }} {{ render_field(form.password, size=10) }}
<span class="pure-form-message-inline">Password protection for your changedetection.io application.</span> <span class="pure-form-message-inline">Password protection for your changedetection.io application.</span>
{% endif %} {% endif %}
</div> </div>
<div class="pure-control-group"> <div class="pure-control-group">
{{ render_field(form.extract_title_as_title) }} {{ 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>
<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 Office365 - o365://TenantID:AccountEmail/ClientID/ClientSecret/TargetEmail
AWS SNS - sns://AccessKeyID/AccessSecretKey/RegionName/+PhoneNo AWS SNS - sns://AccessKeyID/AccessSecretKey/RegionName/+PhoneNo
SMTPS - mailtos://user:pass@mail.domain.com?to=receivingAddress@example.com 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!
<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> <a id="toggle-customise-notifications">Customise notification body: <i
</div> class="arrow down"></i></a>
<div class="pure-controls"> </div>
<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>
<div id="notification-customisation" style="display:none;">
<br/> <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>
<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"> <div class="pure-control-group">
<button type="submit" class="pure-button pure-button-primary">Save</button> <button type="submit" class="pure-button pure-button-primary">Save</button>
</div> </div>
<br/> <br/>
<div class="pure-control-group"> <div class="pure-control-group">
<a href="/" class="pure-button button-small button-cancel">Back</a> <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> <a href="/scrub" class="pure-button button-small button-cancel">Delete History Snapshot Data</a>

@ -22,10 +22,18 @@ def app(request):
except FileExistsError: except FileExistsError:
pass pass
try: # Enable a BASE_URL for notifications to work (so we can look for diff/ etc URLs)
os.unlink("{}/url-watches.json".format(datastore_path)) os.environ["BASE_URL"] = "http://mysite.com/"
except FileNotFoundError:
pass # 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} app_config = {'datastore_path': datastore_path}
datastore = store.ChangeDetectionStore(datastore_path=app_config['datastore_path'], include_default_watches=False) datastore = store.ChangeDetectionStore(datastore_path=app_config['datastore_path'], include_default_watches=False)

@ -1,4 +1,4 @@
import os
import time import time
from flask import url_for 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, live_server_setup
@ -22,7 +22,7 @@ def test_check_notification(client, live_server):
) )
assert b"1 Imported" in res.data 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) time.sleep(3)
# Goto the edit page, add our ignore text # Goto the edit page, add our ignore text
@ -33,16 +33,34 @@ def test_check_notification(client, live_server):
print (">>>> Notification URL: "+notification_url) print (">>>> Notification URL: "+notification_url)
res = client.post( res = client.post(
url_for("edit_page", uuid="first"), 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 follow_redirects=True
) )
assert b"Updated watch." in res.data assert b"Updated watch." in res.data
assert b"Notifications queued" in res.data
# Hit the edit page, be sure that we saved it # Hit the edit page, be sure that we saved it
res = client.get( res = client.get(
url_for("edit_page", uuid="first")) url_for("edit_page", uuid="first"))
assert bytes(notification_url.encode('utf-8')) in res.data 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() set_modified_response()
# Trigger a check # Trigger a check
@ -57,16 +75,48 @@ def test_check_notification(client, live_server):
assert bytes("just now".encode('utf-8')) in res.data 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 # Re #65 - did we see our foobar.com BASE_URL ?
res = client.get( #assert bytes("https://foobar.com".encode('utf-8')) in notification_submission
url_for("test_notification_counter"),
## 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)
# 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
# Did we see the URL that had a change, in the notification? with open("test-datastore/notification.txt", "r") as f:
assert bytes("test-endpoint".encode('utf-8')) in res.data notification_submission = f.read()
# Re #65 - did we see our foobar.com BASE_URL ? assert "diff/" in notification_submission
assert bytes("https://foobar.com".encode('utf-8')) in res.data 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

@ -43,27 +43,18 @@ def live_server_setup(live_server):
with open("test-datastore/output.txt", "r") as f: with open("test-datastore/output.txt", "r") as f:
return f.read() return f.read()
# 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'])
def test_notification_endpoint(): def test_notification_endpoint():
from flask import request from flask import request
with open("test-datastore/count.txt", "w") as f: with open("test-datastore/notification.txt", "wb") as f:
f.write("we hit it\n")
# Debug method, dump all POST to file also, used to prove #65 # Debug method, dump all POST to file also, used to prove #65
data = request.stream.read() data = request.stream.read()
if data != None: if data != None:
f.write(str(data)) f.write(data)
print("\n>> Test notification endpoint was hit.\n") print("\n>> Test notification endpoint was hit.\n")
return "Text was set" 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() live_server.start()

@ -40,26 +40,35 @@ class update_worker(threading.Thread):
try: try:
self.datastore.update_watch(uuid=uuid, update_obj=result) self.datastore.update_watch(uuid=uuid, update_obj=result)
if changed_detected: if changed_detected:
# A change was detected # A change was detected
newest_version_file_contents = ""
self.datastore.save_history_text(uuid=uuid, contents=contents, result_obj=result) self.datastore.save_history_text(uuid=uuid, contents=contents, result_obj=result)
watch = self.datastore.data['watching'][uuid] 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? # Did it have any notification alerts to hit?
if len(watch['notification_urls']): if len(watch['notification_urls']):
print("Processing notifications for UUID: {}".format(uuid)) print("Processing notifications for UUID: {}".format(uuid))
n_object = {'watch_url': self.datastore.data['watching'][uuid]['url'], n_object['notification_urls'] = watch['notification_urls']
'notification_urls': watch['notification_urls']}
self.notification_q.put(n_object) self.notification_q.put(n_object)
# No? maybe theres a global setting, queue them all # No? maybe theres a global setting, queue them all
elif len(self.datastore.data['settings']['application']['notification_urls']): elif len(self.datastore.data['settings']['application']['notification_urls']):
print("Processing GLOBAL notifications for UUID: {}".format(uuid)) print("Processing GLOBAL notifications for UUID: {}".format(uuid))
n_object = {'watch_url': self.datastore.data['watching'][uuid]['url'], n_object['notification_urls'] = self.datastore.data['settings']['application']['notification_urls']
'notification_urls': self.datastore.data['settings']['application'][
'notification_urls']}
self.notification_q.put(n_object) self.notification_q.put(n_object)
except Exception as e: except Exception as e:
print("!!!! Exception in update_worker !!!\n", e) print("!!!! Exception in update_worker !!!\n", e)

Loading…
Cancel
Save