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
+ )