diff --git a/changedetectionio/notification/handler.py b/changedetectionio/notification/handler.py index ed2d01d20..cd2782cbb 100644 --- a/changedetectionio/notification/handler.py +++ b/changedetectionio/notification/handler.py @@ -77,6 +77,55 @@ def notification_format_align_with_apprise(n_format : str): return n_format +def apply_discord_markdown_to_body(n_body): + """ + Discord does not support but it supports non-standard ~~strikethrough~~ + :param n_body: + :return: + """ + import re + # Define the mapping between your placeholders and markdown markers + replacements = [ + (REMOVED_PLACEMARKER_OPEN, '~~', REMOVED_PLACEMARKER_CLOSED, '~~'), + (ADDED_PLACEMARKER_OPEN, '**', ADDED_PLACEMARKER_CLOSED, '**'), + (CHANGED_PLACEMARKER_OPEN, '~~', CHANGED_PLACEMARKER_CLOSED, '~~'), + (CHANGED_INTO_PLACEMARKER_OPEN, '**', CHANGED_INTO_PLACEMARKER_CLOSED, '**'), + ] + # So that the markdown gets added without any whitespace following it which would break it + for open_tag, open_md, close_tag, close_md in replacements: + # Regex: match opening tag, optional whitespace, capture the content, optional whitespace, then closing tag + pattern = re.compile( + re.escape(open_tag) + r'(\s*)(.*?)?(\s*)' + re.escape(close_tag), + flags=re.DOTALL + ) + n_body = pattern.sub(lambda m: f"{m.group(1)}{open_md}{m.group(2)}{close_md}{m.group(3)}", n_body) + return n_body + +def apply_standard_markdown_to_body(n_body): + """ + Apprise does not support ~~strikethrough~~ but it will convert to HTML strikethrough. + :param n_body: + :return: + """ + import re + # Define the mapping between your placeholders and markdown markers + replacements = [ + (REMOVED_PLACEMARKER_OPEN, '', REMOVED_PLACEMARKER_CLOSED, ''), + (ADDED_PLACEMARKER_OPEN, '**', ADDED_PLACEMARKER_CLOSED, '**'), + (CHANGED_PLACEMARKER_OPEN, '', CHANGED_PLACEMARKER_CLOSED, ''), + (CHANGED_INTO_PLACEMARKER_OPEN, '**', CHANGED_INTO_PLACEMARKER_CLOSED, '**'), + ] + + # So that the markdown gets added without any whitespace following it which would break it + for open_tag, open_md, close_tag, close_md in replacements: + # Regex: match opening tag, optional whitespace, capture the content, optional whitespace, then closing tag + pattern = re.compile( + re.escape(open_tag) + r'(\s*)(.*?)?(\s*)' + re.escape(close_tag), + flags=re.DOTALL + ) + n_body = pattern.sub(lambda m: f"{m.group(1)}{open_md}{m.group(2)}{close_md}{m.group(3)}", n_body) + return n_body + def apply_service_tweaks(url, n_body, n_title, requested_output_format): @@ -107,7 +156,7 @@ def apply_service_tweaks(url, n_body, n_title, requested_output_format): n_body = n_body.replace('
', '\n') n_body = n_body.replace('
', '\n') n_body = n_body.replace(CUSTOM_LINEBREAK_PLACEHOLDER, '\n') - + # Use strikethrough for removed content, bold for added content n_body = n_body.replace(REMOVED_PLACEMARKER_OPEN, '') n_body = n_body.replace(REMOVED_PLACEMARKER_CLOSED, '') @@ -141,15 +190,7 @@ def apply_service_tweaks(url, n_body, n_title, requested_output_format): if requested_output_format == 'html': # No diff placeholders, use Discord markdown for any other formatting # Use Discord markdown: strikethrough for removed, bold for added - n_body = n_body.replace(REMOVED_PLACEMARKER_OPEN, '~~') - n_body = n_body.replace(REMOVED_PLACEMARKER_CLOSED, '~~') - n_body = n_body.replace(ADDED_PLACEMARKER_OPEN, '**') - n_body = n_body.replace(ADDED_PLACEMARKER_CLOSED, '**') - # Handle changed/replaced lines (old → new) - n_body = n_body.replace(CHANGED_PLACEMARKER_OPEN, '~~') - n_body = n_body.replace(CHANGED_PLACEMARKER_CLOSED, '~~') - n_body = n_body.replace(CHANGED_INTO_PLACEMARKER_OPEN, '**') - n_body = n_body.replace(CHANGED_INTO_PLACEMARKER_CLOSED, '**') + n_body = apply_discord_markdown_to_body(n_body=n_body) # Apply 2000 char limit for plain content payload_max_size = 1700 @@ -181,6 +222,9 @@ def apply_service_tweaks(url, n_body, n_title, requested_output_format): n_body = n_body.replace(CHANGED_INTO_PLACEMARKER_OPEN, f'(into) ') n_body = n_body.replace(CHANGED_INTO_PLACEMARKER_CLOSED, f'') n_body = n_body.replace('\n', f'{CUSTOM_LINEBREAK_PLACEHOLDER}\n') + elif requested_output_format == 'markdown': + # Markdown to HTML - Apprise will convert this to HTML + n_body = apply_standard_markdown_to_body(n_body=n_body) else: #plaintext etc default n_body = n_body.replace(REMOVED_PLACEMARKER_OPEN, '(removed) ') @@ -301,17 +345,20 @@ def process_notification(n_object: NotificationContextData, datastore): apprise_input_format = NotifyFormat.TEXT.value elif requested_output_format == NotifyFormat.MARKDOWN.value: - # This actually means we request "Markdown to HTML", we want HTML output + # Convert markdown to HTML ourselves since not all plugins do this + from apprise.conversion import markdown_to_html + # Make sure there are paragraph breaks around horizontal rules + n_body = n_body.replace('---', '\n\n---\n\n') + n_body = markdown_to_html(n_body) url = f"{url}{prefix_add_to_url}format={NotifyFormat.HTML.value}" requested_output_format = NotifyFormat.HTML.value - apprise_input_format = NotifyFormat.MARKDOWN.value - + apprise_input_format = NotifyFormat.HTML.value # Changed from MARKDOWN to HTML # Could have arrived at any stage, so we dont end up running .escape on it if 'html' in requested_output_format: n_body = n_body.replace(CUSTOM_LINEBREAK_PLACEHOLDER, '
\r\n') else: - # Markup, text types etc + # texty types n_body = n_body.replace(CUSTOM_LINEBREAK_PLACEHOLDER, '\r\n') sent_objs.append({'title': n_title, diff --git a/changedetectionio/tests/smtp/test_notification_smtp.py b/changedetectionio/tests/smtp/test_notification_smtp.py index e50f62d59..a486fd858 100644 --- a/changedetectionio/tests/smtp/test_notification_smtp.py +++ b/changedetectionio/tests/smtp/test_notification_smtp.py @@ -290,7 +290,8 @@ def test_check_notification_markdown_format(client, live_server, measure_memory_ text_part = parts[0] assert text_part.get_content_type() == 'text/plain' text_content = text_part.get_content() - assert '(added) So let\'s see what happens.\r\n' in text_content # The plaintext part + # We wont see anything in the "FALLBACK" text but that's OK (no added/strikethrough etc) + assert 'So let\'s see what happens.\r\n' in text_content # The plaintext part # Second part should be text/html and roughly converted from markdown to HTML @@ -298,7 +299,7 @@ def test_check_notification_markdown_format(client, live_server, measure_memory_ assert html_part.get_content_type() == 'text/html' html_content = html_part.get_content() assert '

header

' in html_content - assert '(added) So let\'s see what happens.So let\'s see what happens.
' in html_content # Additions are in markdown delete_all_watches(client) # Custom notification body with HTML, that is either sent as HTML or rendered to plaintext and sent