Compare commits

...

1 Commits

8 changed files with 73 additions and 51 deletions

View File

@@ -86,6 +86,10 @@
<div class="tab-pane-inner" id="notifications">
<fieldset>
{{ render_common_settings_form(form.application.form, emailprefix, settings_application, extra_notification_token_placeholder_info) }}
</fieldset>
<fieldset class="pure-group">
{{ render_checkbox_field(form.application.form.notification_html_word_diff_enabled) }}
<span class="pure-form-message-inline">HTML notifications - Use "word by word" difference where possible.</span>
</fieldset>
<div class="pure-control-group" id="notification-base-url">
{{ render_field(form.application.form.base_url, class="m-d") }}

View File

@@ -991,6 +991,7 @@ class globalSettingsApplicationForm(commonSettingsForm):
render_kw={"placeholder": os.getenv('BASE_URL', 'Not set')}
)
empty_pages_are_a_change = BooleanField(_l('Treat empty pages as a change?'), default=False)
notification_html_word_diff_enabled = BooleanField(_l('Notification HTML as word-by-word difference'), default=True, validators=[validators.Optional()])
fetch_backend = RadioField(_l('Fetch Method'), default="html_requests", choices=content_fetchers.available_fetchers(), validators=[ValidateContentFetcherIsReady()])
global_ignore_text = StringListField(_l('Ignore Text'), [ValidateListRegex()])
global_subtractive_selectors = StringListField(_l('Remove elements'), [ValidateCSSJSONXPATHInput(allow_json=False)])

View File

@@ -49,6 +49,7 @@ class model(dict):
'ssim_threshold': '0.96', # Default SSIM threshold for screenshot comparison
'notification_body': default_notification_body,
'notification_format': default_notification_format,
'notification_html_word_diff': True,
'notification_title': default_notification_title,
'notification_urls': [], # Apprise URL list
'pager_size': 50,

View File

@@ -309,6 +309,9 @@ def process_notification(n_object: NotificationContextData, datastore):
if not isinstance(n_object, NotificationContextData):
raise TypeError(f"Expected NotificationContextData, got {type(n_object)}")
if not n_object.get('notification_urls'):
return None
now = time.time()
if n_object.get('notification_timestamp'):
logger.trace(f"Time since queued {now-n_object['notification_timestamp']:.3f}s")
@@ -348,16 +351,15 @@ def process_notification(n_object: NotificationContextData, datastore):
apprise.plugins.N_MGR.remove('discord')
apprise.plugins.N_MGR.add(NotifyDiscordCustom, schemas='discord')
if not n_object.get('notification_urls'):
return None
# Should always be false for 'text' mode or its too hard to read, otherwise it's a setting (for html style).
word_diff_enable = requested_output_format_original == 'text' or (
n_object.get('notification_html_word_diff_enabled', True) and requested_output_format_original.startswith('html'))
n_object.update(add_rendered_diff_to_notification_vars(
notification_scan_text=n_object.get('notification_body', '')+n_object.get('notification_title', ''),
current_snapshot=n_object.get('current_snapshot'),
prev_snapshot=n_object.get('prev_snapshot'),
# Should always be false for 'text' mode or its too hard to read
# But otherwise, this could be some setting
word_diff=False if requested_output_format_original == 'text' else True,
word_diff=word_diff_enable
)
)

View File

@@ -250,6 +250,7 @@ class NotificationService:
if n_object.get('notification_format') == USE_SYSTEM_DEFAULT_NOTIFICATION_FORMAT_FOR_WATCH:
n_object['notification_format'] = self.datastore.data['settings']['application'].get('notification_format')
n_object['notification_html_word_diff_enabled'] = self.datastore.data['settings']['application'].get('notification_html_word_diff_enabled', True)
triggered_text = ''
if len(trigger_text):

View File

