diff --git a/README.md b/README.md index 369adfb..426555b 100644 --- a/README.md +++ b/README.md @@ -164,7 +164,7 @@ The use of environment variables allow you to provide over-rides to default sett | `SECRET_KEY` | A Django variable acting as a *salt* for most things that require security. This API uses it for the hash sequences when writing the configuration files to disk (`hash` mode only). | `ALLOWED_HOSTS` | A list of strings representing the host/domain names that this API can serve. This is a security measure to prevent HTTP Host header attacks, which are possible even under many seemingly-safe web server configurations. By default this is set to `*` allowing any host. Use space to delimit more than one host. | `BASE_URL` | Those who are hosting the API behind a proxy that requires a subpath to gain access to this API should specify this path here as well. By default this is not set at all. -| `LOG_LEVEL` | Adjust the log level to the console; the default value is `DEBUG` if the `DEBUG` is set below otherwise the default is `INFO`. Possible values are `ERROR`, `WARNING`, `INFO`, and `DEBUG`. +| `LOG_LEVEL` | Adjust the log level to the console. Possible values are `CRITICAL`, `ERROR`, `WARNING`, `INFO`, and `DEBUG`. | `DEBUG` | This defaults to `False` however can be set to `True` if defined with a non-zero value (such as `1`). diff --git a/apprise_api/api/templates/config.html b/apprise_api/api/templates/config.html index dac0da2..6175803 100644 --- a/apprise_api/api/templates/config.html +++ b/apprise_api/api/templates/config.html @@ -267,11 +267,18 @@ document.querySelector('#addconfig').onsubmit = function(event) { '{% trans "Successfully saved the specified URL(s)." %}', 'success' ); + } else if(response.status > 500) { + // Disk issue + Swal.fire( + '{% trans "Save" %}', + '{% trans "There was an issue writing the configuration to your filesystem. Check your file permissions and try again." %}', + 'error' + ); } else { // user notification Swal.fire( '{% trans "Save" %}', - '{% trans "Failed to save the specified URL(s)." %}', + '{% trans "Failed to save the specified URL(s). Check your syntax and try again." %}', 'error' ); } @@ -338,23 +345,36 @@ document.querySelector('#donotify').onsubmit = function(event) { let response = fetch('{% url "notify" key %}', { method: 'POST', body: body, + headers: { + 'Accept': 'text/html', + 'X-Apprise-Log-Level': 'info' + } }).then(function(response) { - if(response.status == 200) - { - // user notification - Swal.fire( - '{% trans "Notification" %}', - '{% trans "Successfully sent the notification(s)." %}', - 'success' - ); - } else { - // user notification - Swal.fire( - '{% trans "Notification" %}', - '{% trans "Failed to send the notification(s)." %}', - 'error' - ); - } + response.text().then(function (html) { + if(response.status == 200) + { + // user notification + Swal.fire( + '{% trans "Notification" %}', + '{% trans "Successfully sent the notification(s)." %}' + html, + 'success' + ); + } else if(response.status == 424) { + // user notification + Swal.fire( + '{% trans "Notification" %}', + '{% trans "One or more of the notification(s) were not sent." %}' + html, + 'warning' + ); + } else { + // user notification + Swal.fire( + '{% trans "Notification" %}', + '{% trans "Failed to send the notification(s)." %}' + html, + 'error' + ); + } + }); }); return false; } diff --git a/apprise_api/api/tests/test_notify.py b/apprise_api/api/tests/test_notify.py index 51bc2b2..3af0f2b 100644 --- a/apprise_api/api/tests/test_notify.py +++ b/apprise_api/api/tests/test_notify.py @@ -241,7 +241,7 @@ class NotifyTests(SimpleTestCase): 'format': 'invalid' } - # Test referencing a key that doesn't exist + # Test case with format set to invalid response = self.client.post( '/notify/{}'.format(key), data=json.dumps(json_data), @@ -261,7 +261,7 @@ class NotifyTests(SimpleTestCase): 'format': None, } - # Test referencing a key that doesn't exist + # Test case with format changed response = self.client.post( '/notify/{}'.format(key), data=json.dumps(json_data), @@ -284,3 +284,61 @@ class NotifyTests(SimpleTestCase): assert response.status_code == 200 assert mock_notify.call_count == 1 + + # Reset our count + mock_notify.reset_mock() + + headers = { + 'HTTP_X_APPRISE_LOG_LEVEL': 'debug', + 'HTTP_ACCEPT': 'text/plain', + } + + # 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'] == 'text/plain' + + headers = { + 'HTTP_X_APPRISE_LOG_LEVEL': 'debug', + 'HTTP_ACCEPT': 'text/html', + } + + mock_notify.reset_mock() + + # 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'] == 'text/html' + + headers = { + 'HTTP_X_APPRISE_LOG_LEVEL': 'invalid', + 'HTTP_ACCEPT': 'text/*', + } + + mock_notify.reset_mock() + + # 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'] == 'text/html' diff --git a/apprise_api/api/views.py b/apprise_api/api/views.py index f0f89ef..dfbf51a 100644 --- a/apprise_api/api/views.py +++ b/apprise_api/api/views.py @@ -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.utils.html import escape from django.utils.decorators import method_decorator from django.views.decorators.cache import never_cache from django.views.decorators.gzip import gzip_page @@ -422,25 +423,88 @@ class NotifyView(View): # Add our configuration a_obj.add(ac_obj) - # Perform our notification at this point - result = a_obj.notify( - content.get('body'), - title=content.get('title', ''), - notify_type=content.get('type', apprise.NotifyType.INFO), - tag=content.get('tag'), - ) + # Our return content type can be controlled by the Accept keyword + # If it includes /* or /html somewhere then we return html, otherwise + # 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' + + # Acquire our log level from headers if defined, otherwise use + # the global one set in the settings + level = request.headers.get( + 'X-Apprise-Log-Level', + settings.LOGGING['loggers']['apprise']['level']).upper() + + # Initialize our response object + response = None + + if level in ('CRITICAL', 'ERROR' 'WARNING', 'INFO', 'DEBUG'): + level = getattr(apprise.logging, level) + + esc = '' + fmt = '
  • ' \ + '
    %(asctime)s
    ' \ + '
    %(levelname)s
    ' \ + f'
    {esc}%(message)s{esc}
  • ' \ + 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: + # Perform our notification at this point + result = a_obj.notify( + content.get('body'), + title=content.get('title', ''), + notify_type=content.get('type', apprise.NotifyType.INFO), + tag=content.get('tag'), + ) + + if 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
  • .*?)' + r'{}(?P.+li>$)(?=$|
  • ` tag and escape our message body: + response = ''.format( + ''.join([e[0] + escape(e[1]) + e[2] for e in entries])) + + else: # content_type == 'text/plain' + response = logs.getvalue() + + else: + # Perform our notification at this point without logging + result = a_obj.notify( + content.get('body'), + title=content.get('title', ''), + notify_type=content.get('type', apprise.NotifyType.INFO), + tag=content.get('tag'), + ) if not result: # If at least one notification couldn't be sent; change up # the response to a 424 error code return HttpResponse( + response if response is not None else _('One or more notification could not be sent.'), + content_type=content_type, status=ResponseCode.failed_dependency) # Return our retrieved content return HttpResponse( + response if response is not None else _('Notification(s) sent.'), - status=ResponseCode.okay + content_type=content_type, + status=ResponseCode.okay, ) diff --git a/apprise_api/core/settings/__init__.py b/apprise_api/core/settings/__init__.py index 7715d76..d0bbfe8 100644 --- a/apprise_api/core/settings/__init__.py +++ b/apprise_api/core/settings/__init__.py @@ -41,7 +41,10 @@ SECRET_KEY = os.environ.get( # # export DJANGO_SETTINGS_MODULE=core.settings.debug # ./manage.py runserver -DEBUG = bool(os.environ.get("DEBUG", False)) +# +# Support 'yes', '1', 'true', 'enable', 'active', and + +DEBUG = os.environ.get("DEBUG", 'No')[0].lower() in ( + 'a', 'y', '1', 't', 'e', '+') # allow all hosts by default otherwise read from the # ALLOWED_HOSTS environment variable diff --git a/apprise_api/static/css/base.css b/apprise_api/static/css/base.css index ef7a167..fc86277 100644 --- a/apprise_api/static/css/base.css +++ b/apprise_api/static/css/base.css @@ -74,4 +74,48 @@ textarea { #url-list .chip { background-color: inherit; border: 1px solid #e4e4e4; -} \ No newline at end of file +} + +/* Notification Details */ +ul.logs { + font-family: monospace, monospace; + height: 60%; + overflow: auto; +} + +ul.logs li { + display: flex; + text-align: left; + width: 50em; +} + +ul.logs li div.log_time { + font-weight: normal; + flex: 0 15em; +} + +ul.logs li div.log_level { + font-weight: bold; + align: right; + flex: 0 5em; +} + +ul.logs li div.log_msg { + flex: 1; +} + +ul.logs li.log_INFO { + color: black; +} + +ul.logs li.log_DEBUG { + color: #606060; +} + +ul.logs li.log_WARNING { + color: orange; +} + +ul.logs li.log_ERROR { + color: #8B0000; +} diff --git a/requirements.txt b/requirements.txt index d3cee8d..1bad073 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,2 @@ django -apprise >= 0.8.8 +apprise >= 0.9.0