diff --git a/changedetectionio/notification.py b/changedetectionio/notification.py
index 4c50ef9e..436d08c6 100644
--- a/changedetectionio/notification.py
+++ b/changedetectionio/notification.py
@@ -1,4 +1,5 @@
import apprise
+import time
from jinja2 import Environment, BaseLoader
from apprise import NotifyFormat
import json
@@ -131,90 +132,94 @@ def process_notification(n_object, datastore):
# Initially text or whatever
n_format = datastore.data['settings']['application'].get('notification_format', valid_notification_formats[default_notification_format])
-
# https://github.com/caronc/apprise/wiki/Development_LogCapture
# Anything higher than or equal to WARNING (which covers things like Connection errors)
# raise it as an exception
- apobjs=[]
- sent_objs=[]
+
+ sent_objs = []
from .apprise_asset import asset
- for url in n_object['notification_urls']:
- url = jinja2_env.from_string(url).render(**notification_parameters)
- apobj = apprise.Apprise(debug=True, asset=asset)
- url = url.strip()
- if len(url):
+ apobj = apprise.Apprise(debug=True, asset=asset)
+
+ if not n_object.get('notification_urls'):
+ return None
+
+ with apprise.LogCapture(level=apprise.logging.DEBUG) as logs:
+ for url in n_object['notification_urls']:
+ url = url.strip()
print(">> Process Notification: AppRise notifying {}".format(url))
- with apprise.LogCapture(level=apprise.logging.DEBUG) as logs:
- # Re 323 - Limit discord length to their 2000 char limit total or it wont send.
- # Because different notifications may require different pre-processing, run each sequentially :(
- # 2000 bytes minus -
- # 200 bytes for the overhead of the _entire_ json payload, 200 bytes for {tts, wait, content} etc headers
- # Length of URL - Incase they specify a longer custom avatar_url
-
- # So if no avatar_url is specified, add one so it can be correctly calculated into the total payload
- k = '?' if not '?' in url else '&'
- if not 'avatar_url' in url \
- and not url.startswith('mail') \
- and not url.startswith('post') \
- and not url.startswith('get') \
- and not url.startswith('delete') \
- and not url.startswith('put'):
- url += k + 'avatar_url=https://raw.githubusercontent.com/dgtlmoon/changedetection.io/master/changedetectionio/static/images/avatar-256x256.png'
-
- if url.startswith('tgram://'):
- # Telegram only supports a limit subset of HTML, remove the '
' we place in.
- # re https://github.com/dgtlmoon/changedetection.io/issues/555
- # @todo re-use an existing library we have already imported to strip all non-allowed tags
- n_body = n_body.replace('
', '\n')
- n_body = n_body.replace('', '\n')
- # real limit is 4096, but minus some for extra metadata
- payload_max_size = 3600
- body_limit = max(0, payload_max_size - len(n_title))
- n_title = n_title[0:payload_max_size]
- n_body = n_body[0:body_limit]
-
- elif url.startswith('discord://') or url.startswith('https://discordapp.com/api/webhooks') or url.startswith('https://discord.com/api'):
- # real limit is 2000, but minus some for extra metadata
- payload_max_size = 1700
- body_limit = max(0, payload_max_size - len(n_title))
- n_title = n_title[0:payload_max_size]
- n_body = n_body[0:body_limit]
-
- elif url.startswith('mailto'):
- # Apprise will default to HTML, so we need to override it
- # So that whats' generated in n_body is in line with what is going to be sent.
- # https://github.com/caronc/apprise/issues/633#issuecomment-1191449321
- if not 'format=' in url and (n_format == 'Text' or n_format == 'Markdown'):
- prefix = '?' if not '?' in url else '&'
- # Apprise format is lowercase text https://github.com/caronc/apprise/issues/633
- n_format = n_format.lower()
- url = f"{url}{prefix}format={n_format}"
- # If n_format == HTML, then apprise email should default to text/html and we should be sending HTML only
-
- apobj.add(url)
-
- apobj.notify(
- title=n_title,
- body=n_body,
- body_format=n_format,
- # False is not an option for AppRise, must be type None
- attach=n_object.get('screenshot', None)
- )
-
- apobj.clear()
-
- # Incase it needs to exist in memory for a while after to process(?)
- apobjs.append(apobj)
-
- # Returns empty string if nothing found, multi-line string otherwise
- log_value = logs.getvalue()
- if log_value and 'WARNING' in log_value or 'ERROR' in log_value:
- raise Exception(log_value)
-
- sent_objs.append({'title': n_title,
- 'body': n_body,
- 'url' : url,
- 'body_format': n_format})
+ url = jinja2_env.from_string(url).render(**notification_parameters)
+
+ # Re 323 - Limit discord length to their 2000 char limit total or it wont send.
+ # Because different notifications may require different pre-processing, run each sequentially :(
+ # 2000 bytes minus -
+ # 200 bytes for the overhead of the _entire_ json payload, 200 bytes for {tts, wait, content} etc headers
+ # Length of URL - Incase they specify a longer custom avatar_url
+
+ # So if no avatar_url is specified, add one so it can be correctly calculated into the total payload
+ k = '?' if not '?' in url else '&'
+ if not 'avatar_url' in url \
+ and not url.startswith('mail') \
+ and not url.startswith('post') \
+ and not url.startswith('get') \
+ and not url.startswith('delete') \
+ and not url.startswith('put'):
+ url += k + 'avatar_url=https://raw.githubusercontent.com/dgtlmoon/changedetection.io/master/changedetectionio/static/images/avatar-256x256.png'
+
+ if url.startswith('tgram://'):
+ # Telegram only supports a limit subset of HTML, remove the '
' we place in.
+ # re https://github.com/dgtlmoon/changedetection.io/issues/555
+ # @todo re-use an existing library we have already imported to strip all non-allowed tags
+ n_body = n_body.replace('
', '\n')
+ n_body = n_body.replace('', '\n')
+ # real limit is 4096, but minus some for extra metadata
+ payload_max_size = 3600
+ body_limit = max(0, payload_max_size - len(n_title))
+ n_title = n_title[0:payload_max_size]
+ n_body = n_body[0:body_limit]
+
+ elif url.startswith('discord://') or url.startswith('https://discordapp.com/api/webhooks') or url.startswith(
+ 'https://discord.com/api'):
+ # real limit is 2000, but minus some for extra metadata
+ payload_max_size = 1700
+ body_limit = max(0, payload_max_size - len(n_title))
+ n_title = n_title[0:payload_max_size]
+ n_body = n_body[0:body_limit]
+
+ elif url.startswith('mailto'):
+ # Apprise will default to HTML, so we need to override it
+ # So that whats' generated in n_body is in line with what is going to be sent.
+ # https://github.com/caronc/apprise/issues/633#issuecomment-1191449321
+ if not 'format=' in url and (n_format == 'Text' or n_format == 'Markdown'):
+ prefix = '?' if not '?' in url else '&'
+ # Apprise format is lowercase text https://github.com/caronc/apprise/issues/633
+ n_format = n_format.lower()
+ url = f"{url}{prefix}format={n_format}"
+ # If n_format == HTML, then apprise email should default to text/html and we should be sending HTML only
+
+ apobj.add(url)
+
+ sent_objs.append({'title': n_title,
+ 'body': n_body,
+ 'url': url,
+ 'body_format': n_format})
+
+ # Blast off the notifications tht are set in .add()
+ apobj.notify(
+ title=n_title,
+ body=n_body,
+ body_format=n_format,
+ # False is not an option for AppRise, must be type None
+ attach=n_object.get('screenshot', None)
+ )
+
+ # Give apprise time to register an error
+ time.sleep(3)
+
+ # Returns empty string if nothing found, multi-line string otherwise
+ log_value = logs.getvalue()
+
+ if log_value and 'WARNING' in log_value or 'ERROR' in log_value:
+ raise Exception(log_value)
# Return what was sent for better logging - after the for loop
return sent_objs
diff --git a/changedetectionio/tests/test_notification_errors.py b/changedetectionio/tests/test_notification_errors.py
index da6d851a..c703ebd6 100644
--- a/changedetectionio/tests/test_notification_errors.py
+++ b/changedetectionio/tests/test_notification_errors.py
@@ -1,8 +1,7 @@
import os
import time
-import re
from flask import url_for
-from . util import set_original_response, set_modified_response, live_server_setup
+from .util import set_original_response, set_modified_response, live_server_setup, wait_for_all_checks
import logging
def test_check_notification_error_handling(client, live_server):
@@ -11,7 +10,7 @@ def test_check_notification_error_handling(client, live_server):
set_original_response()
# Give the endpoint time to spin up
- time.sleep(2)
+ time.sleep(1)
# Set a URL and fetch it, then set a notification URL which is going to give errors
test_url = url_for('test_endpoint', _external=True)
@@ -22,12 +21,16 @@ def test_check_notification_error_handling(client, live_server):
)
assert b"Watch added" in res.data
- time.sleep(2)
+ wait_for_all_checks(client)
set_modified_response()
+ working_notification_url = url_for('test_notification_endpoint', _external=True).replace('http', 'json')
+ broken_notification_url = "jsons://broken-url-xxxxxxxx123/test"
+
res = client.post(
url_for("edit_page", uuid="first"),
- data={"notification_urls": "jsons://broken-url-xxxxxxxx123/test",
+ # A URL with errors should not block the one that is working
+ data={"notification_urls": f"{broken_notification_url}\r\n{working_notification_url}",
"notification_title": "xxx",
"notification_body": "xxxxx",
"notification_format": "Text",
@@ -63,4 +66,10 @@ def test_check_notification_error_handling(client, live_server):
found_name_resolution_error = b"Temporary failure in name resolution" in res.data or b"Name or service not known" in res.data
assert found_name_resolution_error
+ # And the working one, which is after the 'broken' one should still have fired
+ with open("test-datastore/notification.txt", "r") as f:
+ notification_submission = f.read()
+ os.unlink("test-datastore/notification.txt")
+ assert 'xxxxx' in notification_submission
+
client.get(url_for("form_delete", uuid="all"), follow_redirects=True)