@@ -1,5 +1,5 @@
{% from '_helpers.html' import render_field %}
{% from '_helpers.html' import render_field, render_checkbox_field %}
{% macro show_token_placeholders(extra_notification_token_placeholder_info, suffix="") %}
@@ -8,9 +8,7 @@
<span class="pure-form-message-inline">
Body for all notifications &dash; You can use <a target="newwindow" href="https://jinja.palletsprojects.com/en/3.0.x/templates/">Jinja2</a> templating in the notification title, body and URL, and tokens from below.
</span><br>
<div data-target="#notification-tokens-info{{ suffix }}" class="toggle-show pure-button button-tag button-xsmall">Show
token/placeholders
</div>
<div data-target="#notification-tokens-info{{ suffix }}" class="toggle-show pure-button button-tag button-xsmall">Show extra help and tokens</div>
</div>
<div class="pure-controls" style="display: none;" id="notification-tokens-info{{ suffix }}">
<table class="pure-table" id="token-table">
@@ -105,11 +103,30 @@
{% endif %}
</tbody>
</table>
<span class="pure-form-message-inline">
<br>
<div class="pure-form-message-inline">
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>
For example, an addition or removal could be perceived as a change in some cases. <a target="newwindow" 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>
</span>
For example, an addition or removal could be perceived as a change in some cases. <a target="newwindow" 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>
<br><br>
<div class="pure-form-message-inline">
<ul>
<li><span class="pure-form-message-inline">
For JSON payloads, use <strong>|tojson</strong> without quotes for automatic escaping, for example - <code>{ "name": {{ '{{ watch_title|tojson }}' }} }</code>
</span></li>
<li><span class="pure-form-message-inline">
URL encoding, use <strong>|urlencode</strong>, for example - <code>gets://hook-website.com/test.php?title={{ '{{ watch_title|urlencode }}' }}</code>
</span></li>
<li><span class="pure-form-message-inline">
Regular-expression replace, use <strong>|regex_replace</strong>, for example - <code>{{ "{{ \"hello world 123\" | regex_replace('[0-9]+', 'no-more-numbers') }}" }}</code>
</span></li>
<li><span class="pure-form-message-inline">
For a complete reference of all Jinja2 built-in filters, users can refer to the <a
href="https://jinja.palletsprojects.com/en/3.1.x/templates/#builtin-filters">https://jinja.palletsprojects.com/en/3.1.x/templates/#builtin-filters</a>
</span></li>
</ul>
<br>
</div>
</div>
{% endmacro %}
@@ -151,28 +168,11 @@
{{ render_field(form.notification_title, class="m-d notification-title", placeholder=settings_application['notification_title']) }}
<span class="pure-form-message-inline">Title for all notifications</span>
</div>
<div class="pure-control-group">
<div>
{{ render_field(form.notification_body , rows=5, class="notification-body", placeholder=settings_application['notification_body']) }}
{{ show_token_placeholders(extra_notification_token_placeholder_info=extra_notification_token_placeholder_info) }}
<div class="pure-form-message-inline">
<ul>
<li><span class="pure-form-message-inline">
For JSON payloads, use <strong>|tojson</strong> without quotes for automatic escaping, for example - <code>{ "name": {{ '{{ watch_title|tojson }}' }} }</code>
</span></li>
<li><span class="pure-form-message-inline">
URL encoding, use <strong>|urlencode</strong>, for example - <code>gets://hook-website.com/test.php?title={{ '{{ watch_title|urlencode }}' }}</code>
</span></li>
<li><span class="pure-form-message-inline">
Regular-expression replace, use <strong>|regex_replace</strong>, for example - <code>{{ "{{ \"hello world 123\" | regex_replace('[0-9]+', 'no-more-numbers') }}" }}</code>
</span></li>
<li><span class="pure-form-message-inline">
For a complete reference of all Jinja2 built-in filters, users can refer to the <a href="https://jinja.palletsprojects.com/en/3.1.x/templates/#builtin-filters">https://jinja.palletsprojects.com/en/3.1.x/templates/#builtin-filters</a>
</span></li>
</ul>
<br>
</div>
</div>
<div class="">
<div>
{{ render_field(form.notification_format , class="notification-format") }}
<span class="pure-form-message-inline">Format for all notifications</span>
</div>

View File

