diff --git a/.github/workflows/test-stack-reusable-workflow.yml b/.github/workflows/test-stack-reusable-workflow.yml
index 0627c1e5..26340a31 100644
--- a/.github/workflows/test-stack-reusable-workflow.yml
+++ b/.github/workflows/test-stack-reusable-workflow.yml
@@ -54,7 +54,10 @@ jobs:
- name: Spin up ancillary SMTP+Echo message test server
run: |
- # Debug SMTP server/echo message back server
+ # Debug SMTP server/echo message back server, telnet 11080 to it should immediately bounce back the most recent message that tried to send (then you can see if cdio tried to send, the format, etc)
+ # 11025 is the SMTP port for testing
+ # apprise example would be 'mailto://changedetection@localhost:11025/?to=fff@home.com (it will also echo to STDOUT)
+ # telnet localhost 11080
docker run --network changedet-network -d -p 11025:11025 -p 11080:11080 --hostname mailserver test-changedetectionio bash -c 'pip3 install aiosmtpd && python changedetectionio/tests/smtp/smtp-test-server.py'
docker ps
diff --git a/changedetectionio/notification/handler.py b/changedetectionio/notification/handler.py
index b8a6369e..9a4f177d 100644
--- a/changedetectionio/notification/handler.py
+++ b/changedetectionio/notification/handler.py
@@ -6,6 +6,34 @@ from .apprise_plugin.assets import apprise_asset, APPRISE_AVATAR_URL
from ..notification_service import NotificationContextData
+def markup_notification_message(body):
+ """
+ Convert plaintext to HTML with clickable links.
+ Automatically linkifies URLs and converts newlines to
.
+ """
+ from linkify_it import LinkifyIt
+ from html import escape
+
+ linkify = LinkifyIt()
+
+ # Escape HTML first
+ safe_body = escape(body)
+
+ # Convert URLs to links
+ matches = linkify.match(safe_body)
+ if matches:
+ # Process matches in reverse order to maintain string positions
+ for match in reversed(matches):
+ url = match.url
+ safe_body = (
+ safe_body[:match.index] +
+ f'{url}' +
+ safe_body[match.last_index:]
+ )
+
+ # Convert newlines to
+ return safe_body.replace('\n', '
')
+
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
@@ -53,6 +81,10 @@ def process_notification(n_object: NotificationContextData, datastore):
# Get the notification body from datastore
n_body = jinja_render(template_str=n_object.get('notification_body', ''), **notification_parameters)
+
+ if n_object.get('markup_text_to_html'):
+ n_body = markup_notification_message(body=n_body)
+
if n_object.get('notification_format', '').startswith('HTML'):
n_body = n_body.replace("\n", '
')
diff --git a/changedetectionio/notification_service.py b/changedetectionio/notification_service.py
index b36d80c4..8e261b09 100644
--- a/changedetectionio/notification_service.py
+++ b/changedetectionio/notification_service.py
@@ -30,7 +30,8 @@ class NotificationContextData(dict):
'diff_url': None,
'preview_url': None,
'watch_tag': None,
- 'watch_title': None
+ 'watch_title': None,
+ 'markup_text_to_html': False, # If automatic conversion of plaintext to HTML should happen
})
# Apply any initial data passed in
@@ -218,19 +219,6 @@ class NotificationService:
return queued
-
- def markup_notification_message(self, body):
- """
- Convert plaintext or Markdown to HTML.
- Automatically linkifies URLs (requires markdown >= 3.4).
- """
- import markdown
-
- return markdown.markdown(
- body,
- extensions=["linkify", "nl2br", "sane_lists", "extra"]
- )
-
def send_filter_failure_notification(self, watch_uuid):
"""
Send notification when CSS/XPath filters fail consecutively
@@ -245,6 +233,7 @@ class NotificationService:
body = f"""Hello,
Your configured CSS/xPath filters of '{filter_list}' for {{{{watch_url}}}} did not appear on the page after {threshold} attempts.
+
It's possible the page changed layout and the filter needs updating ( Try the 'Visual Selector' tab )
Edit link: {{{{base_url}}}}/edit/{{{{watch_uuid}}}}
@@ -252,14 +241,11 @@ Edit link: {{{{base_url}}}}/edit/{{{{watch_uuid}}}}
Thanks - Your omniscient changedetection.io installation.
"""
-
- if n_format.lower().startswith('html'):
- body = self.markup_notification_message(body=body)
-
n_object = NotificationContextData({
'notification_title': 'Changedetection.io - Alert - CSS/xPath filter was not present in the page',
'notification_body': body,
- 'notification_format': n_format
+ 'notification_format': n_format,
+ 'markup_text_to_html': n_format.lower().startswith('html')
})
if len(watch['notification_urls']):
@@ -294,6 +280,7 @@ Thanks - Your omniscient changedetection.io installation.
body = f"""Hello,
Your configured browser step at position {step} for the web page watch {{{{watch_url}}}} did not appear on the page after {threshold} attempts, did the page change layout?
+
The element may have moved and needs editing, or does it need a delay added?
Edit link: {{{{base_url}}}}/edit/{{{{watch_uuid}}}}
@@ -301,13 +288,11 @@ Edit link: {{{{base_url}}}}/edit/{{{{watch_uuid}}}}
Thanks - Your omniscient changedetection.io installation.
"""
- if n_format.lower().startswith('html'):
- body = self.markup_notification_message(body=body)
-
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
+ 'notification_format': n_format,
+ 'markup_text_to_html': n_format.lower().startswith('html')
})
if len(watch['notification_urls']):
diff --git a/requirements.txt b/requirements.txt
index 7b0ca9f0..af5f2b5a 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -42,8 +42,8 @@ jsonpath-ng~=1.5.3
# Notification library
apprise==1.9.5
-# Should come with apprise but needed for HTML conversion of error notifications
-markdown >=3.5
+# Lightweight URL linkifier for notifications
+linkify-it-py
# - Needed for apprise/spush, and maybe others? hopefully doesnt trigger a rust compile.
# - Requires extra wheel for rPi, adds build time for arm/v8 which is not in piwheels