Compare commits

..

17 Commits

Author SHA1 Message Date
dgtlmoon
460c724e51 0.45.2 2023-09-22 09:45:55 +02:00
dgtlmoon
dcf4bf37ed Code/Test - Improve testing for creating backups 2023-09-22 09:21:07 +02:00
dgtlmoon
e3cf22fc27 UI - Re-order notification field settings 2023-09-14 14:34:44 +02:00
dgtlmoon
d497db639e UI - Notifications - Tidyup - Hide the notification tokens but show with a button/link 2023-09-14 14:16:08 +02:00
dgtlmoon
7355ac8d21 UI - Notifications - Tweak discord help text 2023-09-14 13:55:48 +02:00
dgtlmoon
2f2d0ea0f2 RSS feeds - Fixing broken links from RSS index in some environments, refactor code (#152, #148, #1684, #1798) 2023-09-14 13:19:45 +02:00
dgtlmoon
a958e1fe20 UI - "recheck all" button should ignore blank/empty "tag" setting when set 2023-09-12 15:13:21 +02:00
dgtlmoon
5dc3b00ec6 Update README.md 2023-09-11 23:27:10 +02:00
dgtlmoon
8ac4757cd9 UI - Fix spelling error 2023-09-11 13:18:05 +02:00
dgtlmoon
2180bb256d UI - Make tgram:// and discord:// examples in notification settings link to how-to pages (#1785) 2023-09-11 10:22:35 +02:00
dgtlmoon
212f15ad5f Catch possible crash scenario for listing watches - date_created was missing on add (#1787) 2023-09-10 13:44:24 +02:00
dgtlmoon
22b2068208 Ability to select "No proxy" for a watch when you have proxy's configured 2023-09-08 14:14:47 +02:00
dgtlmoon
4916043055 Updating notification library - Adds support for Pushy, PushDeer, PushMe and Matrix attachment support (screenshots) 2023-09-08 12:40:23 +02:00
dgtlmoon
7bf13bad30 Update README.md 2023-09-07 16:46:21 +02:00
dgtlmoon
0aa2276afb UI - Fixing update for sort by "date created" or "#" in watch overview table ( #1775 ) 2023-09-07 10:36:34 +02:00
Tiago Ilieve
3b875e5a6a Add 'diff_patch' notification body token - This will allow the diff to be generated in the "unified patch format." (#1765) 2023-09-07 08:55:06 +02:00
Constantin Hong
8ec50294d2 README/docs: Clarifying xpath version changedetection.io uses (#1773) 2023-09-07 08:49:26 +02:00
20 changed files with 237 additions and 83 deletions

View File

@@ -74,7 +74,7 @@ _Need an actual Chrome runner with Javascript support? We support fetching via W
#### Key Features #### Key Features
- Lots of trigger filters, such as "Trigger on text", "Remove text by selector", "Ignore text", "Extract text", also using regular-expressions! - Lots of trigger filters, such as "Trigger on text", "Remove text by selector", "Ignore text", "Extract text", also using regular-expressions!
- Target elements with xPath and CSS Selectors, Easily monitor complex JSON with JSONPath or jq - Target elements with xPath(1.0) and CSS Selectors, Easily monitor complex JSON with JSONPath or jq
- Switch between fast non-JS and Chrome JS based "fetchers" - Switch between fast non-JS and Chrome JS based "fetchers"
- Track changes in PDF files (Monitor text changed in the PDF, Also monitor PDF filesize and checksums) - Track changes in PDF files (Monitor text changed in the PDF, Also monitor PDF filesize and checksums)
- Easily specify how often a site should be checked - Easily specify how often a site should be checked
@@ -86,7 +86,7 @@ _Need an actual Chrome runner with Javascript support? We support fetching via W
We [recommend and use Bright Data](https://brightdata.grsm.io/n0r16zf7eivq) global proxy services, Bright Data will match any first deposit up to $100 using our signup link. We [recommend and use Bright Data](https://brightdata.grsm.io/n0r16zf7eivq) global proxy services, Bright Data will match any first deposit up to $100 using our signup link.
[OxyLabs](https://oxylabs.io) is also an excellent proxy provider and well worth using, they offer Residental, ISP, Rotating and many other proxy types to suit your project. [Oxylabs](https://oxylabs.go2cloud.org/SH2d) is also an excellent proxy provider and well worth using, they offer Residental, ISP, Rotating and many other proxy types to suit your project.
Please :star: star :star: this project and help it grow! https://github.com/dgtlmoon/changedetection.io/ Please :star: star :star: this project and help it grow! https://github.com/dgtlmoon/changedetection.io/
@@ -147,7 +147,7 @@ See the wiki for more information https://github.com/dgtlmoon/changedetection.io
## Filters ## Filters
XPath, JSONPath, jq, and CSS support comes baked in! You can be as specific as you need, use XPath exported from various XPath element query creation tools. XPath(1.0), JSONPath, jq, and CSS support comes baked in! You can be as specific as you need, use XPath exported from various XPath element query creation tools.
(We support LXML `re:test`, `re:match` and `re:replace`.) (We support LXML `re:test`, `re:match` and `re:replace`.)
## Notifications ## Notifications
@@ -226,7 +226,7 @@ The application also supports notifying you that it can follow this information
## Proxy Configuration ## Proxy Configuration
See the wiki https://github.com/dgtlmoon/changedetection.io/wiki/Proxy-configuration , we also support using [BrightData proxy services where possible]( https://github.com/dgtlmoon/changedetection.io/wiki/Proxy-configuration#brightdata-proxy-support) See the wiki https://github.com/dgtlmoon/changedetection.io/wiki/Proxy-configuration , we also support using [Bright Data proxy services where possible]( https://github.com/dgtlmoon/changedetection.io/wiki/Proxy-configuration#brightdata-proxy-support)
## Raspberry Pi support? ## Raspberry Pi support?

View File

@@ -38,7 +38,9 @@ from flask_paginate import Pagination, get_page_parameter
from changedetectionio import html_tools from changedetectionio import html_tools
from changedetectionio.api import api_v1 from changedetectionio.api import api_v1
__version__ = '0.45.1' __version__ = '0.45.2'
from changedetectionio.store import BASE_URL_NOT_SET_TEXT
datastore = None datastore = None
@@ -356,12 +358,10 @@ def changedetection_app(config=None, datastore_o=None):
# Include a link to the diff page, they will have to login here to see if password protection is enabled. # Include a link to the diff page, they will have to login here to see if password protection is enabled.
# Description is the page you watch, link takes you to the diff JS UI page # Description is the page you watch, link takes you to the diff JS UI page
# Dict val base_url will get overriden with the env var if it is set. # Dict val base_url will get overriden with the env var if it is set.
ext_base_url = datastore.data['settings']['application'].get('base_url') ext_base_url = datastore.data['settings']['application'].get('active_base_url')
if ext_base_url:
# Go with overriden value # Because we are called via whatever web server, flask should figure out the right path (
diff_link = {'href': "{}{}".format(ext_base_url, url_for('diff_history_page', uuid=watch['uuid'], _external=False))} diff_link = {'href': url_for('diff_history_page', uuid=watch['uuid'], _external=True)}
else:
diff_link = {'href': url_for('diff_history_page', uuid=watch['uuid'], _external=True)}
fe.link(link=diff_link) fe.link(link=diff_link)
@@ -714,7 +714,6 @@ def changedetection_app(config=None, datastore_o=None):
output = render_template("edit.html", output = render_template("edit.html",
available_processors=processors.available_processors(), available_processors=processors.available_processors(),
browser_steps_config=browser_step_ui_config, browser_steps_config=browser_step_ui_config,
current_base_url=datastore.data['settings']['application']['base_url'],
emailprefix=os.getenv('NOTIFICATION_MAIL_BUTTON_PREFIX', False), emailprefix=os.getenv('NOTIFICATION_MAIL_BUTTON_PREFIX', False),
form=form, form=form,
has_default_notification_urls=True if len(datastore.data['settings']['application']['notification_urls']) else False, has_default_notification_urls=True if len(datastore.data['settings']['application']['notification_urls']) else False,
@@ -804,7 +803,6 @@ def changedetection_app(config=None, datastore_o=None):
output = render_template("settings.html", output = render_template("settings.html",
form=form, form=form,
current_base_url = datastore.data['settings']['application']['base_url'],
hide_remove_pass=os.getenv("SALTED_PASS", False), hide_remove_pass=os.getenv("SALTED_PASS", False),
api_key=datastore.data['settings']['application'].get('api_access_token'), api_key=datastore.data['settings']['application'].get('api_access_token'),
emailprefix=os.getenv('NOTIFICATION_MAIL_BUTTON_PREFIX', False), emailprefix=os.getenv('NOTIFICATION_MAIL_BUTTON_PREFIX', False),
@@ -1270,10 +1268,10 @@ def changedetection_app(config=None, datastore_o=None):
update_q.put(queuedWatchMetaData.PrioritizedItem(priority=1, item={'uuid': uuid, 'skip_when_checksum_same': False})) update_q.put(queuedWatchMetaData.PrioritizedItem(priority=1, item={'uuid': uuid, 'skip_when_checksum_same': False}))
i = 1 i = 1
elif tag != None: elif tag:
# Items that have this current tag # Items that have this current tag
for watch_uuid, watch in datastore.data['watching'].items(): for watch_uuid, watch in datastore.data['watching'].items():
if (tag != None and tag in watch.get('tags', {})): if tag in watch.get('tags', {}):
if watch_uuid not in running_uuids and not datastore.data['watching'][watch_uuid]['paused']: if watch_uuid not in running_uuids and not datastore.data['watching'][watch_uuid]['paused']:
update_q.put( update_q.put(
queuedWatchMetaData.PrioritizedItem(priority=1, item={'uuid': watch_uuid, 'skip_when_checksum_same': False}) queuedWatchMetaData.PrioritizedItem(priority=1, item={'uuid': watch_uuid, 'skip_when_checksum_same': False})

View File

@@ -35,15 +35,19 @@ def customSequenceMatcher(before, after, include_equal=False, include_removed=Tr
# only_differences - only return info about the differences, no context # only_differences - only return info about the differences, no context
# line_feed_sep could be "<br>" or "<li>" or "\n" etc # line_feed_sep could be "<br>" or "<li>" or "\n" etc
def render_diff(previous_version_file_contents, newest_version_file_contents, include_equal=False, include_removed=True, include_added=True, include_replaced=True, line_feed_sep="\n", include_change_type_prefix=True): def render_diff(previous_version_file_contents, newest_version_file_contents, include_equal=False, include_removed=True, include_added=True, include_replaced=True, line_feed_sep="\n", include_change_type_prefix=True, patch_format=False):
newest_version_file_contents = [line.rstrip() for line in newest_version_file_contents.splitlines()] newest_version_file_contents = [line.rstrip() for line in newest_version_file_contents.splitlines()]
if previous_version_file_contents: if previous_version_file_contents:
previous_version_file_contents = [line.rstrip() for line in previous_version_file_contents.splitlines()] previous_version_file_contents = [line.rstrip() for line in previous_version_file_contents.splitlines()]
else: else:
previous_version_file_contents = "" previous_version_file_contents = ""
if patch_format:
patch = difflib.unified_diff(previous_version_file_contents, newest_version_file_contents)
return line_feed_sep.join(patch)
rendered_diff = customSequenceMatcher(before=previous_version_file_contents, rendered_diff = customSequenceMatcher(before=previous_version_file_contents,
after=newest_version_file_contents, after=newest_version_file_contents,
include_equal=include_equal, include_equal=include_equal,

View File

@@ -502,7 +502,10 @@ class globalSettingsRequestForm(Form):
class globalSettingsApplicationForm(commonSettingsForm): class globalSettingsApplicationForm(commonSettingsForm):
api_access_token_enabled = BooleanField('API access token security check enabled', default=True, validators=[validators.Optional()]) api_access_token_enabled = BooleanField('API access token security check enabled', default=True, validators=[validators.Optional()])
base_url = StringField('Base URL', validators=[validators.Optional()]) base_url = StringField('Notification base URL override',
validators=[validators.Optional()],
render_kw={"placeholder": os.getenv('BASE_URL', 'Not set')}
)
empty_pages_are_a_change = BooleanField('Treat empty pages as a change?', default=False) empty_pages_are_a_change = BooleanField('Treat empty pages as a change?', default=False)
fetch_backend = RadioField('Fetch Method', default="html_requests", choices=content_fetcher.available_fetchers(), validators=[ValidateContentFetcherIsReady()]) fetch_backend = RadioField('Fetch Method', default="html_requests", choices=content_fetcher.available_fetchers(), validators=[ValidateContentFetcherIsReady()])
global_ignore_text = StringListField('Ignore Text', [ValidateListRegex()]) global_ignore_text = StringListField('Ignore Text', [ValidateListRegex()])

View File

@@ -9,6 +9,7 @@ valid_tokens = {
'diff': '', 'diff': '',
'diff_added': '', 'diff_added': '',
'diff_full': '', 'diff_full': '',
'diff_patch': '',
'diff_removed': '', 'diff_removed': '',
'diff_url': '', 'diff_url': '',
'preview_url': '', 'preview_url': '',
@@ -98,7 +99,7 @@ def process_notification(n_object, datastore):
# Initially text or whatever # Initially text or whatever
n_format = datastore.data['settings']['application'].get('notification_format', valid_notification_formats[default_notification_format]) n_format = datastore.data['settings']['application'].get('notification_format', valid_notification_formats[default_notification_format])
# https://github.com/caronc/apprise/wiki/Development_LogCapture # https://github.com/caronc/apprise/wiki/Development_LogCapture
# Anything higher than or equal to WARNING (which covers things like Connection errors) # Anything higher than or equal to WARNING (which covers things like Connection errors)
# raise it as an exception # raise it as an exception
@@ -177,7 +178,7 @@ def process_notification(n_object, datastore):
log_value = logs.getvalue() log_value = logs.getvalue()
if log_value and 'WARNING' in log_value or 'ERROR' in log_value: if log_value and 'WARNING' in log_value or 'ERROR' in log_value:
raise Exception(log_value) raise Exception(log_value)
sent_objs.append({'title': n_title, sent_objs.append({'title': n_title,
'body': n_body, 'body': n_body,
'url' : url, 'url' : url,
@@ -207,15 +208,11 @@ def create_notification_parameters(n_object, datastore):
watch_tag = '' watch_tag = ''
# Create URLs to customise the notification with # Create URLs to customise the notification with
base_url = datastore.data['settings']['application']['base_url'] # active_base_url - set in store.py data property
base_url = datastore.data['settings']['application'].get('active_base_url')
watch_url = n_object['watch_url'] watch_url = n_object['watch_url']
# Re #148 - Some people have just {{ base_url }} in the body or title, but this may break some notification services
# like 'Join', so it's always best to atleast set something obvious so that they are not broken.
if base_url == '':
base_url = "<base-url-env-var-not-set>"
diff_url = "{}/diff/{}".format(base_url, uuid) diff_url = "{}/diff/{}".format(base_url, uuid)
preview_url = "{}/preview/{}".format(base_url, uuid) preview_url = "{}/preview/{}".format(base_url, uuid)
@@ -225,11 +222,12 @@ def create_notification_parameters(n_object, datastore):
# Valid_tokens also used as a field validator # Valid_tokens also used as a field validator
tokens.update( tokens.update(
{ {
'base_url': base_url if base_url is not None else '', 'base_url': base_url,
'current_snapshot': n_object['current_snapshot'] if 'current_snapshot' in n_object else '', 'current_snapshot': n_object['current_snapshot'] if 'current_snapshot' in n_object else '',
'diff': n_object.get('diff', ''), # Null default in the case we use a test 'diff': n_object.get('diff', ''), # Null default in the case we use a test
'diff_added': n_object.get('diff_added', ''), # Null default in the case we use a test 'diff_added': n_object.get('diff_added', ''), # Null default in the case we use a test
'diff_full': n_object.get('diff_full', ''), # Null default in the case we use a test 'diff_full': n_object.get('diff_full', ''), # Null default in the case we use a test
'diff_patch': n_object.get('diff_patch', ''), # Null default in the case we use a test
'diff_removed': n_object.get('diff_removed', ''), # Null default in the case we use a test 'diff_removed': n_object.get('diff_removed', ''), # Null default in the case we use a test
'diff_url': diff_url, 'diff_url': diff_url,
'preview_url': preview_url, 'preview_url': preview_url,

View File

@@ -2,6 +2,8 @@
# exit when any command fails # exit when any command fails
set -e set -e
# enable debug
set -x
# Test proxy list handling, starting two squids on different ports # Test proxy list handling, starting two squids on different ports
# Each squid adds a different header to the response, which is the main thing we test for. # Each squid adds a different header to the response, which is the main thing we test for.
@@ -19,7 +21,6 @@ docker run --network changedet-network -d \
## 2nd test actually choose the preferred proxy from proxies.json ## 2nd test actually choose the preferred proxy from proxies.json
docker run --network changedet-network \ docker run --network changedet-network \
-v `pwd`/tests/proxy_list/proxies.json-example:/app/changedetectionio/test-datastore/proxies.json \ -v `pwd`/tests/proxy_list/proxies.json-example:/app/changedetectionio/test-datastore/proxies.json \
test-changedetectionio \ test-changedetectionio \
@@ -44,7 +45,6 @@ fi
# Test the UI configurable proxies # Test the UI configurable proxies
docker run --network changedet-network \ docker run --network changedet-network \
test-changedetectionio \ test-changedetectionio \
bash -c 'cd changedetectionio && pytest tests/proxy_list/test_select_custom_proxy.py' bash -c 'cd changedetectionio && pytest tests/proxy_list/test_select_custom_proxy.py'
@@ -58,4 +58,25 @@ then
exit 1 exit 1
fi fi
# Test "no-proxy" option
docker run --network changedet-network \
test-changedetectionio \
bash -c 'cd changedetectionio && pytest tests/proxy_list/test_noproxy.py'
# We need to handle grep returning 1
set +e
# Check request was never seen in any container
for c in $(echo "squid-one squid-two squid-custom"); do
echo Checking $c
docker logs $c &> $c.txt
grep noproxy $c.txt
if [ $? -ne 1 ]
then
echo "Saw request for noproxy in $c container"
cat $c.txt
exit 1
fi
done
docker kill squid-one squid-two squid-custom docker kill squid-one squid-two squid-custom

View File

@@ -32,5 +32,10 @@ $(document).ready(function () {
window.getSelection().removeAllRanges(); window.getSelection().removeAllRanges();
}); });
$("#notification-token-toggle").click(function (e) {
e.preventDefault();
$('#notification-tokens-info').toggle();
});
}); });

View File

@@ -42,4 +42,8 @@ $(document).ready(function () {
$('#notification_urls').val(''); $('#notification_urls').val('');
e.preventDefault(); e.preventDefault();
}); });
$("#notification-token-toggle").click(function (e) {
e.preventDefault();
$('#notification-tokens-info').toggle();
});
}); });

View File

@@ -1,3 +1,5 @@
from distutils.util import strtobool
from flask import ( from flask import (
flash flash
) )
@@ -16,6 +18,9 @@ import threading
import time import time
import uuid as uuid_builder import uuid as uuid_builder
# Because the server will run as a daemon and wont know the URL for notification links when firing off a notification
BASE_URL_NOT_SET_TEXT = '("Base URL" not set - see settings - notifications)'
dictfilt = lambda x, y: dict([ (i,x[i]) for i in x if i in set(y) ]) dictfilt = lambda x, y: dict([ (i,x[i]) for i in x if i in set(y) ])
# Is there an existing library to ensure some data store (JSON etc) is in sync with CRUD methods? # Is there an existing library to ensure some data store (JSON etc) is in sync with CRUD methods?
@@ -173,12 +178,21 @@ class ChangeDetectionStore:
@property @property
def data(self): def data(self):
# Re #152, Return env base_url if not overriden, @todo also prefer the proxy pass url # Re #152, Return env base_url if not overriden
env_base_url = os.getenv('BASE_URL','') # Re #148 - Some people have just {{ base_url }} in the body or title, but this may break some notification services
if not self.__data['settings']['application']['base_url']: # like 'Join', so it's always best to atleast set something obvious so that they are not broken.
self.__data['settings']['application']['base_url'] = env_base_url.strip('" ')
return self.__data active_base_url = BASE_URL_NOT_SET_TEXT
if self.__data['settings']['application'].get('base_url'):
active_base_url = self.__data['settings']['application'].get('base_url')
elif os.getenv('BASE_URL'):
active_base_url = os.getenv('BASE_URL')
# I looked at various ways todo the following, but in the end just copying the dict seemed simplest/most reliable
# even given the memory tradeoff - if you know a better way.. maybe return d|self.__data.. or something
d = self.__data
d['settings']['application']['active_base_url'] = active_base_url.strip('" ')
return d
# Delete a single watch by UUID # Delete a single watch by UUID
def delete(self, uuid): def delete(self, uuid):
@@ -325,6 +339,9 @@ class ChangeDetectionStore:
if k in apply_extras: if k in apply_extras:
del apply_extras[k] del apply_extras[k]
if not apply_extras.get('date_created'):
apply_extras['date_created'] = int(time.time())
new_watch.update(apply_extras) new_watch.update(apply_extras)
new_watch.ensure_data_dir_exists() new_watch.ensure_data_dir_exists()
self.__data['watching'][new_uuid] = new_watch self.__data['watching'][new_uuid] = new_watch
@@ -468,6 +485,8 @@ class ChangeDetectionStore:
k = "ui-" + str(i) + proxy.get('proxy_name') k = "ui-" + str(i) + proxy.get('proxy_name')
proxy_list[k] = {'label': proxy.get('proxy_name'), 'url': proxy.get('proxy_url')} proxy_list[k] = {'label': proxy.get('proxy_name'), 'url': proxy.get('proxy_url')}
if proxy_list and strtobool(os.getenv('ENABLE_NO_PROXY_OPTION', 'True')):
proxy_list["no-proxy"] = {'label': "No proxy", 'url': ''}
return proxy_list if len(proxy_list) else None return proxy_list if len(proxy_list) else None
@@ -485,6 +504,9 @@ class ChangeDetectionStore:
# If it's a valid one # If it's a valid one
watch = self.data['watching'].get(uuid) watch = self.data['watching'].get(uuid)
if strtobool(os.getenv('ENABLE_NO_PROXY_OPTION', 'True')) and watch.get('proxy') == "no-proxy":
return None
if watch.get('proxy') and watch.get('proxy') in list(self.proxy_list.keys()): if watch.get('proxy') and watch.get('proxy') in list(self.proxy_list.keys()):
return watch.get('proxy') return watch.get('proxy')
@@ -778,15 +800,6 @@ class ChangeDetectionStore:
continue continue
return return
# We don't know when the date_created was in the past until now, so just add an index number for now.
def update_11(self):
i = 0
for uuid, watch in self.data['watching'].items():
if not watch.get('date_created'):
watch['date_created'] = i
i+=1
return
# Create tag objects and their references from existing tag text # Create tag objects and their references from existing tag text
def update_12(self): def update_12(self):
i = 0 i = 0
@@ -800,3 +813,11 @@ class ChangeDetectionStore:
self.data['watching'][uuid]['tags'] = tag_uuids self.data['watching'][uuid]['tags'] = tag_uuids
# #1775 - Update 11 did not update the records correctly when adding 'date_created' values for sorting
def update_13(self):
i = 0
for uuid, watch in self.data['watching'].items():
if not watch.get('date_created'):
self.data['watching'][uuid]['date_created'] = i
i+=1
return

View File

@@ -13,9 +13,9 @@
<div class="pure-form-message-inline"> <div class="pure-form-message-inline">
<ul> <ul>
<li>Use <a target=_new href="https://github.com/caronc/apprise">AppRise URLs</a> for notification to just about any service! <i><a target=_new href="https://github.com/dgtlmoon/changedetection.io/wiki/Notification-configuration-notes">Please read the notification services wiki here for important configuration notes</a></i>.</li> <li>Use <a target=_new href="https://github.com/caronc/apprise">AppRise URLs</a> for notification to just about any service! <i><a target=_new href="https://github.com/dgtlmoon/changedetection.io/wiki/Notification-configuration-notes">Please read the notification services wiki here for important configuration notes</a></i>.</li>
<li><code>discord://</code> only supports a maximum <strong>2,000 characters</strong> of notification text, including the title.</li> <li><code><a target=_new href="https://github.com/caronc/apprise/wiki/Notify_discord">discord://</a></code> (or <code>https://discord.com/api/webhooks...</code>)) </code> only supports a maximum <strong>2,000 characters</strong> of notification text, including the title.</li>
<li><code>tgram://</code> bots cant send messages to other bots, so you should specify chat ID of non-bot user.</li> <li><code><a target=_new href="https://github.com/caronc/apprise/wiki/Notify_telegram">tgram://</a></code> bots can't send messages to other bots, so you should specify chat ID of non-bot user.</li>
<li><code>tgram://</code> only supports very limited HTML and can fail when extra tags are sent, <a href="https://core.telegram.org/bots/api#html-style">read more here</a> (or use plaintext/markdown format)</li> <li><code><a target=_new href="https://github.com/caronc/apprise/wiki/Notify_telegram">tgram://</a></code> only supports very limited HTML and can fail when extra tags are sent, <a href="https://core.telegram.org/bots/api#html-style">read more here</a> (or use plaintext/markdown format)</li>
<li><code>gets://</code>, <code>posts://</code>, <code>puts://</code>, <code>deletes://</code> for direct API calls (or omit the "<code>s</code>" for non-SSL ie <code>get://</code>)</li> <li><code>gets://</code>, <code>posts://</code>, <code>puts://</code>, <code>deletes://</code> for direct API calls (or omit the "<code>s</code>" for non-SSL ie <code>get://</code>)</li>
<li>Accepts the <code>{{ '{{token}}' }}</code> placeholders listed below</li> <li>Accepts the <code>{{ '{{token}}' }}</code> placeholders listed below</li>
</ul> </ul>
@@ -35,18 +35,14 @@
</div> </div>
<div class="pure-control-group"> <div class="pure-control-group">
{{ render_field(form.notification_body , rows=5, class="notification-body", placeholder=settings_application['notification_body']) }} {{ render_field(form.notification_body , rows=5, class="notification-body", placeholder=settings_application['notification_body']) }}
<span class="pure-form-message-inline">Body for all notifications</span> <span class="pure-form-message-inline">Body for all notifications &dash; You can use <a target="_new" href="https://jinja.palletsprojects.com/en/3.0.x/templates/">Jinja2</a> templating in the notification title, body and URL, and tokens from below.
</div> </span>
<div class="pure-control-group">
<!-- unsure -->
{{ render_field(form.notification_format , class="notification-format") }}
<span class="pure-form-message-inline">Format for all notifications</span>
</div> </div>
<div class="pure-controls"> <div class="pure-controls">
<p class="pure-form-message-inline"> <div id="notification-token-toggle" class="pure-button button-tag button-xsmall">Show token/placeholders</div>
You can use <a target="_new" href="https://jinja.palletsprojects.com/en/3.0.x/templates/">Jinja2</a> templating in the notification title, body and URL. </div>
</p> <div class="pure-controls" style="display: none;" id="notification-tokens-info">
<table class="pure-table" id="token-table"> <table class="pure-table" id="token-table">
<thead> <thead>
<tr> <tr>
@@ -99,6 +95,10 @@
<td><code>{{ '{{diff_full}}' }}</code></td> <td><code>{{ '{{diff_full}}' }}</code></td>
<td>The diff output - full difference output</td> <td>The diff output - full difference output</td>
</tr> </tr>
<tr>
<td><code>{{ '{{diff_patch}}' }}</code></td>
<td>The diff output - patch in unified format</td>
</tr>
<tr> <tr>
<td><code>{{ '{{current_snapshot}}' }}</code></td> <td><code>{{ '{{current_snapshot}}' }}</code></td>
<td>The current snapshot value, useful when combined with JSON or CSS filters <td>The current snapshot value, useful when combined with JSON or CSS filters
@@ -111,12 +111,15 @@
</tbody> </tbody>
</table> </table>
<div class="pure-form-message-inline"> <div class="pure-form-message-inline">
<br> <p>
URLs generated by changedetection.io (such as <code>{{ '{{diff_url}}' }}</code>) require the <code>BASE_URL</code> environment variable set.<br> Warning: Contents of <code>{{ '{{diff}}' }}</code>, <code>{{ '{{diff_removed}}' }}</code>, and <code>{{ '{{diff_added}}' }}</code> depend on how the difference algorithm perceives the change. <br>
Your <code>BASE_URL</code> var is currently "{{settings_application['current_base_url']}}" For example, an addition or removal could be perceived as a change in some cases. <a target="_new" href="https://github.com/dgtlmoon/changedetection.io/wiki/Using-the-%7B%7Bdiff%7D%7D,-%7B%7Bdiff_added%7D%7D,-and-%7B%7Bdiff_removed%7D%7D-notification-tokens">More Here</a> <br>
<br> </p>
Warning: Contents of <code>{{ '{{diff}}' }}</code>, <code>{{ '{{diff_removed}}' }}</code>, and <code>{{ '{{diff_added}}' }}</code> depend on how the difference algorithm perceives the change. For example, an addition or removal could be perceived as a change in some cases. <a target="_new" href="https://github.com/dgtlmoon/changedetection.io/wiki/Using-the-%7B%7Bdiff%7D%7D,-%7B%7Bdiff_added%7D%7D,-and-%7B%7Bdiff_removed%7D%7D-notification-tokens">More Here</a> <br>
</div> </div>
</div> </div>
<div class="pure-control-group">
{{ render_field(form.notification_format , class="notification-format") }}
<span class="pure-form-message-inline">Format for all notifications</span>
</div>
</div> </div>
{% endmacro %} {% endmacro %}

View File

@@ -62,14 +62,6 @@
<span class="pure-form-message-inline">Allow access to view watch diff page when password is enabled (Good for sharing the diff page) <span class="pure-form-message-inline">Allow access to view watch diff page when password is enabled (Good for sharing the diff page)
</span> </span>
</div> </div>
<div class="pure-control-group">
{{ render_field(form.application.form.base_url, placeholder="http://yoursite.com:5000/",
class="m-d") }}
<span class="pure-form-message-inline">
Base URL used for the <code>{{ '{{ base_url }}' }}</code> token in notifications and RSS links.<br>Default value is the ENV var 'BASE_URL' (Currently "{{settings_application['current_base_url']}}"),
<a href="https://github.com/dgtlmoon/changedetection.io/wiki/Configurable-BASE_URL-setting">read more here</a>.
</span>
</div>
<div class="pure-control-group"> <div class="pure-control-group">
{{ render_field(form.application.form.pager_size) }} {{ render_field(form.application.form.pager_size) }}
<span class="pure-form-message-inline">Number of items per page in the watch overview list, 0 to disable.</span> <span class="pure-form-message-inline">Number of items per page in the watch overview list, 0 to disable.</span>
@@ -100,6 +92,13 @@
{{ render_common_settings_form(form.application.form, emailprefix, settings_application) }} {{ render_common_settings_form(form.application.form, emailprefix, settings_application) }}
</div> </div>
</fieldset> </fieldset>
<div class="pure-control-group" id="notification-base-url">
{{ render_field(form.application.form.base_url, class="m-d") }}
<span class="pure-form-message-inline">
Base URL used for the <code>{{ '{{ base_url }}' }}</code> token in notification links.<br>
Default value is the system environment variable '<code>BASE_URL</code>' - <a href="https://github.com/dgtlmoon/changedetection.io/wiki/Configurable-BASE_URL-setting">read more here</a>.
</span>
</div>
</div> </div>
<div class="tab-pane-inner" id="fetching"> <div class="tab-pane-inner" id="fetching">

View File

@@ -0,0 +1,77 @@
#!/usr/bin/python3
import time
from flask import url_for
from ..util import live_server_setup, wait_for_all_checks, extract_UUID_from_client
def test_noproxy_option(client, live_server):
live_server_setup(live_server)
# Run by run_proxy_tests.sh
# Call this URL then scan the containers that it never went through them
url = "http://noproxy.changedetection.io"
# Should only be available when a proxy is setup
res = client.get(
url_for("edit_page", uuid="first", unpause_on_save=1))
assert b'No proxy' not in res.data
# Setup a proxy
res = client.post(
url_for("settings_page"),
data={
"requests-time_between_check-minutes": 180,
"application-ignore_whitespace": "y",
"application-fetch_backend": "html_requests",
"requests-extra_proxies-0-proxy_name": "custom-one-proxy",
"requests-extra_proxies-0-proxy_url": "http://test:awesome@squid-one:3128",
"requests-extra_proxies-1-proxy_name": "custom-two-proxy",
"requests-extra_proxies-1-proxy_url": "http://test:awesome@squid-two:3128",
"requests-extra_proxies-2-proxy_name": "custom-proxy",
"requests-extra_proxies-2-proxy_url": "http://test:awesome@squid-custom:3128",
},
follow_redirects=True
)
assert b"Settings updated." in res.data
# Should be available as an option
res = client.get(
url_for("settings_page", unpause_on_save=1))
assert b'No proxy' in res.data
# This will add it paused
res = client.post(
url_for("form_quick_watch_add"),
data={"url": url, "tags": '', 'edit_and_watch_submit_button': 'Edit > Watch'},
follow_redirects=True
)
assert b"Watch added in Paused state, saving will unpause" in res.data
uuid = extract_UUID_from_client(client)
res = client.get(
url_for("edit_page", uuid=uuid, unpause_on_save=1))
assert b'No proxy' in res.data
res = client.post(
url_for("edit_page", uuid=uuid, unpause_on_save=1),
data={
"include_filters": "",
"fetch_backend": "html_requests",
"headers": "",
"proxy": "no-proxy",
"tags": "",
"url": url,
},
follow_redirects=True
)
assert b"unpaused" in res.data
wait_for_all_checks(client)
client.get(url_for("form_watch_checknow"), follow_redirects=True)
wait_for_all_checks(client)
# Now the request should NOT appear in the second-squid logs (handled by the run_test_proxies.sh script)
# Prove that it actually checked
assert live_server.app.config['DATASTORE'].data['watching'][uuid]['last_checked'] != 0

View File

@@ -1,6 +1,6 @@
#!/usr/bin/python3 #!/usr/bin/python3
from .util import set_original_response, set_modified_response, live_server_setup from .util import set_original_response, set_modified_response, live_server_setup, wait_for_all_checks
from flask import url_for from flask import url_for
from urllib.request import urlopen from urllib.request import urlopen
from zipfile import ZipFile from zipfile import ZipFile
@@ -19,12 +19,12 @@ def test_backup(client, live_server):
# Add our URL to the import page # Add our URL to the import page
res = client.post( res = client.post(
url_for("import_page"), url_for("import_page"),
data={"urls": url_for('test_endpoint', _external=True)}, data={"urls": url_for('test_endpoint', _external=True)+"?somechar=őőőőőőőő"},
follow_redirects=True follow_redirects=True
) )
assert b"1 Imported" in res.data assert b"1 Imported" in res.data
time.sleep(3) wait_for_all_checks(client)
res = client.get( res = client.get(
url_for("get_backup"), url_for("get_backup"),

View File

@@ -15,7 +15,7 @@ def set_response_without_filter():
<p>Which is across multiple lines</p> <p>Which is across multiple lines</p>
<br> <br>
So let's see what happens. <br> So let's see what happens. <br>
<div id="nope-doesnt-exist">Some text thats the same</div> <div id="nope-doesnt-exist">Some text thats the same</div>
</body> </body>
</html> </html>
""" """
@@ -32,7 +32,7 @@ def set_response_with_filter():
<p>Which is across multiple lines</p> <p>Which is across multiple lines</p>
<br> <br>
So let's see what happens. <br> So let's see what happens. <br>
<div class="ticket-available">Ticket now on sale!</div> <div class="ticket-available">Ticket now on sale!</div>
</body> </body>
</html> </html>
""" """
@@ -84,6 +84,7 @@ def test_filter_doesnt_exist_then_exists_should_get_notification(client, live_se
"Snapshot: {{current_snapshot}}\n" "Snapshot: {{current_snapshot}}\n"
"Diff: {{diff}}\n" "Diff: {{diff}}\n"
"Diff Full: {{diff_full}}\n" "Diff Full: {{diff_full}}\n"
"Diff as Patch: {{diff_patch}}\n"
":-)", ":-)",
"notification_format": "Text"} "notification_format": "Text"}

View File

@@ -12,7 +12,7 @@ def set_response_with_filter():
<p>Which is across multiple lines</p> <p>Which is across multiple lines</p>
<br> <br>
So let's see what happens. <br> So let's see what happens. <br>
<div id="nope-doesnt-exist">Some text thats the same</div> <div id="nope-doesnt-exist">Some text thats the same</div>
</body> </body>
</html> </html>
""" """
@@ -66,6 +66,7 @@ def run_filter_test(client, content_filter):
"Snapshot: {{current_snapshot}}\n" "Snapshot: {{current_snapshot}}\n"
"Diff: {{diff}}\n" "Diff: {{diff}}\n"
"Diff Full: {{diff_full}}\n" "Diff Full: {{diff_full}}\n"
"Diff as Patch: {{diff_patch}}\n"
":-)", ":-)",
"notification_format": "Text"} "notification_format": "Text"}

View File

@@ -15,7 +15,7 @@ def set_original_response():
Some initial text<br> Some initial text<br>
<p id="only-this">Should be only this</p> <p id="only-this">Should be only this</p>
<br> <br>
<p id="not-this">And never this</p> <p id="not-this">And never this</p>
</body> </body>
</html> </html>
""" """
@@ -30,7 +30,7 @@ def set_modified_response():
Some initial text<br> Some initial text<br>
<p id="only-this">Should be REALLY only this</p> <p id="only-this">Should be REALLY only this</p>
<br> <br>
<p id="not-this">And never this</p> <p id="not-this">And never this</p>
</body> </body>
</html> </html>
""" """
@@ -189,6 +189,7 @@ def test_group_tag_notification(client, live_server):
"Diff Added: {{diff_added}}\n" "Diff Added: {{diff_added}}\n"
"Diff Removed: {{diff_removed}}\n" "Diff Removed: {{diff_removed}}\n"
"Diff Full: {{diff_full}}\n" "Diff Full: {{diff_full}}\n"
"Diff as Patch: {{diff_patch}}\n"
":-)", ":-)",
"notification_screenshot": True, "notification_screenshot": True,
"notification_format": "Text", "notification_format": "Text",
@@ -319,4 +320,4 @@ def test_clone_tag_on_quickwatchform_add(client, live_server):
assert b'Deleted' in res.data assert b'Deleted' in res.data
res = client.get(url_for("tags.delete_all"), follow_redirects=True) res = client.get(url_for("tags.delete_all"), follow_redirects=True)
assert b'All tags deleted' in res.data assert b'All tags deleted' in res.data

View File

@@ -98,6 +98,7 @@ def test_check_notification(client, live_server):
"Diff Added: {{diff_added}}\n" "Diff Added: {{diff_added}}\n"
"Diff Removed: {{diff_removed}}\n" "Diff Removed: {{diff_removed}}\n"
"Diff Full: {{diff_full}}\n" "Diff Full: {{diff_full}}\n"
"Diff as Patch: {{diff_patch}}\n"
":-)", ":-)",
"notification_screenshot": True, "notification_screenshot": True,
"notification_format": "Text"} "notification_format": "Text"}

View File

@@ -36,7 +36,7 @@ class TestDiffBuilder(unittest.TestCase):
output = output.split("\n") output = output.split("\n")
self.assertIn('(removed) for having learned computerese,', output) self.assertIn('(removed) for having learned computerese,', output)
self.assertIn('(removed) I continue to examine bits, bytes and words', output) self.assertIn('(removed) I continue to examine bits, bytes and words', output)
#diff_removed #diff_removed
with open(base_dir + "/test-content/before.txt", 'r') as f: with open(base_dir + "/test-content/before.txt", 'r') as f:
previous_version_file_contents = f.read() previous_version_file_contents = f.read()
@@ -49,7 +49,7 @@ class TestDiffBuilder(unittest.TestCase):
self.assertIn('(into) xok', output) self.assertIn('(into) xok', output)
self.assertIn('(into) next-x-ok', output) self.assertIn('(into) next-x-ok', output)
self.assertNotIn('(added) and something new', output) self.assertNotIn('(added) and something new', output)
#diff_removed #diff_removed
with open(base_dir + "/test-content/after-2.txt", 'r') as f: with open(base_dir + "/test-content/after-2.txt", 'r') as f:
newest_version_file_contents = f.read() newest_version_file_contents = f.read()
@@ -57,9 +57,25 @@ class TestDiffBuilder(unittest.TestCase):
output = output.split("\n") output = output.split("\n")
self.assertIn('(removed) for having learned computerese,', output) self.assertIn('(removed) for having learned computerese,', output)
self.assertIn('(removed) I continue to examine bits, bytes and words', output) self.assertIn('(removed) I continue to examine bits, bytes and words', output)
def test_expected_diff_patch_output(self):
base_dir = os.path.dirname(__file__)
with open(base_dir + "/test-content/before.txt", 'r') as f:
before = f.read()
with open(base_dir + "/test-content/after.txt", 'r') as f:
after = f.read()
output = diff.render_diff(previous_version_file_contents=before,
newest_version_file_contents=after,
patch_format=True)
output = output.split("\n")
self.assertIn('-ok', output)
self.assertIn('+xok', output)
self.assertIn('+next-x-ok', output)
self.assertIn('+and something new', output)
# @todo test blocks of changed, blocks of added, blocks of removed # @todo test blocks of changed, blocks of added, blocks of removed
if __name__ == '__main__': if __name__ == '__main__':
unittest.main() unittest.main()

View File

@@ -58,6 +58,7 @@ class update_worker(threading.Thread):
'diff': diff.render_diff(watch.get_history_snapshot(dates[-2]), watch.get_history_snapshot(dates[-1]), line_feed_sep=line_feed_sep), 'diff': diff.render_diff(watch.get_history_snapshot(dates[-2]), watch.get_history_snapshot(dates[-1]), line_feed_sep=line_feed_sep),
'diff_added': diff.render_diff(watch.get_history_snapshot(dates[-2]), watch.get_history_snapshot(dates[-1]), include_removed=False, line_feed_sep=line_feed_sep), 'diff_added': diff.render_diff(watch.get_history_snapshot(dates[-2]), watch.get_history_snapshot(dates[-1]), include_removed=False, line_feed_sep=line_feed_sep),
'diff_full': diff.render_diff(watch.get_history_snapshot(dates[-2]), watch.get_history_snapshot(dates[-1]), include_equal=True, line_feed_sep=line_feed_sep), 'diff_full': diff.render_diff(watch.get_history_snapshot(dates[-2]), watch.get_history_snapshot(dates[-1]), include_equal=True, line_feed_sep=line_feed_sep),
'diff_patch': diff.render_diff(watch.get_history_snapshot(dates[-2]), watch.get_history_snapshot(dates[-1]), line_feed_sep=line_feed_sep, patch_format=True),
'diff_removed': diff.render_diff(watch.get_history_snapshot(dates[-2]), watch.get_history_snapshot(dates[-1]), include_added=False, line_feed_sep=line_feed_sep), 'diff_removed': diff.render_diff(watch.get_history_snapshot(dates[-2]), watch.get_history_snapshot(dates[-1]), include_added=False, line_feed_sep=line_feed_sep),
'screenshot': watch.get_screenshot() if watch.get('notification_screenshot') else None, 'screenshot': watch.get_screenshot() if watch.get('notification_screenshot') else None,
'triggered_text': triggered_text, 'triggered_text': triggered_text,

View File

@@ -33,7 +33,7 @@ dnspython<2.3.0
# jq not available on Windows so must be installed manually # jq not available on Windows so must be installed manually
# Notification library # Notification library
apprise~=1.4.5 apprise~=1.5.0
# apprise mqtt https://github.com/dgtlmoon/changedetection.io/issues/315 # apprise mqtt https://github.com/dgtlmoon/changedetection.io/issues/315
paho-mqtt paho-mqtt