@@ -532,7 +532,7 @@ def test_single_send_test_notification_on_watch(client, live_server, measure_mem
assert 'Current snapshot: Example text: example test' in x
os.unlink(os.path.join(datastore_path, "notification.txt"))
def _test_color_notifications(client, notification_body_token, datastore_path):
def _test_color_notifications(client, notification_body_token, datastore_path, word_diff_enabled = True):
set_original_response(datastore_path=datastore_path)
@@ -551,6 +551,7 @@ def _test_color_notifications(client, notification_body_token, datastore_path):
"application-minutes_between_check": 180,
"application-notification_body": notification_body_token,
"application-notification_format": "htmlcolor",
"application-notification_html_word_diff_enabled": 'y' if word_diff_enabled else '',
"application-notification_urls": test_notification_url,
"application-notification_title": "New ChangeDetection.io Notification - {{ watch_url }}",
},
@@ -559,17 +560,13 @@ def _test_color_notifications(client, notification_body_token, datastore_path):
assert b'Settings updated' in res.data
test_url = url_for('test_endpoint', _external=True)
res = client.post(
url_for("ui.ui_views.form_quick_watch_add"),
data={"url": test_url, "tags": 'nice one'},
follow_redirects=True
)
assert b"Watch added" in res.data
uuid = client.application.config.get('DATASTORE').add_watch(url=test_url)
res = client.get(url_for("ui.form_watch_checknow"), follow_redirects=True)
assert b'Queued 1 watch for rechecking.' in res.data
wait_for_all_checks(client)
set_modified_response(datastore_path=datastore_path)
extras='XXX ' if word_diff_enabled else ''
set_modified_response(datastore_path=datastore_path, extras=extras)
res = client.get(url_for("ui.form_watch_checknow"), follow_redirects=True)
@@ -579,9 +576,13 @@ def _test_color_notifications(client, notification_body_token, datastore_path):
wait_for_notification_endpoint_output(datastore_path=datastore_path)
with open(os.path.join(datastore_path, "notification.txt"), 'r') as f:
x = f.read()
contents = f.read()
s = f'<span style="{HTML_CHANGED_STYLE}" role="note" aria-label="Changed text" title="Changed text">Which is across multiple lines</span><br>'
assert s in x
assert s in contents
if word_diff_enabled:
assert '>XXX</span>' in contents
else:
assert '>XXX</span>' not in contents
client.get(
url_for("ui.form_delete", uuid="all"),
@@ -590,6 +591,12 @@ def _test_color_notifications(client, notification_body_token, datastore_path):
# Just checks the format of the colour notifications was correct
def test_html_color_notifications(client, live_server, measure_memory_usage, datastore_path):
_test_color_notifications(client, '{{diff}}',datastore_path=datastore_path)
_test_color_notifications(client, '{{diff_full}}',datastore_path=datastore_path)
# Word-level diff only triggers when difflib.SequenceMatcher identifies a single-line to single-line replacement.
# If you have multiple changed lines close together, you need at least 1 unchanged content line (not empty) between them to
# prevent them from being grouped into a multi-line replacement that falls back to line-level diff.
_test_color_notifications(client, '{{diff}}',datastore_path=datastore_path, word_diff_enabled = True)
_test_color_notifications(client, '{{diff_full}}',datastore_path=datastore_path, word_diff_enabled = True)
_test_color_notifications(client, '{{diff}}',datastore_path=datastore_path, word_diff_enabled = False)
_test_color_notifications(client, '{{diff_full}}',datastore_path=datastore_path, word_diff_enabled = False)

View File

@@ -7,7 +7,7 @@ import logging
import time
import os
def set_original_response(datastore_path, extra_title=''):
def set_original_response(datastore_path, extra_title='', extras=''):
test_return_data = f"""<html>
<head><title>head title{extra_title}</title></head>
<body>
@@ -15,6 +15,9 @@ def set_original_response(datastore_path, extra_title=''):
<p>Which is across multiple lines</p>
<br>
So let's see what happens. <br>
with more text that helps word-diff if needed<br>
and more text that helps word-diff if needed<br>
and even more text {extras}that helps word-diff if needed<br>
<span class="foobar-detection" style='display:none'></span>
</body>
</html>
@@ -24,14 +27,17 @@ def set_original_response(datastore_path, extra_title=''):
f.write(test_return_data)
return None
def set_modified_response(datastore_path):
test_return_data = """<html>
def set_modified_response(datastore_path, extras=''):
test_return_data =f"""<html>
<head><title>modified head title</title></head>
<body>
Some initial text<br>
<p>which has this one new line</p>
<br>
So let's see what happens. <br>
with more text that helps word-diff if needed<br>
and more text that helps word-diff if needed<br>
and even more text {extras}that helps word-diff if needed<br>
</body>
</html>
"""
@@ -92,8 +98,8 @@ def wait_for_notification_endpoint_output(datastore_path):
#@todo - could check the apprise object directly instead of looking for this file
from os.path import isfile
notification_file = os.path.join(datastore_path, "notification.txt")
for i in range(1, 20):
time.sleep(1)
for i in range(1, 100):
time.sleep(0.3)
if isfile(notification_file):
return True