From 4b9c072d6b497adbc5c333f21119c1adc188e653 Mon Sep 17 00:00:00 2001 From: Chris Caron Date: Sun, 5 Jan 2020 18:19:09 -0500 Subject: [PATCH] Support a /json/urls/{key} query to provide loaded details (#4) --- .coveragerc | 2 + README.md | 43 +++-- apprise_api/api/tests/test_get.py | 5 - apprise_api/api/tests/test_json_urls.py | 148 ++++++++++++++++++ apprise_api/api/urls.py | 3 + apprise_api/api/views.py | 200 ++++++++++++++++++------ 6 files changed, 333 insertions(+), 68 deletions(-) create mode 100644 apprise_api/api/tests/test_json_urls.py diff --git a/.coveragerc b/.coveragerc index 4e436a7..618121f 100644 --- a/.coveragerc +++ b/.coveragerc @@ -4,6 +4,8 @@ omit = *migrations/*, *settings*, *tests/*, + lib/*, + lib64/*, *urls.py, *core/wsgi.py, gunicorn.conf.py, diff --git a/README.md b/README.md index ad61974..e073812 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ Apprise API was designed to easily fit into existing (and new) eco-systems that [![Docker Pulls](https://img.shields.io/docker/pulls/caronc/apprise.svg?style=flat-square)](https://hub.docker.com/r/caronc/apprise) ## Screenshots -There is a small built-in *Configuration Manager* that can be accesed using `/cfg/{KEY}`:
+There is a small built-in *Configuration Manager* that can be optionally accessed through your web browser accessible via `/cfg/{KEY}`:
![Screenshot of GUI - Using Keys](https://raw.githubusercontent.com/caronc/apprise-api/master/Screenshot-1.png)
Below is a screenshot of how you can set either a series of URL's to your `{KEY}`, or set your YAML and/or TEXT configuration below. @@ -47,18 +47,39 @@ docker-compose up ## API Details -| Path | Description | -|------------- | ----------- | -| `/add/{KEY}` | Saves Apprise Configuration (or set of URLs) to the persistent store.
*Parameters*
:small_red_triangle: **urls**: Used to define one or more Apprise URL(s). Use a comma and/or space to separate one URL from the next.
:small_red_triangle: **config**: Provide the contents of either a YAML or TEXT based Apprise configuration.
:small_red_triangle: **format**: This field is only required if you've specified the _config_ parameter. Used to tell the server which of the supported (Apprise) configuration types you are passing. Valid options are _text_ and _yaml_. -| `/del/{KEY}` | Removes Apprise Configuration from the persistent store. -| `/get/{KEY}` | Returns the Apprise Configuration from the persistent store. This can be directly used with the *Apprise CLI* and/or the *AppriseConfig()* object ([see here for details](https://github.com/caronc/apprise/wiki/config)). -| `/notify/{KEY}` | Sends a notification based on the Apprise Configuration associated with the specified *{KEY}*.
*Parameters*
:small_red_triangle: **body**: Your message body. This is the *only* required field.
:small_red_triangle: **title**: Optionally define a title to go along with the *body*.
:small_red_triangle: **type**: Defines the message type you want to send as. The valid options are `info`, `success`, `warning`, and `error`. If no *type* is specified then `info` is the default value used.
:small_red_triangle: **tag**: Optionally notify only those tagged accordingly. -| `/notify/` | Similar to the `notify` API identified above except this one sends a stateless notification and requires no reference to a `{KEY}`.
*Parameters*
:small_red_triangle: **urls**: One or more URLs identifying where the notification should be sent to. If this field isn't specified then it automatically assumes the `settings.APPRISE_STATELESS_URLS` value or `APPRISE_STATELESS_URLS` environment variable.
:small_red_triangle: **body**: Your message body. This is a required field.
:small_red_triangle: **title**: Optionally define a title to go along with the *body*.
:small_red_triangle: **type**: Defines the message type you want to send as. The valid options are `info`, `success`, `warning`, and `error`. If no *type* is specified then `info` is the default value used. +| Path | Method | Description | +|------------- | ------ | ----------- | +| `/add/{KEY}` | POST | Saves Apprise Configuration (or set of URLs) to the persistent store.
*Parameters*
:small_red_triangle: **urls**: Used to define one or more Apprise URL(s). Use a comma and/or space to separate one URL from the next.
:small_red_triangle: **config**: Provide the contents of either a YAML or TEXT based Apprise configuration.
:small_red_triangle: **format**: This field is only required if you've specified the _config_ parameter. Used to tell the server which of the supported (Apprise) configuration types you are passing. Valid options are _text_ and _yaml_. +| `/del/{KEY}` | POST | Removes Apprise Configuration from the persistent store. +| `/get/{KEY}` | POST | Returns the Apprise Configuration from the persistent store. This can be directly used with the *Apprise CLI* and/or the *AppriseConfig()* object ([see here for details](https://github.com/caronc/apprise/wiki/config)). +| `/notify/{KEY}` | POST | Sends a notification based on the Apprise Configuration associated with the specified *{KEY}*.
*Parameters*
:small_red_triangle: **body**: Your message body. This is the *only* required field.
:small_red_triangle: **title**: Optionally define a title to go along with the *body*.
:small_red_triangle: **type**: Defines the message type you want to send as. The valid options are `info`, `success`, `warning`, and `error`. If no *type* is specified then `info` is the default value used.
:small_red_triangle: **tag**: Optionally notify only those tagged accordingly. +| `/notify/` | POST | Similar to the `notify` API identified above except this one sends a stateless notification and requires no reference to a `{KEY}`.
*Parameters*
:small_red_triangle: **urls**: One or more URLs identifying where the notification should be sent to. If this field isn't specified then it automatically assumes the `settings.APPRISE_STATELESS_URLS` value or `APPRISE_STATELESS_URLS` environment variable.
:small_red_triangle: **body**: Your message body. This is a required field.
:small_red_triangle: **title**: Optionally define a title to go along with the *body*.
:small_red_triangle: **type**: Defines the message type you want to send as. The valid options are `info`, `success`, `warning`, and `error`. If no *type* is specified then `info` is the default value used. +| `/json/urls/{KEY}` | GET | Returns a JSON response object that contains all of the URLS and Tags associated with the key specified. + +The `/json/urls/{KEY}` response might look like this: +```json +{ + "tags": ["devops", "admin", "me"], + "urls": [ + { + "url": "slack://TokenA/TokenB/TokenC", + "tags": ["devops", "admin"] + }, + { + "url": "discord://WebhookID/WebhookToken", + "tags": ["devops"] + }, + { + "url": "mailto://user:pass@gmail.com", + "tags": ["me"] + } + ] +} +``` ### API Notes - `{KEY}` must be 1-64 alphanumeric characters in length. In addition to this, the underscore (`_`) and dash (`-`) are also accepted. -- You must `POST` to URLs defined above in order for them to respond. - Specify the `Content-Type` of `application/json` to use the JSON support. - There is no authentication (or SSL encryption) required to use this API; this is by design. The intentio here to be a lightweight and fast micro-service that can be parked behind another tier that was designed to handle security. - There are no additional dependencies should you choose to use the optional persistent store (mounted as `/config`). @@ -69,8 +90,8 @@ The use of environment variables allow you to provide over-rides to default sett | Variable | Description | |--------------------- | ----------- | -| `APPRISE_CONFIG_DIR` | Defines the persistent store location of all configuration files saved. By default:
- Configuration is written to the `apprise_api/var/config` directory when just using the _Django_ `manage runserver` script. -| `APPRISE_STATELESS_URLS` | A default set of URLs to notify when using the stateless `/notify` reference (no reference to `{KEY}` variables). Use this option if you don't intend to use the persistent store at all. +| `APPRISE_CONFIG_DIR` | Defines an (optional) persistent store location of all configuration files saved. By default:
- Configuration is written to the `apprise_api/var/config` directory when just using the _Django_ `manage runserver` script. However for the path for containers is `/config` +| `APPRISE_STATELESS_URLS` | For a non-persistent solution, you can take avantage of this global variable. Use this to define a default set of Apprise URLs to notify when using API calls to `/notify`. If no `{KEY}` is defined when calling `/notify` then the URLs defined here are used instead. | `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. | `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 then one host. | `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/tests/test_get.py b/apprise_api/api/tests/test_get.py index c7b431a..dbc4b50 100644 --- a/apprise_api/api/tests/test_get.py +++ b/apprise_api/api/tests/test_get.py @@ -77,11 +77,6 @@ class GetTests(SimpleTestCase): - dbus://"""}) assert response.status_code == 200 - # Verify that the correct Content-Type is set in the header of the - # response - assert 'Content-Type' in response - assert response['Content-Type'].startswith('text/plain') - # Now retrieve our YAML configuration response = self.client.post('/get/{}'.format(key)) assert response.status_code == 200 diff --git a/apprise_api/api/tests/test_json_urls.py b/apprise_api/api/tests/test_json_urls.py new file mode 100644 index 0000000..9ce545f --- /dev/null +++ b/apprise_api/api/tests/test_json_urls.py @@ -0,0 +1,148 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2019 Chris Caron +# All rights reserved. +# +# This code is licensed under the MIT License. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files(the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and / or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions : +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +from django.test import SimpleTestCase +from unittest.mock import patch + + +class JsonUrlsTests(SimpleTestCase): + + def test_get_invalid_key_status_code(self): + """ + Test GET requests to invalid key + """ + response = self.client.get('/get/**invalid-key**') + assert response.status_code == 404 + + def test_post_not_supported(self): + """ + Test POST requests with key + """ + response = self.client.post('/json/urls/test') + # 405 as posting is not allowed + assert response.status_code == 405 + + def test_json_urls_config(self): + """ + Test retrieving configuration + """ + + # our key to use + key = 'test_json_urls_config' + + # Nothing to return + response = self.client.get('/json/urls/{}'.format(key)) + assert response.status_code == 204 + + # Add some content + response = self.client.post( + '/add/{}'.format(key), + {'urls': 'mailto://user:pass@yahoo.ca'}) + assert response.status_code == 200 + + # Handle case when we try to retrieve our content but we have no idea + # what the format is in. Essentialy there had to have been disk + # corruption here or someone meddling with the backend. + with patch('gzip.open', side_effect=OSError): + response = self.client.get('/json/urls/{}'.format(key)) + assert response.status_code == 500 + assert response['Content-Type'].startswith('application/json') + assert 'tags' in response.json() + assert 'urls' in response.json() + + # has error directive + assert 'error' in response.json() + + # entries exist by are empty + assert len(response.json()['tags']) == 0 + assert len(response.json()['urls']) == 0 + + # Now we should be able to see our content + response = self.client.get('/json/urls/{}'.format(key)) + assert response.status_code == 200 + assert response['Content-Type'].startswith('application/json') + assert 'tags' in response.json() + assert 'urls' in response.json() + + # No errors occured, therefore no error entry + assert 'error' not in response.json() + + # No tags (but can be assumed "all") is always present + assert len(response.json()['tags']) == 0 + + # One URL loaded + assert len(response.json()['urls']) == 1 + assert 'url' in response.json()['urls'][0] + assert 'tags' in response.json()['urls'][0] + assert len(response.json()['urls'][0]['tags']) == 0 + + # Add a YAML file + response = self.client.post( + '/add/{}'.format(key), { + 'format': 'yaml', + 'config': """ + urls: + - dbus://: + - tag: tag1, tag2"""}) + assert response.status_code == 200 + + # Now retrieve our JSON resonse + response = self.client.get('/json/urls/{}'.format(key)) + assert response.status_code == 200 + + # No errors occured, therefore no error entry + assert 'error' not in response.json() + + # No tags (but can be assumed "all") is always present + assert len(response.json()['tags']) == 2 + + # One URL loaded + assert len(response.json()['urls']) == 1 + assert 'url' in response.json()['urls'][0] + assert 'tags' in response.json()['urls'][0] + assert len(response.json()['urls'][0]['tags']) == 2 + + # Handle case when we try to retrieve our content but we have no idea + # what the format is in. Essentialy there had to have been disk + # corruption here or someone meddling with the backend. + with patch('tempfile._TemporaryFileWrapper') as mock_ntf: + mock_ntf.side_effect = OSError() + # Now retrieve our JSON resonse + response = self.client.get('/json/urls/{}'.format(key)) + assert response.status_code == 500 + assert response['Content-Type'].startswith('application/json') + assert 'tags' in response.json() + assert 'urls' in response.json() + + # has error directive + assert 'error' in response.json() + + # entries exist by are empty + assert len(response.json()['tags']) == 0 + assert len(response.json()['urls']) == 0 + + # Verify that the correct Content-Type is set in the header of the + # response + assert 'Content-Type' in response + assert response['Content-Type'].startswith('application/json') diff --git a/apprise_api/api/urls.py b/apprise_api/api/urls.py index 75e44b6..a968bbb 100644 --- a/apprise_api/api/urls.py +++ b/apprise_api/api/urls.py @@ -47,4 +47,7 @@ urlpatterns = [ re_path( r'^notify/?', views.StatelessNotifyView.as_view(), name='s_notify'), + re_path( + r'^json/urls/(?P[\w_-]{1,64})/?', + views.JsonUrlView.as_view(), name='json_urls'), ] diff --git a/apprise_api/api/views.py b/apprise_api/api/views.py index b9f4249..7415b6a 100644 --- a/apprise_api/api/views.py +++ b/apprise_api/api/views.py @@ -24,12 +24,15 @@ # THE SOFTWARE. from django.shortcuts import render from django.http import HttpResponse +from django.http import JsonResponse from django.views import View from django.conf import settings from django.utils.decorators import method_decorator from django.views.decorators.cache import never_cache from django.views.decorators.gzip import gzip_page from django.utils.translation import gettext_lazy as _ +from django.core.serializers.json import DjangoJSONEncoder + from .utils import ConfigCache from .forms import AddByUrlForm from .forms import AddByConfigForm @@ -57,6 +60,17 @@ MIME_IS_JSON = re.compile( r'(text|application)/(x-)?json', re.I) +class JSONEncoder(DjangoJSONEncoder): + """ + A wrapper to the DjangoJSONEncoder to support + sets() (converting them to lists). + """ + def default(self, obj): + if isinstance(obj, set): + return list(obj) + return super().default(obj) + + class ResponseCode(object): """ These codes are based on those provided by the requests object @@ -108,9 +122,6 @@ class AddView(View): """ Handle a POST request """ - # Our default response type - content_type = 'text/plain; charset=utf-8' - # our content content = {} if MIME_IS_FORM.match(request.content_type): @@ -133,13 +144,11 @@ class AddView(View): # could not parse JSON response... return HttpResponse( _('Invalid JSON specified.'), - content_type=content_type, status=ResponseCode.bad_request) if not content: return HttpResponse( _('The message format is not supported.'), - content_type=content_type, status=ResponseCode.bad_request) # Create ourselves an apprise object to work with @@ -151,8 +160,8 @@ class AddView(View): # No URLs were loaded return HttpResponse( _('No valid URLs were found.'), - content_type=content_type, - status=ResponseCode.bad_request) + status=ResponseCode.bad_request, + ) if not ConfigCache.put( key, '\r\n'.join([s.url() for s in a_obj]), @@ -160,7 +169,6 @@ class AddView(View): return HttpResponse( _('The configuration could not be saved.'), - content_type=content_type, status=ResponseCode.internal_server_error, ) @@ -170,8 +178,8 @@ class AddView(View): # Format must be one supported by apprise return HttpResponse( _('The format specified is invalid.'), - content_type=content_type, - status=ResponseCode.bad_request) + status=ResponseCode.bad_request, + ) # prepare our apprise config object ac_obj = apprise.AppriseConfig() @@ -185,11 +193,12 @@ class AddView(View): if not ac_obj.add( 'file://{}?format={}'.format(f.name, fmt)): + # Bad Configuration return HttpResponse( _('The configuration specified is invalid.'), - content_type=content_type, - status=ResponseCode.bad_request) + status=ResponseCode.bad_request, + ) # Add our configuration a_obj.add(ac_obj) @@ -199,34 +208,35 @@ class AddView(View): # mis-configuration on the caller's part return HttpResponse( _('No valid URL(s) were specified.'), - content_type=content_type, - status=ResponseCode.bad_request) + status=ResponseCode.bad_request, + ) except OSError: # We could not write the temporary file to disk return HttpResponse( _('The configuration could not be loaded.'), - content_type=content_type, - status=ResponseCode.internal_server_error) + status=ResponseCode.internal_server_error, + ) if not ConfigCache.put(key, content['config'], fmt=fmt): # Something went very wrong; return 500 return HttpResponse( _('An error occured saving configuration.'), - content_type=content_type, status=ResponseCode.internal_server_error, ) else: # No configuration specified; we're done return HttpResponse( _('No configuration specified.'), - content_type=content_type, status=ResponseCode.bad_request) + status=ResponseCode.bad_request, + ) # 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.'), - content_type=content_type, status=ResponseCode.okay) + status=ResponseCode.okay, + ) @method_decorator(never_cache, name='dispatch') @@ -238,15 +248,11 @@ class DelView(View): """ Handle a POST request """ - # Our default response type - content_type = 'text/plain; charset=utf-8' - # Clear the key result = ConfigCache.clear(key) if result is None: return HttpResponse( _('There was no configuration to remove.'), - content_type=content_type, status=ResponseCode.no_content, ) @@ -254,14 +260,12 @@ class DelView(View): # There was a failure at the os level return HttpResponse( _('The configuration could not be removed.'), - content_type=content_type, status=ResponseCode.internal_server_error, ) # Removed content return HttpResponse( _('Successfully removed configuration.'), - content_type=content_type, status=ResponseCode.okay, ) @@ -275,8 +279,6 @@ class GetView(View): """ Handle a POST request """ - # Our default response type - content_type = 'text/plain; charset=utf-8' config, format = ConfigCache.get(key) if config is None: @@ -290,28 +292,29 @@ class GetView(View): # no content to return return HttpResponse( _('There was no configuration found.'), - content_type=content_type, status=ResponseCode.no_content, ) # Something went very wrong; return 500 return HttpResponse( _('An error occured accessing configuration.'), - content_type=content_type, status=ResponseCode.internal_server_error, ) # Our configuration was retrieved; now our response varies on whether # we are a YAML configuration or a TEXT based one. This allows us to # be compatible with those using the AppriseConfig() library or the - # reference to it through the --config (-c) option in the CLI - if format == apprise.ConfigFormat.YAML: - # update our return content type from the default text - content_type = 'text/yaml; charset=utf-8' + # 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' # Return our retrieved content return HttpResponse( - config, content_type=content_type, status=ResponseCode.okay) + config, + content_type=content_type, + status=ResponseCode.okay, + ) @method_decorator((gzip_page, never_cache), name='dispatch') @@ -323,9 +326,6 @@ class NotifyView(View): """ Handle a POST request """ - # Our default response type - content_type = 'text/plain; charset=utf-8' - # our content content = {} if MIME_IS_FORM.match(request.content_type): @@ -344,14 +344,12 @@ class NotifyView(View): # could not parse JSON response... return HttpResponse( _('Invalid JSON specified.'), - content_type=content_type, status=ResponseCode.bad_request) if not content: # We could not handle the Content-Type return HttpResponse( _('The message format is not supported.'), - content_type=content_type, status=ResponseCode.bad_request) # Some basic error checking @@ -361,7 +359,6 @@ class NotifyView(View): return HttpResponse( _('An invalid payload was specified.'), - content_type=content_type, status=ResponseCode.bad_request) # If we get here, we have enough information to generate a notification @@ -378,14 +375,12 @@ class NotifyView(View): # no content to return return HttpResponse( _('There was no configuration found.'), - content_type=content_type, status=ResponseCode.no_content, ) # Something went very wrong; return 500 return HttpResponse( _('An error occured accessing configuration.'), - content_type=content_type, status=ResponseCode.internal_server_error, ) @@ -423,13 +418,13 @@ class NotifyView(View): # We could not write the temporary file to disk return HttpResponse( _('The configuration could not be loaded.'), - content_type=content_type, status=ResponseCode.internal_server_error) # Return our retrieved content return HttpResponse( _('Notification(s) sent.'), - content_type=content_type, status=ResponseCode.okay) + status=ResponseCode.okay + ) @method_decorator((gzip_page, never_cache), name='dispatch') @@ -441,9 +436,6 @@ class StatelessNotifyView(View): """ Handle a POST request """ - # Our default response type - content_type = 'text/plain; charset=utf-8' - # our content content = {} if MIME_IS_FORM.match(request.content_type): @@ -462,14 +454,12 @@ class StatelessNotifyView(View): # could not parse JSON response... return HttpResponse( _('Invalid JSON specified.'), - content_type=content_type, status=ResponseCode.bad_request) if not content: # We could not handle the Content-Type return HttpResponse( _('The message format is not supported.'), - content_type=content_type, status=ResponseCode.bad_request) if not content.get('urls') and settings.APPRISE_STATELESS_URLS: @@ -484,7 +474,6 @@ class StatelessNotifyView(View): return HttpResponse( _('An invalid payload was specified.'), - content_type=content_type, status=ResponseCode.bad_request) # Prepare our apprise object @@ -495,7 +484,6 @@ class StatelessNotifyView(View): if not len(a_obj): return HttpResponse( _('There was no services to notify.'), - content_type=content_type, status=ResponseCode.no_content, ) @@ -510,4 +498,112 @@ class StatelessNotifyView(View): # Return our retrieved content return HttpResponse( _('Notification(s) sent.'), - content_type=content_type, status=ResponseCode.okay) + status=ResponseCode.okay, + ) + + +@method_decorator((gzip_page, never_cache), name='dispatch') +class JsonUrlView(View): + """ + A Django view that lists all loaded tags and URLs for a given key + """ + def get(self, request, key): + """ + Handle a POST request + """ + + # Now build our tag response that identifies all of the tags + # and the URL's they're associated with + # { + # "tags": ["tag1', "tag2", "tag3"], + # "urls": [ + # { + # "url": "windows://", + # "tags": [], + # }, + # { + # "url": "mailto://user:pass@gmail.com" + # "tags": ["tag1", "tag2", "tag3"] + # } + # ] + # } + response = { + 'tags': set(), + 'urls': [], + } + + config, format = ConfigCache.get(key) + if config is None: + # The returned value of config and format tell a rather cryptic + # story; this portion could probably be updated in the future. + # but for now it reads like this: + # config == None and format == None: We had an internal error + # config == None and format != None: we simply have no data + # config != None: we simply have no data + if format is not None: + # no content to return + return JsonResponse( + response, + encoder=JSONEncoder, + safe=False, + status=ResponseCode.no_content, + ) + + # Something went very wrong; return 500 + response['error'] = _('There was no configuration found.') + return JsonResponse( + response, + encoder=JSONEncoder, + safe=False, + status=ResponseCode.internal_server_error, + ) + + # Prepare our apprise object + a_obj = apprise.Apprise() + + # Create an apprise config object + ac_obj = apprise.AppriseConfig() + + try: + # Write our file to a temporary file containing our configuration + # so that we can read it back. In the future a change will be to + # Apprise so that we can just directly write the configuration as + # is to the AppriseConfig() object... but for now... + with NamedTemporaryFile() as f: + # Write our content to disk + f.write(config.encode()) + f.flush() + + # Read our configuration back in to our configuration + ac_obj.add('file://{}?format={}'.format(f.name, format)) + + # Add our configuration + a_obj.add(ac_obj) + + for notification in a_obj: + # Set Notification + response['urls'].append({ + 'url': notification.url(privacy=False), + 'tags': notification.tags, + }) + + # Store Tags + response['tags'] |= notification.tags + + except OSError: + # We could not write the temporary file to disk + response['error'] = _('The configuration could not be loaded.'), + return JsonResponse( + response, + encoder=JSONEncoder, + safe=False, + status=ResponseCode.internal_server_error, + ) + + # Return our retrieved content + return JsonResponse( + response, + encoder=JSONEncoder, + safe=False, + status=ResponseCode.okay + )