diff --git a/changedetectionio/__init__.py b/changedetectionio/__init__.py index db0567ac4..36753e195 100644 --- a/changedetectionio/__init__.py +++ b/changedetectionio/__init__.py @@ -2,7 +2,7 @@ # Read more https://github.com/dgtlmoon/changedetection.io/wiki -__version__ = '0.50.32' +__version__ = '0.50.33' from changedetectionio.strtobool import strtobool from json.decoder import JSONDecodeError diff --git a/changedetectionio/blueprint/settings/templates/settings.html b/changedetectionio/blueprint/settings/templates/settings.html index d933f9383..9913e0958 100644 --- a/changedetectionio/blueprint/settings/templates/settings.html +++ b/changedetectionio/blueprint/settings/templates/settings.html @@ -240,9 +240,7 @@ nav
{{ render_field(form.application.form.scheduler_timezone_default) }}
diff --git a/changedetectionio/blueprint/ui/__init__.py b/changedetectionio/blueprint/ui/__init__.py index ce4bb716a..7755d2ca8 100644 --- a/changedetectionio/blueprint/ui/__init__.py +++ b/changedetectionio/blueprint/ui/__init__.py @@ -76,14 +76,14 @@ def _handle_operations(op, uuids, datastore, worker_handler, update_q, queuedWat elif (op == 'notification-default'): from changedetectionio.notification import ( - default_notification_format_for_watch + USE_SYSTEM_DEFAULT_NOTIFICATION_FORMAT_FOR_WATCH ) for uuid in uuids: if datastore.data['watching'].get(uuid): datastore.data['watching'][uuid]['notification_title'] = None datastore.data['watching'][uuid]['notification_body'] = None datastore.data['watching'][uuid]['notification_urls'] = [] - datastore.data['watching'][uuid]['notification_format'] = default_notification_format_for_watch + datastore.data['watching'][uuid]['notification_format'] = USE_SYSTEM_DEFAULT_NOTIFICATION_FORMAT_FOR_WATCH if emit_flash: flash(f"{len(uuids)} watches set to use default notification settings") diff --git a/changedetectionio/content_fetchers/base.py b/changedetectionio/content_fetchers/base.py index a6fe2005b..6d7d3d5d8 100644 --- a/changedetectionio/content_fetchers/base.py +++ b/changedetectionio/content_fetchers/base.py @@ -75,7 +75,6 @@ class Fetcher(): self.screenshot = None self.xpath_data = None # Keep headers and status_code as they're small - logger.trace("Fetcher content cleared from memory") @abstractmethod def get_error(self): diff --git a/changedetectionio/forms.py b/changedetectionio/forms.py index 836f90658..148731956 100644 --- a/changedetectionio/forms.py +++ b/changedetectionio/forms.py @@ -741,7 +741,6 @@ class quickWatchForm(Form): edit_and_watch_submit_button = SubmitField('Edit > Watch', render_kw={"class": "pure-button pure-button-primary"}) - # Common to a single watch and the global settings class commonSettingsForm(Form): from . import processors @@ -754,7 +753,7 @@ class commonSettingsForm(Form): fetch_backend = RadioField(u'Fetch Method', choices=content_fetchers.available_fetchers(), validators=[ValidateContentFetcherIsReady()]) notification_body = TextAreaField('Notification Body', default='{{ watch_url }} had a change.', validators=[validators.Optional(), ValidateJinja2Template()]) - notification_format = SelectField('Notification format', choices=valid_notification_formats.keys()) + notification_format = SelectField('Notification format', choices=list(valid_notification_formats.items())) notification_title = StringField('Notification Title', default='ChangeDetection.io Notification - {{ watch_url }}', validators=[validators.Optional(), ValidateJinja2Template()]) notification_urls = StringListField('Notification URL List', validators=[validators.Optional(), ValidateAppRiseServers(), ValidateJinja2Template()]) processor = RadioField( label=u"Processor - What do you want to achieve?", choices=processors.available_processors(), default="text_json_diff") diff --git a/changedetectionio/model/__init__.py b/changedetectionio/model/__init__.py index 222dafc70..cfc8aee64 100644 --- a/changedetectionio/model/__init__.py +++ b/changedetectionio/model/__init__.py @@ -2,7 +2,7 @@ import os import uuid from changedetectionio import strtobool -default_notification_format_for_watch = 'System default' +USE_SYSTEM_DEFAULT_NOTIFICATION_FORMAT_FOR_WATCH = 'System default' CONDITIONS_MATCH_LOGIC_DEFAULT = 'ALL' class watch_base(dict): @@ -44,7 +44,7 @@ class watch_base(dict): 'method': 'GET', 'notification_alert_count': 0, 'notification_body': None, - 'notification_format': default_notification_format_for_watch, + 'notification_format': USE_SYSTEM_DEFAULT_NOTIFICATION_FORMAT_FOR_WATCH, 'notification_muted': False, 'notification_screenshot': False, # Include the latest screenshot if available and supported by the apprise URL 'notification_title': None, diff --git a/changedetectionio/notification/__init__.py b/changedetectionio/notification/__init__.py index 06ed830a8..e82687871 100644 --- a/changedetectionio/notification/__init__.py +++ b/changedetectionio/notification/__init__.py @@ -1,17 +1,16 @@ -from changedetectionio.model import default_notification_format_for_watch +from changedetectionio.model import USE_SYSTEM_DEFAULT_NOTIFICATION_FORMAT_FOR_WATCH -default_notification_format = 'HTML Color' +default_notification_format = 'htmlcolor' default_notification_body = '{{watch_url}} had a change.\n---\n{{diff}}\n---\n' default_notification_title = 'ChangeDetection.io Notification - {{watch_url}}' # The values (markdown etc) are from apprise NotifyFormat, # But to avoid importing the whole heavy module just use the same strings here. valid_notification_formats = { - 'Plain Text': 'text', - 'HTML': 'html', - 'HTML Color': 'htmlcolor', - 'Markdown to HTML': 'markdown', + 'text': 'Plain Text', + 'html': 'HTML', + 'htmlcolor': 'HTML Color', + 'markdown': 'Markdown to HTML', # Used only for editing a watch (not for global) - default_notification_format_for_watch: default_notification_format_for_watch + USE_SYSTEM_DEFAULT_NOTIFICATION_FORMAT_FOR_WATCH: USE_SYSTEM_DEFAULT_NOTIFICATION_FORMAT_FOR_WATCH } - diff --git a/changedetectionio/notification/handler.py b/changedetectionio/notification/handler.py index 47c6b3bf0..fa61e9381 100644 --- a/changedetectionio/notification/handler.py +++ b/changedetectionio/notification/handler.py @@ -64,13 +64,13 @@ def notification_format_align_with_apprise(n_format : str): :return: """ - if n_format.lower().startswith('html'): + if n_format.startswith('html'): # Apprise only knows 'html' not 'htmlcolor' etc, which shouldnt matter here n_format = NotifyFormat.HTML.value - elif n_format.lower().startswith('markdown'): + elif n_format.startswith('markdown'): # probably the same but just to be safe n_format = NotifyFormat.MARKDOWN.value - elif n_format.lower().startswith('text'): + elif n_format.startswith('text'): # probably the same but just to be safe n_format = NotifyFormat.TEXT.value else: @@ -252,7 +252,7 @@ def apply_service_tweaks(url, n_body, n_title, requested_output_format): def process_notification(n_object: NotificationContextData, datastore): from changedetectionio.jinja2_custom import render as jinja_render - from . import default_notification_format_for_watch, default_notification_format, valid_notification_formats + from . import USE_SYSTEM_DEFAULT_NOTIFICATION_FORMAT_FOR_WATCH, default_notification_format, valid_notification_formats # be sure its registered from .apprise_plugin.custom_handlers import apprise_http_custom_handler # Register custom Discord plugin @@ -268,18 +268,17 @@ def process_notification(n_object: NotificationContextData, datastore): # Insert variables into the notification content notification_parameters = create_notification_parameters(n_object, datastore) - requested_output_format = valid_notification_formats.get( - n_object.get('notification_format', default_notification_format), - valid_notification_formats[default_notification_format], - ) + requested_output_format = n_object.get('notification_format', default_notification_format) + logger.debug(f"Requested notification output format: '{requested_output_format}'") # If we arrived with 'System default' then look it up - if requested_output_format == default_notification_format_for_watch and datastore.data['settings']['application'].get('notification_format') != default_notification_format_for_watch: + if requested_output_format == USE_SYSTEM_DEFAULT_NOTIFICATION_FORMAT_FOR_WATCH: # Initially text or whatever - requested_output_format = datastore.data['settings']['application'].get('notification_format', valid_notification_formats[default_notification_format]).lower() + requested_output_format = datastore.data['settings']['application'].get('notification_format', default_notification_format) requested_output_format_original = requested_output_format + # Now clean it up so it fits perfectly with apprise requested_output_format = notification_format_align_with_apprise(n_format=requested_output_format) logger.trace(f"Complete notification body including Jinja and placeholders calculated in {time.time() - now:.2f}s") diff --git a/changedetectionio/notification_service.py b/changedetectionio/notification_service.py index 9744a1e98..0771b2464 100644 --- a/changedetectionio/notification_service.py +++ b/changedetectionio/notification_service.py @@ -9,7 +9,8 @@ for both sync and async workers from loguru import logger import time -from changedetectionio.notification import default_notification_format +from changedetectionio.model import USE_SYSTEM_DEFAULT_NOTIFICATION_FORMAT_FOR_WATCH +from changedetectionio.notification import default_notification_format, valid_notification_formats # This gets modified on notification time (handler.py) depending on the required notification output CUSTOM_LINEBREAK_PLACEHOLDER='@BR@' @@ -48,15 +49,28 @@ class NotificationContextData(dict): if kwargs: self.update(kwargs) + n_format = self.get('notification_format') + if n_format and not valid_notification_formats.get(n_format): + raise ValueError(f'Invalid notification format: "{n_format}"') + def set_random_for_validation(self): import random, string - """Randomly fills all dict keys with random strings (for validation/testing).""" + """Randomly fills all dict keys with random strings (for validation/testing). + So we can test the output in the notification body + """ for key in self.keys(): if key in ['uuid', 'time', 'watch_uuid']: continue rand_str = 'RANDOM-PLACEHOLDER-'+''.join(random.choices(string.ascii_letters + string.digits, k=12)) self[key] = rand_str + def __setitem__(self, key, value): + if key == 'notification_format' and isinstance(value, str) and not value.startswith('RANDOM-PLACEHOLDER-'): + if not valid_notification_formats.get(value): + raise ValueError(f'Invalid notification format: "{value}"') + + super().__setitem__(key, value) + class NotificationService: """ Standalone notification service that handles all notification functionality @@ -72,7 +86,7 @@ class NotificationService: Queue a notification for a watch with full diff rendering and template variables """ from changedetectionio import diff - from changedetectionio.notification import default_notification_format_for_watch + from changedetectionio.notification import USE_SYSTEM_DEFAULT_NOTIFICATION_FORMAT_FOR_WATCH if not isinstance(n_object, NotificationContextData): raise TypeError(f"Expected NotificationContextData, got {type(n_object)}") @@ -94,7 +108,7 @@ class NotificationService: snapshot_contents = "No snapshot/history available, the watch should fetch atleast once." # If we ended up here with "System default" - if n_object.get('notification_format') == default_notification_format_for_watch: + 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') @@ -153,7 +167,7 @@ class NotificationService: Individual watch settings > Tag settings > Global settings """ from changedetectionio.notification import ( - default_notification_format_for_watch, + USE_SYSTEM_DEFAULT_NOTIFICATION_FORMAT_FOR_WATCH, default_notification_body, default_notification_title ) @@ -161,7 +175,7 @@ class NotificationService: # Would be better if this was some kind of Object where Watch can reference the parent datastore etc v = watch.get(var_name) if v and not watch.get('notification_muted'): - if var_name == 'notification_format' and v == default_notification_format_for_watch: + if var_name == 'notification_format' and v == USE_SYSTEM_DEFAULT_NOTIFICATION_FORMAT_FOR_WATCH: return self.datastore.data['settings']['application'].get('notification_format') return v @@ -178,7 +192,7 @@ class NotificationService: # Otherwise could be defaults if var_name == 'notification_format': - return default_notification_format_for_watch + return USE_SYSTEM_DEFAULT_NOTIFICATION_FORMAT_FOR_WATCH if var_name == 'notification_body': return default_notification_body if var_name == 'notification_title': @@ -233,7 +247,6 @@ class NotificationService: if not watch: return - n_format = self.datastore.data['settings']['application'].get('notification_format', default_notification_format) filter_list = ", ".join(watch['include_filters']) # @todo - This could be a markdown template on the disk, apprise will convert the markdown to HTML+Plaintext parts in the email, and then 'markup_text_links_to_html_links' is not needed body = f"""Hello, @@ -250,9 +263,9 @@ Thanks - Your omniscient changedetection.io installation. n_object = NotificationContextData({ 'notification_title': 'Changedetection.io - Alert - CSS/xPath filter was not present in the page', 'notification_body': body, - 'notification_format': n_format, - 'markup_text_links_to_html_links': n_format.lower().startswith('html') + 'notification_format': self._check_cascading_vars('notification_format', watch), }) + n_object['markup_text_links_to_html_links'] = n_object.get('notification_format').startswith('html') if len(watch['notification_urls']): n_object['notification_urls'] = watch['notification_urls'] @@ -280,7 +293,7 @@ Thanks - Your omniscient changedetection.io installation. if not watch: return threshold = self.datastore.data['settings']['application'].get('filter_failure_notification_threshold_attempts') - n_format = self.datastore.data['settings']['application'].get('notification_format', default_notification_format).lower() + step = step_n + 1 # @todo - This could be a markdown template on the disk, apprise will convert the markdown to HTML+Plaintext parts in the email, and then 'markup_text_links_to_html_links' is not needed @@ -299,9 +312,9 @@ Thanks - Your omniscient changedetection.io installation. n_object = NotificationContextData({ 'notification_title': f"Changedetection.io - Alert - Browser step at position {step} could not be run", 'notification_body': body, - 'notification_format': n_format, - 'markup_text_links_to_html_links': n_format.lower().startswith('html') + 'notification_format': self._check_cascading_vars('notification_format', watch), }) + n_object['markup_text_links_to_html_links'] = n_object.get('notification_format').startswith('html') if len(watch['notification_urls']): n_object['notification_urls'] = watch['notification_urls'] diff --git a/changedetectionio/store.py b/changedetectionio/store.py index 893cdac34..38009a736 100644 --- a/changedetectionio/store.py +++ b/changedetectionio/store.py @@ -5,7 +5,7 @@ from flask import ( ) from .html_tools import TRANSLATE_WHITESPACE_TABLE -from . model import App, Watch +from .model import App, Watch, USE_SYSTEM_DEFAULT_NOTIFICATION_FORMAT_FOR_WATCH from copy import deepcopy, copy from os import path, unlink from threading import Lock @@ -987,10 +987,35 @@ class ChangeDetectionStore: self.data['settings']['application']['ui']['use_page_title_in_list'] = self.data['settings']['application'].get('extract_title_as_title') def update_21(self): - self.data['settings']['application']['scheduler_timezone_default'] = self.data['settings']['application'].get('timezone') - del self.data['settings']['application']['timezone'] + if self.data['settings']['application'].get('timezone'): + self.data['settings']['application']['scheduler_timezone_default'] = self.data['settings']['application'].get('timezone') + del self.data['settings']['application']['timezone'] + # Some notification formats got the wrong name type + def update_22(self): + from .notification import valid_notification_formats + + sys_n_format = self.data['settings']['application'].get('notification_format') + key_exists_as_value = next((k for k, v in valid_notification_formats.items() if v == sys_n_format), None) + if key_exists_as_value: # key of "Plain text" + logger.success(f"['settings']['application']['notification_format'] '{sys_n_format}' -> '{key_exists_as_value}'") + self.data['settings']['application']['notification_format'] = key_exists_as_value + + for uuid, watch in self.data['watching'].items(): + n_format = self.data['watching'][uuid].get('notification_format') + key_exists_as_value = next((k for k, v in valid_notification_formats.items() if v == n_format), None) + if key_exists_as_value and key_exists_as_value != USE_SYSTEM_DEFAULT_NOTIFICATION_FORMAT_FOR_WATCH: # key of "Plain text" + logger.success(f"['watching'][{uuid}]['notification_format'] '{n_format}' -> '{key_exists_as_value}'") + self.data['watching'][uuid]['notification_format'] = key_exists_as_value # should be 'text' or whatever + + for uuid, tag in self.data['settings']['application']['tags'].items(): + n_format = self.data['settings']['application']['tags'][uuid].get('notification_format') + key_exists_as_value = next((k for k, v in valid_notification_formats.items() if v == n_format), None) + if key_exists_as_value and key_exists_as_value != USE_SYSTEM_DEFAULT_NOTIFICATION_FORMAT_FOR_WATCH: # key of "Plain text" + logger.success(f"['settings']['application']['tags'][{uuid}]['notification_format'] '{n_format}' -> '{key_exists_as_value}'") + self.data['settings']['application']['tags'][uuid]['notification_format'] = key_exists_as_value # should be 'text' or whatever + def add_notification_url(self, notification_url): logger.debug(f">>> Adding new notification_url - '{notification_url}'") diff --git a/changedetectionio/templates/_helpers.html b/changedetectionio/templates/_helpers.html index 8195c3453..22b326ab9 100644 --- a/changedetectionio/templates/_helpers.html +++ b/changedetectionio/templates/_helpers.html @@ -266,9 +266,7 @@