Logging improvements and code tidying (#184)

This commit is contained in:
Chris Caron
2024-04-21 16:51:43 -04:00
committed by GitHub
parent 9be5a8d3c6
commit 711ab2b630
7 changed files with 493 additions and 169 deletions

View File

@ -23,8 +23,10 @@
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
from django.test import SimpleTestCase
from django.core.exceptions import RequestDataTooBig
from apprise import ConfigFormat
from unittest.mock import patch
from unittest import mock
from django.test.utils import override_settings
from ..forms import AUTO_DETECT_CONFIG_KEYWORD
import json
@ -139,6 +141,18 @@ class AddTests(SimpleTestCase):
)
assert response.status_code == 200
with mock.patch('json.loads') as mock_loads:
mock_loads.side_effect = RequestDataTooBig()
# Send our notification by specifying the tag in the parameters
response = self.client.post(
'/add/{}'.format(key),
data=json.dumps({'urls': 'mailto://user:pass@yahoo.ca'}),
content_type='application/json',
)
# Our notification failed
assert response.status_code == 431
# Test with JSON (and no payload provided)
response = self.client.post(
'/add/{}'.format(key),

View File

@ -24,6 +24,7 @@
# THE SOFTWARE.
from django.test import SimpleTestCase, override_settings
from django.core.files.uploadedfile import SimpleUploadedFile
from django.core.exceptions import RequestDataTooBig
from unittest import mock
import requests
from ..forms import NotifyForm
@ -577,6 +578,22 @@ class NotifyTests(SimpleTestCase):
assert response['message'] == form_data['body']
assert response['type'] == apprise.NotifyType.WARNING
# Test case where RequestDataTooBig thrown
# Reset our mock object
mock_post.reset_mock()
with mock.patch('json.loads') as mock_loads:
mock_loads.side_effect = RequestDataTooBig()
# Send our notification by specifying the tag in the parameters
response = self.client.post(
f'/notify/{key}?tag=home&body=test',
form_data,
content_type='application/json')
# Our notification failed
assert response.status_code == 431
assert mock_post.call_count == 0
@mock.patch('requests.post')
def test_advanced_notify_with_tags(self, mock_post):
"""
@ -883,7 +900,7 @@ class NotifyTests(SimpleTestCase):
# Preare our JSON data
json_data = {
'body': 'test notifiction',
'body': 'test notification',
'type': apprise.NotifyType.WARNING,
}
@ -1059,6 +1076,7 @@ class NotifyTests(SimpleTestCase):
headers = {
'HTTP_X_APPRISE_LOG_LEVEL': 'debug',
# Accept is over-ridden to be that of the content type
'HTTP_ACCEPT': 'text/plain',
}
@ -1074,13 +1092,14 @@ class NotifyTests(SimpleTestCase):
assert mock_notify.call_count == 1
assert response['content-type'] == 'text/plain'
mock_notify.reset_mock()
headers = {
'HTTP_X_APPRISE_LOG_LEVEL': 'debug',
# Accept is over-ridden to be that of the content type
'HTTP_ACCEPT': 'text/html',
}
mock_notify.reset_mock()
# Test referencing a key that doesn't exist
response = self.client.post(
'/notify/{}'.format(key),
@ -1093,6 +1112,38 @@ class NotifyTests(SimpleTestCase):
assert mock_notify.call_count == 1
assert response['content-type'] == 'text/html'
mock_notify.reset_mock()
# Test referencing a key that doesn't exist
response = self.client.post(
'/notify/{}'.format(key),
data={'body': 'test'},
**headers,
)
assert response.status_code == 200
assert mock_notify.call_count == 1
assert response['content-type'].startswith('text/html')
mock_notify.reset_mock()
headers = {
'HTTP_X_APPRISE_LOG_LEVEL': 'debug',
'HTTP_ACCEPT': '*/*',
}
# Test referencing a key that doesn't exist
response = self.client.post(
'/notify/{}'.format(key),
data=json.dumps(json_data),
content_type='application/json',
**headers,
)
assert response.status_code == 200
assert mock_notify.call_count == 1
assert response['content-type'] == 'application/json'
headers = {
'HTTP_X_APPRISE_LOG_LEVEL': 'invalid',
'HTTP_ACCEPT': 'text/*',
@ -1112,6 +1163,19 @@ class NotifyTests(SimpleTestCase):
assert mock_notify.call_count == 1
assert response['content-type'] == 'text/html'
mock_notify.reset_mock()
# Test referencing a key that doesn't exist
response = self.client.post(
'/notify/{}'.format(key),
data=json_data,
**headers,
)
assert response.status_code == 200
assert mock_notify.call_count == 1
assert response['content-type'].startswith('text/html')
@mock.patch('apprise.plugins.NotifyEmail.NotifyEmail.send')
def test_notify_with_filters(self, mock_send):
"""

View File

@ -24,6 +24,7 @@
# THE SOFTWARE.
from django.test import SimpleTestCase
from django.core.files.uploadedfile import SimpleUploadedFile
from django.core.exceptions import RequestDataTooBig
from django.test.utils import override_settings
from unittest import mock
from ..forms import NotifyByUrlForm
@ -442,6 +443,22 @@ class StatelessNotifyTests(SimpleTestCase):
assert response.status_code == 200
assert mock_notify.call_count == 1
# Reset our count
mock_notify.reset_mock()
with mock.patch('json.loads') as mock_loads:
mock_loads.side_effect = RequestDataTooBig()
# Send our notification
response = self.client.post(
'/notify/?title=my%20title&format=text&type=info',
data=json.dumps(json_data),
content_type='application/json',
)
# Our notification failed
assert response.status_code == 431
assert mock_notify.call_count == 0
@mock.patch('apprise.Apprise.notify')
def test_notify_by_loaded_urls_with_json(self, mock_notify):
"""

View File

@ -27,6 +27,7 @@ from django.http import HttpResponse
from django.http import JsonResponse
from django.views import View
from django.conf import settings
from django.core.exceptions import RequestDataTooBig
from django.utils.html import escape
from django.utils.decorators import method_decorator
from django.views.decorators.cache import never_cache
@ -55,7 +56,6 @@ logger = logging.getLogger('django')
# Content-Type Parsing
# application/x-www-form-urlencoded
# application/x-www-form-urlencoded
# multipart/form-data
MIME_IS_FORM = re.compile(
r'(multipart|application)/(x-www-)?form-(data|urlencoded)', re.I)
@ -68,6 +68,11 @@ MIME_IS_FORM = re.compile(
MIME_IS_JSON = re.compile(
r'(text|application)/(x-)?json', re.I)
# Parsing of Accept; the following amounts to Accept All
# */*
# <blank>
ACCEPT_ALL = re.compile(r'^\s*([*]/[*]|)\s*$', re.I)
# Tags separated by space , &, or + are and'ed together
# Tags separated by commas (even commas wrapped in spaces) are "or'ed" together
# We start with a regular expression used to clean up provided tags
@ -111,6 +116,7 @@ class ResponseCode(object):
method_not_allowed = 405
method_not_accepted = 406
failed_dependency = 424
fields_too_large = 431
internal_server_error = 500
@ -215,20 +221,27 @@ class AddView(View):
"""
Handle a POST request
"""
# Detect the format our response should be in
json_response = \
# Detect the format our incoming payload
json_payload = \
MIME_IS_JSON.match(
request.content_type
if request.content_type
else request.headers.get(
'accept', request.headers.get(
'content-type', ''))) is not None
'content-type', '')) is not None
# Detect the format our response should be in
json_response = True if json_payload \
and ACCEPT_ALL.match(request.headers.get('accept', '')) else \
MIME_IS_JSON.match(request.headers.get('accept', '')) is not None
if settings.APPRISE_CONFIG_LOCK:
# General Access Control
msg = _('The site has been configured to deny this request.')
logger.warning(
'ADD - %s - Config Lock Active - Request Denied',
request.META['REMOTE_ADDR'])
msg = _('The site has been configured to deny this request')
status = ResponseCode.no_access
return HttpResponse(msg, status=status) \
return HttpResponse(msg, status=status, content_type='text/plain') \
if not json_response else JsonResponse({
'error': msg,
},
@ -239,7 +252,7 @@ class AddView(View):
# our content
content = {}
if MIME_IS_FORM.match(request.content_type):
if not json_payload:
content = {}
form = AddByConfigForm(request.POST)
if form.is_valid():
@ -249,27 +262,49 @@ class AddView(View):
if form.is_valid():
content.update(form.cleaned_data)
elif json_response:
else: # JSON Payload
# Prepare our default response
try:
# load our JSON content
content = json.loads(request.body.decode('utf-8'))
except (RequestDataTooBig):
# DATA_UPLOAD_MAX_MEMORY_SIZE exceeded it's value; this is usually the case
# when there is a very large flie attachment that can't be pulled out of the
# payload without exceeding memory limitations (default is 3MB)
logger.warning(
'ADD - %s - JSON Payload Exceeded %dMB; operaton aborted using KEY: %s',
request.META['REMOTE_ADDR'], (settings.DATA_UPLOAD_MAX_MEMORY_SIZE / 1048576), key)
status = ResponseCode.fields_too_large
msg = _('JSON Payload provided is to large')
return HttpResponse(msg, status=status, content_type='text/plain') \
if not json_response else JsonResponse({
'error': msg,
}, encoder=JSONEncoder, safe=False, status=status)
except (AttributeError, ValueError):
# could not parse JSON response...
return JsonResponse({
'error': _('Invalid JSON specified.'),
},
encoder=JSONEncoder,
safe=False,
status=ResponseCode.bad_request,
)
logger.warning(
'ADD - %s - Invalid JSON Payload provided using KEY: %s',
request.META['REMOTE_ADDR'], key)
status = ResponseCode.bad_request
msg = _('Invalid JSON Payload provided')
return HttpResponse(msg, status=status, content_type='text/plain') \
if not json_response else JsonResponse({
'error': msg,
}, encoder=JSONEncoder, safe=False, status=status)
if not content:
# No information was posted
msg = _('The message format is not supported.')
logger.warning(
'ADD - %s - Invalid payload structure provided using KEY: %s',
request.META['REMOTE_ADDR'], key)
msg = _('Invalid payload structure provided')
status = ResponseCode.bad_request
return HttpResponse(msg, status=status) \
return HttpResponse(msg, status=status, content_type='text/plain') \
if not json_response else JsonResponse({
'error': msg,
},
@ -285,9 +320,13 @@ class AddView(View):
a_obj.add(content['urls'])
if not len(a_obj):
# No URLs were loaded
msg = _('No valid URLs were found.')
logger.warning(
'ADD - %s - No valid URLs defined using KEY: %s',
request.META['REMOTE_ADDR'], key)
msg = _('No valid URLs defined')
status = ResponseCode.bad_request
return HttpResponse(msg, status=status) \
return HttpResponse(msg, status=status, content_type='text/plain') \
if not json_response else JsonResponse({
'error': msg,
},
@ -300,9 +339,12 @@ class AddView(View):
key, '\r\n'.join([s.url() for s in a_obj]),
apprise.ConfigFormat.TEXT):
msg = _('The configuration could not be saved.')
logger.warning(
'ADD - %s - configuration could not be saved using KEY: %s',
request.META['REMOTE_ADDR'], key)
msg = _('The configuration could not be saved')
status = ResponseCode.internal_server_error
return HttpResponse(msg, status=status) \
return HttpResponse(msg, status=status, content_type='text/plain') \
if not json_response else JsonResponse({
'error': msg,
},
@ -315,9 +357,12 @@ class AddView(View):
fmt = content.get('format', '').lower()
if fmt not in [i[0] for i in CONFIG_FORMATS]:
# Format must be one supported by apprise
msg = _('The format specified is invalid.')
logger.warning(
'ADD - %s - Invalid configuration format specified (%s) using KEY: %s',
request.META['REMOTE_ADDR'], fmt, key)
msg = _('Invalid configuration format specified')
status = ResponseCode.bad_request
return HttpResponse(msg, status=status) \
return HttpResponse(msg, status=status, content_type='text/plain') \
if not json_response else JsonResponse({
'error': msg,
},
@ -337,9 +382,12 @@ class AddView(View):
# Load our configuration
if not ac_obj.add_config(content['config'], format=fmt):
# The format could not be detected
msg = _('The configuration format could not be detected.')
logger.warning(
'ADD - %s - The configuration format could not be auto-detected using KEY: %s',
request.META['REMOTE_ADDR'], key)
msg = _('The configuration format could not be auto-detected')
status = ResponseCode.bad_request
return HttpResponse(msg, status=status) \
return HttpResponse(msg, status=status, content_type='text/plain') \
if not json_response else JsonResponse({
'error': msg,
},
@ -354,9 +402,12 @@ class AddView(View):
if not len(a_obj):
# No specified URL(s) were loaded due to
# mis-configuration on the caller's part
msg = _('No valid URL(s) were specified.')
logger.warning(
'ADD - %s - No valid URL(s) defined using KEY: %s',
request.META['REMOTE_ADDR'], key)
msg = _('No valid URL(s) defined')
status = ResponseCode.bad_request
return HttpResponse(msg, status=status) \
return HttpResponse(msg, status=status, content_type='text/plain') \
if not json_response else JsonResponse({
'error': msg,
},
@ -368,9 +419,12 @@ class AddView(View):
if not ConfigCache.put(
key, content['config'], fmt=ac_obj[0].config_format):
# Something went very wrong; return 500
msg = _('An error occured saving configuration.')
logger.error(
'ADD - %s - Configuration could not be saved using KEY: %s',
request.META['REMOTE_ADDR'], key)
msg = _('An error occured saving configuration')
status = ResponseCode.internal_server_error
return HttpResponse(msg, status=status) \
return HttpResponse(msg, status=status, content_type='text/plain') \
if not json_response else JsonResponse({
'error': msg,
},
@ -381,9 +435,12 @@ class AddView(View):
else:
# No configuration specified; we're done
msg = _('No configuration specified.')
msg = _('No configuration provided')
logger.warning(
'ADD - %s - No configuration provided using KEY: %s',
request.META['REMOTE_ADDR'], key)
status = ResponseCode.bad_request
return HttpResponse(msg, status=status) \
return HttpResponse(msg, status=status, content_type='text/plain') \
if not json_response else JsonResponse({
'error': msg,
},
@ -394,10 +451,16 @@ class AddView(View):
# If we reach here; we successfully loaded the configuration so we can
# go ahead and write it to disk and alert our caller of the success.
return HttpResponse(
_('Successfully saved configuration.'),
status=ResponseCode.okay,
)
logger.info(
'ADD - %s - Configuration saved using KEY: %s',
request.META['REMOTE_ADDR'], key)
status = ResponseCode.okay
msg = _('Successfully saved configuration')
return HttpResponse(msg, status=status, content_type='text/plain') \
if not json_response else JsonResponse({
"error": None,
}, encoder=JSONEncoder, safe=False, status=status)
@method_decorator(never_cache, name='dispatch')
@ -420,9 +483,12 @@ class DelView(View):
if settings.APPRISE_CONFIG_LOCK:
# General Access Control
msg = _('The site has been configured to deny this request.')
logger.warning(
'DEL - %s - Config Lock Active - Request Denied',
request.META['REMOTE_ADDR'])
msg = _('The site has been configured to deny this request')
status = ResponseCode.no_access
return HttpResponse(msg, status=status) \
return HttpResponse(msg, status=status, content_type='text/plain') \
if not json_response else JsonResponse({
'error': msg,
},
@ -434,9 +500,12 @@ class DelView(View):
# Clear the key
result = ConfigCache.clear(key)
if result is None:
msg = _('There was no configuration to remove.')
logger.warning(
'DEL - %s - No configuration associated using KEY: %s',
request.META['REMOTE_ADDR'], key)
msg = _('There was no configuration to remove')
status = ResponseCode.no_content
return HttpResponse(msg, status=status) \
return HttpResponse(msg, status=status, content_type='text/plain') \
if not json_response else JsonResponse({
'error': msg,
},
@ -447,9 +516,13 @@ class DelView(View):
elif result is False:
# There was a failure at the os level
msg = _('The configuration could not be removed.')
logger.error(
'DEL - %s - Configuration could not be removed associated using KEY: %s',
request.META['REMOTE_ADDR'], key)
msg = _('The configuration could not be removed')
status = ResponseCode.internal_server_error
return HttpResponse(msg, status=status) \
return HttpResponse(msg, status=status, content_type='text/plain') \
if not json_response else JsonResponse({
'error': msg,
},
@ -459,10 +532,16 @@ class DelView(View):
)
# Removed content
return HttpResponse(
_('Successfully removed configuration.'),
status=ResponseCode.okay,
)
logger.info(
'DEL - %s - Removed configuration associated using KEY: %s',
request.META['REMOTE_ADDR'], key)
status = ResponseCode.okay
msg = _('Successfully removed configuration')
return HttpResponse(msg, status=status, content_type='text/plain') \
if not json_response else JsonResponse({
"error": None,
}, encoder=JSONEncoder, safe=False, status=status)
@method_decorator((gzip_page, never_cache), name='dispatch')
@ -486,9 +565,13 @@ class GetView(View):
if settings.APPRISE_CONFIG_LOCK:
# General Access Control
msg = _('The site has been configured to deny this request.')
logger.warning(
'VIEW - %s - Config Lock Active - Request Denied',
request.META['REMOTE_ADDR'])
msg = _('The site has been configured to deny this request')
status = ResponseCode.no_access
return HttpResponse(msg, status=status) \
return HttpResponse(msg, status=status, content_type='text/plain') \
if not json_response else JsonResponse(
{'error': msg},
encoder=JSONEncoder,
@ -505,9 +588,12 @@ class GetView(View):
# config != None: we simply have no data
if format is not None:
# no content to return
msg = _('There was no configuration found.')
logger.warning(
'VIEW - %s - No configuration associated using KEY: %s',
request.META['REMOTE_ADDR'], key)
msg = _('There was no configuration found')
status = ResponseCode.no_content
return HttpResponse(msg, status=status) \
return HttpResponse(msg, status=status, content_type='text/plain') \
if not json_response else JsonResponse(
{'error': msg},
encoder=JSONEncoder,
@ -515,9 +601,12 @@ class GetView(View):
status=status)
# Something went very wrong; return 500
msg = _('An error occured accessing configuration.')
logger.error(
'VIEW - %s - Configuration could not be accessed associated using KEY: %s',
request.META['REMOTE_ADDR'], key)
msg = _('An error occured accessing configuration')
status = ResponseCode.internal_server_error
return HttpResponse(msg, status=status) \
return HttpResponse(msg, status=status, content_type='text/plain') \
if not json_response else JsonResponse({
'error': msg,
},
@ -532,9 +621,12 @@ class GetView(View):
# reference to it through the --config (-c) option in the CLI.
content_type = 'text/yaml; charset=utf-8' \
if format == apprise.ConfigFormat.YAML \
else 'text/html; charset=utf-8'
else 'text/plain; charset=utf-8'
# Return our retrieved content
logger.info(
'VIEW - %s - Retrieved configuration associated using KEY: %s',
request.META['REMOTE_ADDR'], key)
return HttpResponse(
config,
content_type=content_type,
@ -557,49 +649,69 @@ class NotifyView(View):
"""
Handle a POST request
"""
# Detect the format our response should be in
json_response = \
# Detect the format our incoming payload
json_payload = \
MIME_IS_JSON.match(
request.content_type
if request.content_type
else request.headers.get(
'accept', request.headers.get(
'content-type', ''))) is not None
'content-type', '')) is not None
# Detect the format our response should be in
json_response = True if json_payload \
and ACCEPT_ALL.match(request.headers.get('accept', '')) else \
MIME_IS_JSON.match(request.headers.get('accept', '')) is not None
# our content
content = {}
if MIME_IS_FORM.match(request.content_type):
if not json_payload:
form = NotifyForm(data=request.POST, files=request.FILES)
if form.is_valid():
content.update(form.cleaned_data)
elif json_response:
else: # JSON Payload
# Prepare our default response
try:
# load our JSON content
content = json.loads(request.body.decode('utf-8'))
except (RequestDataTooBig):
# DATA_UPLOAD_MAX_MEMORY_SIZE exceeded it's value; this is usually the case
# when there is a very large flie attachment that can't be pulled out of the
# payload without exceeding memory limitations (default is 3MB)
logger.warning(
'NOTIFY - %s - JSON Payload Exceeded %dMB using KEY: %s',
request.META['REMOTE_ADDR'], (settings.DATA_UPLOAD_MAX_MEMORY_SIZE / 1048576), key)
status = ResponseCode.fields_too_large
msg = _('JSON Payload provided is to large')
return HttpResponse(msg, status=status, content_type='text/plain') \
if not json_response else JsonResponse({
'error': msg,
}, encoder=JSONEncoder, safe=False, status=status)
except (AttributeError, ValueError):
# could not parse JSON response...
logger.warning(
'NOTIFY - %s - Invalid JSON Payload provided',
request.META['REMOTE_ADDR'])
'NOTIFY - %s - Invalid JSON Payload provided using KEY: %s',
request.META['REMOTE_ADDR'], key)
return JsonResponse(
_('Invalid JSON provided.'),
encoder=JSONEncoder,
safe=False,
status=ResponseCode.bad_request)
status = ResponseCode.bad_request
msg = _('Invalid JSON Payload provided')
return HttpResponse(msg, status=status, content_type='text/plain') \
if not json_response else JsonResponse({
'error': msg,
}, encoder=JSONEncoder, safe=False, status=status)
if not content:
# We could not handle the Content-Type
logger.warning(
'NOTIFY - %s - Invalid FORM Payload provided',
request.META['REMOTE_ADDR'])
'NOTIFY - %s - Invalid FORM Payload provided using KEY: %s',
request.META['REMOTE_ADDR'], key)
msg = _('Bad FORM Payload provided.')
msg = _('Bad FORM Payload provided')
status = ResponseCode.bad_request
return HttpResponse(msg, status=status) \
return HttpResponse(msg, status=status, content_type='text/plain') \
if not json_response else JsonResponse({
'error': msg,
},
@ -632,12 +744,15 @@ class NotifyView(View):
except (TypeError, ValueError) as e:
# Invalid entry found in list
logger.warning(
'NOTIFY - %s - Bad attachment: %s',
request.META['REMOTE_ADDR'], str(e))
'NOTIFY - %s - Bad attachment using KEY: %s - %s',
request.META['REMOTE_ADDR'], key, str(e))
return HttpResponse(
_('Bad attachment'),
status=ResponseCode.bad_request)
status = ResponseCode.bad_request
msg = _('Bad Attachment')
return HttpResponse(msg, status=status, content_type='text/plain') \
if not json_response else JsonResponse({
'error': msg,
}, encoder=JSONEncoder, safe=False, status=status)
#
# Allow 'tag' value to be specified as part of the URL parameters
@ -670,12 +785,12 @@ class NotifyView(View):
# Invalid entry found in list
logger.warning(
'NOTIFY - %s - Ignored invalid tag specified '
'(type %s): %s', request.META['REMOTE_ADDR'],
str(type(tag)), str(tag)[:12])
'(type %s): %s using KEY: %s', request.META['REMOTE_ADDR'],
str(type(tag)), str(tag)[:12], key)
msg = _('Unsupported characters found in tag definition.')
msg = _('Unsupported characters found in tag definition')
status = ResponseCode.bad_request
return HttpResponse(msg, status=status) \
return HttpResponse(msg, status=status, content_type='text/plain') \
if not json_response else JsonResponse({
'error': msg,
},
@ -704,12 +819,12 @@ class NotifyView(View):
else: # Could be int, float or some other unsupported type
logger.warning(
'NOTIFY - %s - Ignored invalid tag specified (type %s): '
'%s', request.META['REMOTE_ADDR'],
str(type(tag)), str(tag)[:12])
'%s using KEY: %s', request.META['REMOTE_ADDR'],
str(type(tag)), str(tag)[:12], key)
msg = _('Unsupported characters found in tag definition.')
msg = _('Unsupported characters found in tag definition')
status = ResponseCode.bad_request
return HttpResponse(msg, status=status) \
return HttpResponse(msg, status=status, content_type='text/plain') \
if not json_response else JsonResponse({
'error': msg,
},
@ -744,12 +859,14 @@ class NotifyView(View):
not in apprise.NOTIFY_TYPES:
logger.warning(
'NOTIFY - %s - Payload lacks minimum requirements',
request.META['REMOTE_ADDR'])
'NOTIFY - %s - Payload lacks minimum requirements using KEY: %s',
request.META['REMOTE_ADDR'], key)
return HttpResponse(msg, status=status) \
status = ResponseCode.bad_request
msg = _('Payload lacks minimum requirements')
return HttpResponse(msg, status=status, content_type='text/plain') \
if not json_response else JsonResponse({
'error': _('Payload lacks minimum requirements.'),
'error': msg,
},
encoder=JSONEncoder,
safe=False,
@ -761,11 +878,11 @@ class NotifyView(View):
if body_format and body_format not in apprise.NOTIFY_FORMATS:
logger.warning(
'NOTIFY - %s - Format parameter contains an unsupported '
'value (%s)', request.META['REMOTE_ADDR'], str(body_format))
'value (%s) using KEY: %s', request.META['REMOTE_ADDR'], str(body_format), key)
msg = _('An invalid body input format was specified.')
msg = _('An invalid body input format was specified')
status = ResponseCode.bad_request
return HttpResponse(msg, status=status) \
return HttpResponse(msg, status=status, content_type='text/plain') \
if not json_response else JsonResponse({
'error': msg,
},
@ -789,9 +906,10 @@ class NotifyView(View):
logger.debug(
'NOTIFY - %s - Empty configuration found using KEY: %s',
request.META['REMOTE_ADDR'], key)
msg = _('There was no configuration found.')
msg = _('There was no configuration found')
status = ResponseCode.no_content
return HttpResponse(msg, status=status) \
return HttpResponse(msg, status=status, content_type='text/plain') \
if not json_response else JsonResponse({
'error': msg,
},
@ -800,13 +918,14 @@ class NotifyView(View):
status=status,
)
# Something went very wrong; return 500
msg = _('An error occured accessing configuration.')
status = ResponseCode.internal_server_error
logger.error(
'NOTIFY - %s - I/O error accessing configuration '
'using KEY: %s', request.META['REMOTE_ADDR'], key)
return HttpResponse(msg, status=status) \
# Something went very wrong; return 500
msg = _('An error occured accessing configuration')
status = ResponseCode.internal_server_error
return HttpResponse(msg, status=status, content_type='text/plain') \
if not json_response else JsonResponse({
'error': msg,
},
@ -839,9 +958,13 @@ class NotifyView(View):
'NOTIFY - %s - Recursion limit reached (%d > %d)',
request.META['REMOTE_ADDR'], recursion,
settings.APPRISE_RECURSION_MAX)
return HttpResponse(
_('The recursion limit has been reached.'),
status=ResponseCode.method_not_accepted)
status = ResponseCode.method_not_accepted
msg = _('The recursion limit has been reached')
return HttpResponse(msg, status=status, content_type='text/plain') \
if not json_response else JsonResponse({
'error': msg,
}, encoder=JSONEncoder, safe=False, status=status)
# Store our recursion value for our AppriseAsset() initialization
kwargs['_recursion'] = recursion
@ -850,9 +973,13 @@ class NotifyView(View):
logger.warning(
'NOTIFY - %s - Invalid recursion value (%s) provided',
request.META['REMOTE_ADDR'], str(recursion))
return HttpResponse(
_('An invalid recursion value was specified.'),
status=ResponseCode.bad_request)
status = ResponseCode.bad_request
msg = _('An invalid recursion value was specified')
return HttpResponse(msg, status=status, content_type='text/plain') \
if not json_response else JsonResponse({
'error': msg,
}, encoder=JSONEncoder, safe=False, status=status)
# Acquire our unique identifier (if defined)
uid = request.headers.get('X-Apprise-ID', '').strip()
@ -884,11 +1011,15 @@ class NotifyView(View):
# we return the logs as they're processed in their text format.
# The HTML response type has a bit of overhead where as it's not
# the case with text/plain
content_type = \
'text/html' if re.search(r'text\/(\*|html)',
request.headers.get('Accept', ''),
re.IGNORECASE) \
else 'text/plain'
if not json_response:
content_type = \
'text/html' if re.search(r'text\/(\*|html)',
request.headers.get('Accept', ''),
re.IGNORECASE) \
else 'text/plain'
else:
content_type = 'application/json'
# Acquire our log level from headers if defined, otherwise use
# the global one set in the settings
@ -923,13 +1054,17 @@ class NotifyView(View):
esc = '<!!-!ESC!-!!>'
# Format is only updated if the content_type is html
fmt = '<li class="log_%(levelname)s">' \
'<div class="log_time">%(asctime)s</div>' \
'<div class="log_level">%(levelname)s</div>' \
f'<div class="log_msg">{esc}%(message)s{esc}</div></li>' \
if content_type == 'text/html' else \
settings.LOGGING['formatters']['standard']['format']
if json_response:
fmt = f'["%(levelname)s","%(asctime)s","{esc}%(message)s{esc}"]'
else:
# Format is only updated if the content_type is html
fmt = '<li class="log_%(levelname)s">' \
'<div class="log_time">%(asctime)s</div>' \
'<div class="log_level">%(levelname)s</div>' \
f'<div class="log_msg">{esc}%(message)s{esc}</div></li>' \
if content_type == 'text/html' else \
settings.LOGGING['formatters']['standard']['format']
# Now specify our format (and over-ride the default):
with apprise.LogCapture(level=level, fmt=fmt) as logs:
@ -942,15 +1077,25 @@ class NotifyView(View):
attach=attach,
)
if content_type == 'text/html':
if json_response:
esc = re.escape(esc)
entries = re.findall(
r'\r*\n?(?P<head>\["[^"]+","[^"]+",)"{}(?P<to_escape>.+?)'
r'{}"(?P<tail>\]\r*\n?$)(?=$|\r*\n?\["[^"]+","[^"]+","{})'.format(
esc, esc, esc), logs.getvalue(), re.DOTALL | re.MULTILINE)
# Prepare ourselves a JSON Response
response = json.loads('[{}]'.format(
','.join([e[0] + json.dumps(e[1].rstrip()) + e[2] for e in entries])))
elif content_type == 'text/html':
# Iterate over our entries so that we can prepare to escape
# things to be presented as HTML
esc = re.escape(esc)
entries = re.findall(
r'(?P<head><li .+?){}(?P<to_escape>.*?)'
r'{}(?P<tail>.+li>$)(?=$|<li .+{})'.format(
esc, esc, esc), logs.getvalue(),
re.DOTALL)
r'\r*\n?(?P<head><li class="log_.+?){}(?P<to_escape>.+?)'
r'{}(?P<tail></div></li>\r*\n?$)(?=$|\r*\n?<li class="log_.+{})'.format(
esc, esc, esc), logs.getvalue(), re.DOTALL | re.MULTILINE)
# Wrap logs in `<ul>` tag and escape our message body:
response = '<ul class="logs">{}</ul>'.format(
@ -972,15 +1117,16 @@ class NotifyView(View):
if not result:
# If at least one notification couldn't be sent; change up
# the response to a 424 error code
msg = _('One or more notification could not be sent.')
msg = _('One or more notification could not be sent')
status = ResponseCode.failed_dependency
logger.warning(
'NOTIFY - %s - One or more notifications not '
'sent%s using KEY: %s', request.META['REMOTE_ADDR'],
'' if not tag else f' (Tags: {tag})', key)
return HttpResponse(response if response else msg, status=status) \
return HttpResponse(response if response else msg, status=status, content_type=content_type) \
if not json_response else JsonResponse({
'error': msg,
'details': response,
},
encoder=JSONEncoder,
safe=False,
@ -992,13 +1138,13 @@ class NotifyView(View):
request.META['REMOTE_ADDR'],
'' if not tag else f'Tags: {tag}, ', key)
# Return our retrieved content
return HttpResponse(
response if response is not None else
_('Notification(s) sent.'),
content_type=content_type,
status=ResponseCode.okay,
)
# Return our success message
status = ResponseCode.okay
return HttpResponse(response, status=status, content_type=content_type) \
if not json_response else JsonResponse({
'error': None,
'details': response,
}, encoder=JSONEncoder, safe=False, status=status)
@method_decorator((gzip_page, never_cache), name='dispatch')
@ -1010,29 +1156,60 @@ class StatelessNotifyView(View):
"""
Handle a POST request
"""
# Detect the format our incoming payload
json_payload = \
MIME_IS_JSON.match(
request.content_type
if request.content_type
else request.headers.get(
'content-type', '')) is not None
# Detect the format our response should be in
json_response = True if json_payload \
and ACCEPT_ALL.match(request.headers.get('accept', '')) else \
MIME_IS_JSON.match(request.headers.get('accept', '')) is not None
# our content
content = {}
if MIME_IS_FORM.match(request.content_type):
if not json_payload:
content = {}
form = NotifyByUrlForm(request.POST, request.FILES)
if form.is_valid():
content.update(form.cleaned_data)
elif MIME_IS_JSON.match(request.content_type):
else: # JSON Payload
# Prepare our default response
try:
# load our JSON content
content = json.loads(request.body.decode('utf-8'))
except (RequestDataTooBig):
# DATA_UPLOAD_MAX_MEMORY_SIZE exceeded it's value; this is usually the case
# when there is a very large flie attachment that can't be pulled out of the
# payload without exceeding memory limitations (default is 3MB)
logger.warning(
'NOTIFY - %s - JSON Payload Exceeded %dMB; operaton aborted',
request.META['REMOTE_ADDR'], (settings.DATA_UPLOAD_MAX_MEMORY_SIZE / 1048576))
status = ResponseCode.fields_too_large
msg = _('JSON Payload provided is to large')
return HttpResponse(msg, status=status, content_type='text/plain') \
if not json_response else JsonResponse({
'error': msg,
}, encoder=JSONEncoder, safe=False, status=status)
except (AttributeError, ValueError):
# could not parse JSON response...
logger.warning(
'NOTIFY - %s - Invalid JSON Payload provided',
request.META['REMOTE_ADDR'])
return HttpResponse(
_('Invalid JSON specified.'),
status=ResponseCode.bad_request)
status = ResponseCode.bad_request
msg = _('Invalid JSON Payload provided')
return HttpResponse(msg, status=status, content_type='text/plain') \
if not json_response else JsonResponse({
'error': msg,
}, encoder=JSONEncoder, safe=False, status=status)
if not content:
# We could not handle the Content-Type
@ -1040,9 +1217,12 @@ class StatelessNotifyView(View):
'NOTIFY - %s - Invalid FORM Payload provided',
request.META['REMOTE_ADDR'])
return HttpResponse(
_('Bad FORM Payload provided.'),
status=ResponseCode.bad_request)
status = ResponseCode.bad_request
msg = _('Bad FORM Payload provided')
return HttpResponse(msg, status=status, content_type='text/plain') \
if not json_response else JsonResponse({
'error': msg,
}, encoder=JSONEncoder, safe=False, status=status)
if not content.get('urls') and settings.APPRISE_STATELESS_URLS:
# fallback to settings.APPRISE_STATELESS_URLS if no urls were
@ -1079,9 +1259,12 @@ class StatelessNotifyView(View):
'NOTIFY - %s - Payload lacks minimum requirements',
request.META['REMOTE_ADDR'])
return HttpResponse(
_('Payload lacks minimum requirements.'),
status=ResponseCode.bad_request)
status = ResponseCode.bad_request
msg = _('Payload lacks minimum requirements')
return HttpResponse(msg, status=status, content_type='text/plain') \
if not json_response else JsonResponse({
'error': msg,
}, encoder=JSONEncoder, safe=False, status=status)
# Acquire our body format (if identified)
body_format = content.get('format', apprise.NotifyFormat.TEXT)
@ -1089,9 +1272,13 @@ class StatelessNotifyView(View):
logger.warning(
'NOTIFY - %s - Format parameter contains an unsupported '
'value (%s)', request.META['REMOTE_ADDR'], str(body_format))
return HttpResponse(
_('An invalid body input format was specified.'),
status=ResponseCode.bad_request)
status = ResponseCode.bad_request
msg = _('An invalid body input format was specified')
return HttpResponse(msg, status=status, content_type='text/plain') \
if not json_response else JsonResponse({
'error': msg,
}, encoder=JSONEncoder, safe=False, status=status)
# Prepare our keyword arguments (to be passed into an AppriseAsset
# object)
@ -1117,9 +1304,13 @@ class StatelessNotifyView(View):
'NOTIFY - %s - Recursion limit reached (%d > %d)',
request.META['REMOTE_ADDR'], recursion,
settings.APPRISE_RECURSION_MAX)
return HttpResponse(
_('The recursion limit has been reached.'),
status=ResponseCode.method_not_accepted)
status = ResponseCode.method_not_accepted
msg = _('The recursion limit has been reached')
return HttpResponse(msg, status=status, content_type='text/plain') \
if not json_response else JsonResponse({
'error': msg,
}, encoder=JSONEncoder, safe=False, status=status)
# Store our recursion value for our AppriseAsset() initialization
kwargs['_recursion'] = recursion
@ -1128,9 +1319,13 @@ class StatelessNotifyView(View):
logger.warning(
'NOTIFY - %s - Invalid recursion value (%s) provided',
request.META['REMOTE_ADDR'], str(recursion))
return HttpResponse(
_('An invalid recursion value was specified.'),
status=ResponseCode.bad_request)
status = ResponseCode.bad_request
msg = _('An invalid recursion value was specified')
return HttpResponse(msg, status=status, content_type='text/plain') \
if not json_response else JsonResponse({
'error': msg,
}, encoder=JSONEncoder, safe=False, status=status)
# Acquire our unique identifier (if defined)
uid = request.headers.get('X-Apprise-ID', '').strip()
@ -1151,10 +1346,16 @@ class StatelessNotifyView(View):
# Add URLs
a_obj.add(content.get('urls'))
if not len(a_obj):
return HttpResponse(
_('There was no services to notify.'),
status=ResponseCode.no_content,
)
logger.warning(
'NOTIFY - %s - No valid URLs provided',
request.META['REMOTE_ADDR'])
status = ResponseCode.no_content
msg = _('There was no valid URLs provided to notify')
return HttpResponse(msg, status=status, content_type='text/plain') \
if not json_response else JsonResponse({
'error': msg,
}, encoder=JSONEncoder, safe=False, status=status)
# Handle Attachments
attach = None
@ -1183,9 +1384,12 @@ class StatelessNotifyView(View):
'NOTIFY - %s - Bad attachment: %s',
request.META['REMOTE_ADDR'], str(e))
return HttpResponse(
_('Bad attachment'),
status=ResponseCode.bad_request)
status = ResponseCode.bad_request
msg = _('Bad Attachment')
return HttpResponse(msg, status=status, content_type='text/plain') \
if not json_response else JsonResponse({
'error': msg,
}, encoder=JSONEncoder, safe=False, status=status)
# Acquire our log level from headers if defined, otherwise use
# the global one set in the settings
@ -1216,7 +1420,8 @@ class StatelessNotifyView(View):
level = logging.DEBUG - 1
if settings.APPRISE_WEBHOOK_URL:
fmt = settings.LOGGING['formatters']['standard']['format']
esc = '<!!-!ESC!-!!>'
fmt = f'["%(levelname)s","%(asctime)s","{esc}%(message)s{esc}"]'
with apprise.LogCapture(level=level, fmt=fmt) as logs:
# Perform our notification at this point
result = a_obj.notify(
@ -1227,7 +1432,15 @@ class StatelessNotifyView(View):
attach=attach,
)
response = logs.getvalue()
esc = re.escape(esc)
entries = re.findall(
r'\r*\n?(?P<head>\["[^"]+","[^"]+",)"{}(?P<to_escape>.+?)'
r'{}"(?P<tail>\]\r*\n?$)(?=$|\r*\n?\["[^"]+","[^"]+","{})'.format(
esc, esc, esc), logs.getvalue(), re.DOTALL | re.MULTILINE)
# Prepare ourselves a JSON Response
response = json.loads('[{}]'.format(
','.join([e[0] + json.dumps(e[1].rstrip()) + e[2] for e in entries])))
webhook_payload = {
'source': request.META['REMOTE_ADDR'],
@ -1251,19 +1464,27 @@ class StatelessNotifyView(View):
if not result:
# If at least one notification couldn't be sent; change up the
# response to a 424 error code
return HttpResponse(
_('One or more notification could not be sent.'),
status=ResponseCode.failed_dependency)
logger.warning(
'NOTIFY - %s - One or more stateless notification(s) could not be actioned',
request.META['REMOTE_ADDR'])
status = ResponseCode.failed_dependency
msg = _('One or more notification could not be sent')
return HttpResponse(msg, status=status, content_type='text/plain') \
if not json_response else JsonResponse({
'error': msg,
}, encoder=JSONEncoder, safe=False, status=status)
logger.info(
'NOTIFY - %s - Delivered Stateless Notification(s)',
request.META['REMOTE_ADDR'])
# Return our retrieved content
return HttpResponse(
_('Notification(s) sent.'),
status=ResponseCode.okay,
)
# Return our success message
status = ResponseCode.okay
msg = _('Notification(s) sent')
return HttpResponse(msg, status=status, content_type='text/plain') \
if not json_response else JsonResponse({
}, encoder=JSONEncoder, safe=False, status=status)
@method_decorator((gzip_page, never_cache), name='dispatch')
@ -1323,7 +1544,7 @@ class JsonUrlView(View):
)
# Something went very wrong; return 500
response['error'] = _('There was no configuration found.')
response['error'] = _('There was no configuration found')
return JsonResponse(
response,
encoder=JSONEncoder,