Notifications - When any in a list of notifications fails, the others should still work (#2106)

pull/2107/head
dgtlmoon 12 months ago committed by GitHub
parent 8be0029260
commit 65428655b8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -1,4 +1,5 @@
import apprise import apprise
import time
from jinja2 import Environment, BaseLoader from jinja2 import Environment, BaseLoader
from apprise import NotifyFormat from apprise import NotifyFormat
import json import json
@ -131,90 +132,94 @@ def process_notification(n_object, datastore):
# Initially text or whatever # Initially text or whatever
n_format = datastore.data['settings']['application'].get('notification_format', valid_notification_formats[default_notification_format]) n_format = datastore.data['settings']['application'].get('notification_format', valid_notification_formats[default_notification_format])
# https://github.com/caronc/apprise/wiki/Development_LogCapture # https://github.com/caronc/apprise/wiki/Development_LogCapture
# Anything higher than or equal to WARNING (which covers things like Connection errors) # Anything higher than or equal to WARNING (which covers things like Connection errors)
# raise it as an exception # raise it as an exception
apobjs=[]
sent_objs=[] sent_objs = []
from .apprise_asset import asset from .apprise_asset import asset
for url in n_object['notification_urls']: apobj = apprise.Apprise(debug=True, asset=asset)
url = jinja2_env.from_string(url).render(**notification_parameters)
apobj = apprise.Apprise(debug=True, asset=asset) if not n_object.get('notification_urls'):
url = url.strip() return None
if len(url):
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)) print(">> Process Notification: AppRise notifying {}".format(url))
with apprise.LogCapture(level=apprise.logging.DEBUG) as logs: 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 :( # Re 323 - Limit discord length to their 2000 char limit total or it wont send.
# 2000 bytes minus - # Because different notifications may require different pre-processing, run each sequentially :(
# 200 bytes for the overhead of the _entire_ json payload, 200 bytes for {tts, wait, content} etc headers # 2000 bytes minus -
# Length of URL - Incase they specify a longer custom avatar_url # 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 '&' # So if no avatar_url is specified, add one so it can be correctly calculated into the total payload
if not 'avatar_url' in url \ k = '?' if not '?' in url else '&'
and not url.startswith('mail') \ if not 'avatar_url' in url \
and not url.startswith('post') \ and not url.startswith('mail') \
and not url.startswith('get') \ and not url.startswith('post') \
and not url.startswith('delete') \ and not url.startswith('get') \
and not url.startswith('put'): and not url.startswith('delete') \
url += k + 'avatar_url=https://raw.githubusercontent.com/dgtlmoon/changedetection.io/master/changedetectionio/static/images/avatar-256x256.png' 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 '<br>' we place in. if url.startswith('tgram://'):
# re https://github.com/dgtlmoon/changedetection.io/issues/555 # Telegram only supports a limit subset of HTML, remove the '<br>' we place in.
# @todo re-use an existing library we have already imported to strip all non-allowed tags # re https://github.com/dgtlmoon/changedetection.io/issues/555
n_body = n_body.replace('<br>', '\n') # @todo re-use an existing library we have already imported to strip all non-allowed tags
n_body = n_body.replace('</br>', '\n') n_body = n_body.replace('<br>', '\n')
# real limit is 4096, but minus some for extra metadata n_body = n_body.replace('</br>', '\n')
payload_max_size = 3600 # real limit is 4096, but minus some for extra metadata
body_limit = max(0, payload_max_size - len(n_title)) payload_max_size = 3600
n_title = n_title[0:payload_max_size] body_limit = max(0, payload_max_size - len(n_title))
n_body = n_body[0:body_limit] 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 elif url.startswith('discord://') or url.startswith('https://discordapp.com/api/webhooks') or url.startswith(
payload_max_size = 1700 'https://discord.com/api'):
body_limit = max(0, payload_max_size - len(n_title)) # real limit is 2000, but minus some for extra metadata
n_title = n_title[0:payload_max_size] payload_max_size = 1700
n_body = n_body[0:body_limit] body_limit = max(0, payload_max_size - len(n_title))
n_title = n_title[0:payload_max_size]
elif url.startswith('mailto'): n_body = n_body[0:body_limit]
# 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. elif url.startswith('mailto'):
# https://github.com/caronc/apprise/issues/633#issuecomment-1191449321 # Apprise will default to HTML, so we need to override it
if not 'format=' in url and (n_format == 'Text' or n_format == 'Markdown'): # So that whats' generated in n_body is in line with what is going to be sent.
prefix = '?' if not '?' in url else '&' # https://github.com/caronc/apprise/issues/633#issuecomment-1191449321
# Apprise format is lowercase text https://github.com/caronc/apprise/issues/633 if not 'format=' in url and (n_format == 'Text' or n_format == 'Markdown'):
n_format = n_format.lower() prefix = '?' if not '?' in url else '&'
url = f"{url}{prefix}format={n_format}" # Apprise format is lowercase text https://github.com/caronc/apprise/issues/633
# If n_format == HTML, then apprise email should default to text/html and we should be sending HTML only n_format = n_format.lower()
url = f"{url}{prefix}format={n_format}"
apobj.add(url) # If n_format == HTML, then apprise email should default to text/html and we should be sending HTML only
apobj.notify( apobj.add(url)
title=n_title,
body=n_body, sent_objs.append({'title': n_title,
body_format=n_format, 'body': n_body,
# False is not an option for AppRise, must be type None 'url': url,
attach=n_object.get('screenshot', None) 'body_format': n_format})
)
# Blast off the notifications tht are set in .add()
apobj.clear() apobj.notify(
title=n_title,
# Incase it needs to exist in memory for a while after to process(?) body=n_body,
apobjs.append(apobj) body_format=n_format,
# False is not an option for AppRise, must be type None
# Returns empty string if nothing found, multi-line string otherwise attach=n_object.get('screenshot', None)
log_value = logs.getvalue() )
if log_value and 'WARNING' in log_value or 'ERROR' in log_value:
raise Exception(log_value) # Give apprise time to register an error
time.sleep(3)
sent_objs.append({'title': n_title,
'body': n_body, # Returns empty string if nothing found, multi-line string otherwise
'url' : url, log_value = logs.getvalue()
'body_format': n_format})
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 what was sent for better logging - after the for loop
return sent_objs return sent_objs

@ -1,8 +1,7 @@
import os import os
import time import time
import re
from flask import url_for 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 import logging
def test_check_notification_error_handling(client, live_server): 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() set_original_response()
# Give the endpoint time to spin up # 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 # 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) 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 assert b"Watch added" in res.data
time.sleep(2) wait_for_all_checks(client)
set_modified_response() 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( res = client.post(
url_for("edit_page", uuid="first"), 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_title": "xxx",
"notification_body": "xxxxx", "notification_body": "xxxxx",
"notification_format": "Text", "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 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 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) client.get(url_for("form_delete", uuid="all"), follow_redirects=True)

Loading…
Cancel
Save