From 62e125975060f135253ef7e1d13cba14c23e6387 Mon Sep 17 00:00:00 2001 From: dgtlmoon Date: Thu, 12 Mar 2026 15:24:19 +0100 Subject: [PATCH] refactor attempt --- changedetectionio/notification_service.py | 45 ++++++++++++--- changedetectionio/tests/test_group.py | 68 ++++++++++++++--------- changedetectionio/tests/util.py | 7 ++- 3 files changed, 86 insertions(+), 34 deletions(-) diff --git a/changedetectionio/notification_service.py b/changedetectionio/notification_service.py index 3290ff7b548..37f912f0a63 100644 --- a/changedetectionio/notification_service.py +++ b/changedetectionio/notification_service.py @@ -6,6 +6,7 @@ for both sync and async workers """ import datetime +from copy import deepcopy import pytz from loguru import logger @@ -352,7 +353,7 @@ def send_content_changed_notification(self, watch_uuid): """ Send notification when content changes are detected """ - n_object = NotificationContextData() + watch = self.datastore.data['watching'].get(watch_uuid) if not watch: return @@ -369,21 +370,51 @@ def send_content_changed_notification(self, watch_uuid): # Should be a better parent getter in the model object # Prefer - Individual watch settings > Tag settings > Global settings (in that order) - # this change probably not needed? - n_object['notification_urls'] = _check_cascading_vars(self.datastore, 'notification_urls', watch) + # If the watch has no notification_body for example, it will try to get from the first matching group or system setting + + # Should be, if none in the watch, and no group tag ones found, then use system ones at the end + #n_object['notification_urls'] = _check_cascading_vars(self.datastore, 'notification_urls', watch) + n_object = NotificationContextData() n_object['notification_title'] = _check_cascading_vars(self.datastore,'notification_title', watch) n_object['notification_body'] = _check_cascading_vars(self.datastore,'notification_body', watch) n_object['notification_format'] = _check_cascading_vars(self.datastore,'notification_format', watch) + notification_objects = [] + if n_object.get('notification_urls'): + notification_objects.append(n_object) + + + # LOGIC SHOULD BE something that all tests currently pass too + # !!! _check_cascading_vars is not really used much, only used here.. + # + + + # If any related group/tag has a notification_url set, then we fan out horizontally and collect it as extra notifications + tags = self.datastore.get_all_tags_for_watch(uuid=watch.get('uuid')) + logger.debug(f'{len(tags)} related to this watch') + if tags: + for tag_uuid, tag in tags.items(): + logger.debug(f"Checking group/tag for notification URLs '{tag['title']}' Muted? '{tag.get('notification_muted')}', URLs {tag.get('notification_urls')}") + v = tag.get('notification_urls') + if v and not tag.get('notification_muted'): + logger.debug("OK MAN") + next_n_object = deepcopy(n_object) + next_n_object['notification_urls'] = v + next_n_object['notification_title'] = _check_cascading_vars(self.datastore, 'notification_title', watch) + next_n_object['notification_body'] = _check_cascading_vars(self.datastore, 'notification_body', watch) + next_n_object['notification_format'] = _check_cascading_vars(self.datastore, 'notification_format', watch) + notification_objects.append(next_n_object) + logger.debug(f"Adding notification from group/tag {tag['title']}") + + # (Individual watch) Only prepare to notify if the rules above matched queued = False - if n_object and n_object.get('notification_urls'): + if notification_objects: queued = True - count = watch.get('notification_alert_count', 0) + 1 self.datastore.update_watch(uuid=watch_uuid, update_obj={'notification_alert_count': count}) - - self.queue_notification_for_watch(n_object=n_object, watch=watch) + for n_object in notification_objects: + self.queue_notification_for_watch(n_object=n_object, watch=watch) return queued diff --git a/changedetectionio/tests/test_group.py b/changedetectionio/tests/test_group.py index 8e694114a5b..a80685971ba 100644 --- a/changedetectionio/tests/test_group.py +++ b/changedetectionio/tests/test_group.py @@ -171,6 +171,7 @@ def test_group_tag_notification(client, live_server, measure_memory_usage, datas delete_all_watches(client) set_original_response(datastore_path=datastore_path) + notification_url_endpoint = url_for('test_notification_endpoint', _external=True).replace('http', 'post') test_url = url_for('test_endpoint', _external=True) res = client.post( @@ -181,35 +182,50 @@ def test_group_tag_notification(client, live_server, measure_memory_usage, datas assert b"Watch added" in res.data - notification_url = url_for('test_notification_endpoint', _external=True).replace('http', 'json') - notification_form_data = {"notification_urls": notification_url, - "notification_title": "New GROUP TAG ChangeDetection.io Notification - {{watch_url}}", - "notification_body": "BASE URL: {{base_url}}\n" - "Watch URL: {{watch_url}}\n" - "Watch UUID: {{watch_uuid}}\n" - "Watch title: {{watch_title}}\n" - "Watch tag: {{watch_tag}}\n" - "Preview: {{preview_url}}\n" - "Diff URL: {{diff_url}}\n" - "Snapshot: {{current_snapshot}}\n" - "Diff: {{diff}}\n" - "Diff Added: {{diff_added}}\n" - "Diff Removed: {{diff_removed}}\n" - "Diff Full: {{diff_full}}\n" - "Diff as Patch: {{diff_patch}}\n" - ":-)", - "notification_screenshot": True, - "notification_format": 'text', - "title": "test-tag"} + client.get(url_for("ui.form_watch_checknow"), follow_redirects=True) + wait_for_all_checks(client) + group_tag_form_data = { + "notification_title": "New GROUP TAG ChangeDetection.io Notification - {{watch_url}}", + "notification_body": "BASE URL: {{base_url}}\n" + "Watch URL: {{watch_url}}\n" + "Watch UUID: {{watch_uuid}}\n" + "Watch title: {{watch_title}}\n" + "Watch tag: {{watch_tag}}\n" + "Preview: {{preview_url}}\n" + "Diff URL: {{diff_url}}\n" + "Snapshot: {{current_snapshot}}\n" + "Diff: {{diff}}\n" + "Diff Added: {{diff_added}}\n" + "Diff Removed: {{diff_removed}}\n" + "Diff Full: {{diff_full}}\n" + "Diff as Patch: {{diff_patch}}\n" + ":-)", + "notification_screenshot": True, + "notification_format": 'text', + } + + # Setup for test-tag + group_tag_form_data['notification_urls'] = notification_url_endpoint+"?outputfilename=test-tag.txt" + group_tag_form_data['title'] = 'test-tag' res = client.post( url_for("tags.form_tag_edit_submit", uuid=get_UUID_for_tag_name(client, name="test-tag")), - data=notification_form_data, + data=group_tag_form_data, + follow_redirects=True + ) + assert b"Updated" in res.data + + # Setup for other-tag, we only add notifications-urls + group_tag_form_data['notification_urls'] = notification_url_endpoint+"?outputfilename=other-tag.txt" + group_tag_form_data['title'] = 'other-tag' + + res = client.post( + url_for("tags.form_tag_edit_submit", uuid=get_UUID_for_tag_name(client, name="other-tag")), + data=group_tag_form_data, follow_redirects=True ) assert b"Updated" in res.data - wait_for_all_checks(client) set_modified_response(datastore_path=datastore_path) client.get(url_for("ui.form_watch_checknow"), follow_redirects=True) @@ -217,12 +233,14 @@ def test_group_tag_notification(client, live_server, measure_memory_usage, datas time.sleep(3) - assert os.path.isfile(os.path.join(datastore_path, "notification.txt")) + assert os.path.isfile(os.path.join(datastore_path, "test-tag.txt")) + assert os.path.isfile(os.path.join(datastore_path, "other-tag.txt")) + # @todo assert the group name or other unique body is in other-tag.txt # Verify what was sent as a notification, this file should exist - with open(os.path.join(datastore_path, "notification.txt"), "r") as f: + with open(os.path.join(datastore_path, "test-tag.txt"), "r") as f: notification_submission = f.read() - os.unlink(os.path.join(datastore_path, "notification.txt")) + os.unlink(os.path.join(datastore_path, "test-tag.txt")) # Did we see the URL that had a change, in the notification? # Diff was correctly executed diff --git a/changedetectionio/tests/util.py b/changedetectionio/tests/util.py index 6c30aeba1f2..cdb3a7726ad 100644 --- a/changedetectionio/tests/util.py +++ b/changedetectionio/tests/util.py @@ -343,8 +343,11 @@ def test_method(): @live_server.app.route('/test_notification_endpoint', methods=['POST', 'GET']) def test_notification_endpoint(): datastore_path = current_app.config.get('TEST_DATASTORE_PATH', 'test-datastore') - - with open(os.path.join(datastore_path, "notification.txt"), "wb") as f: + from loguru import logger + # @todo make safe + fname = request.args.get('outputfilename', "notification.txt") + logger.debug(f"Writing test notification endpoint data to '{fname}' - {request.args}") + with open(os.path.join(datastore_path, fname), "wb") as f: # Debug method, dump all POST to file also, used to prove #65 data = request.stream.read() if data != None: