Support a /json/urls/{key} query to provide loaded details (#4)

This commit is contained in:
Chris Caron 2020-01-05 18:19:09 -05:00 committed by GitHub
parent d571a8e186
commit 4b9c072d6b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 333 additions and 68 deletions

View File

@ -4,6 +4,8 @@ omit =
*migrations/*, *migrations/*,
*settings*, *settings*,
*tests/*, *tests/*,
lib/*,
lib64/*,
*urls.py, *urls.py,
*core/wsgi.py, *core/wsgi.py,
gunicorn.conf.py, gunicorn.conf.py,

View File

@ -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) [![Docker Pulls](https://img.shields.io/docker/pulls/caronc/apprise.svg?style=flat-square)](https://hub.docker.com/r/caronc/apprise)
## Screenshots ## Screenshots
There is a small built-in *Configuration Manager* that can be accesed using `/cfg/{KEY}`:<br/> There is a small built-in *Configuration Manager* that can be optionally accessed through your web browser accessible via `/cfg/{KEY}`:<br/>
![Screenshot of GUI - Using Keys](https://raw.githubusercontent.com/caronc/apprise-api/master/Screenshot-1.png)<br/> ![Screenshot of GUI - Using Keys](https://raw.githubusercontent.com/caronc/apprise-api/master/Screenshot-1.png)<br/>
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. 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 ## API Details
| Path | Description | | Path | Method | Description |
|------------- | ----------- | |------------- | ------ | ----------- |
| `/add/{KEY}` | Saves Apprise Configuration (or set of URLs) to the persistent store.<br/>*Parameters*<br/>: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.<br/>:small_red_triangle: **config**: Provide the contents of either a YAML or TEXT based Apprise configuration.<br/>: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_. | `/add/{KEY}` | POST | Saves Apprise Configuration (or set of URLs) to the persistent store.<br/>*Parameters*<br/>: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.<br/>:small_red_triangle: **config**: Provide the contents of either a YAML or TEXT based Apprise configuration.<br/>: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. | `/del/{KEY}` | POST | 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)). | `/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}` | Sends a notification based on the Apprise Configuration associated with the specified *{KEY}*.<br/>*Parameters*<br/>:small_red_triangle: **body**: Your message body. This is the *only* required field.<br/>:small_red_triangle: **title**: Optionally define a title to go along with the *body*.<br/>: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.<br/>:small_red_triangle: **tag**: Optionally notify only those tagged accordingly. | `/notify/{KEY}` | POST | Sends a notification based on the Apprise Configuration associated with the specified *{KEY}*.<br/>*Parameters*<br/>:small_red_triangle: **body**: Your message body. This is the *only* required field.<br/>:small_red_triangle: **title**: Optionally define a title to go along with the *body*.<br/>: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.<br/>: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}`. <br/>*Parameters*<br/>: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.<br/>:small_red_triangle: **body**: Your message body. This is a required field.<br/>:small_red_triangle: **title**: Optionally define a title to go along with the *body*.<br/>: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. | `/notify/` | POST | Similar to the `notify` API identified above except this one sends a stateless notification and requires no reference to a `{KEY}`. <br/>*Parameters*<br/>: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.<br/>:small_red_triangle: **body**: Your message body. This is a required field.<br/>:small_red_triangle: **title**: Optionally define a title to go along with the *body*.<br/>: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 ### API Notes
- `{KEY}` must be 1-64 alphanumeric characters in length. In addition to this, the underscore (`_`) and dash (`-`) are also accepted. - `{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. - 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 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`). - 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 | | Variable | Description |
|--------------------- | ----------- | |--------------------- | ----------- |
| `APPRISE_CONFIG_DIR` | Defines the persistent store location of all configuration files saved. By default:<br/> - Configuration is written to the `apprise_api/var/config` directory when just using the _Django_ `manage runserver` script. | `APPRISE_CONFIG_DIR` | Defines an (optional) persistent store location of all configuration files saved. By default:<br/> - 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` | 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_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. | `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. | `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`). | `DEBUG` | This defaults to `False` however can be set to `True`if defined with a non-zero value (such as `1`).

View File

@ -77,11 +77,6 @@ class GetTests(SimpleTestCase):
- dbus://"""}) - dbus://"""})
assert response.status_code == 200 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 # Now retrieve our YAML configuration
response = self.client.post('/get/{}'.format(key)) response = self.client.post('/get/{}'.format(key))
assert response.status_code == 200 assert response.status_code == 200

View File

@ -0,0 +1,148 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2019 Chris Caron <lead2gold@gmail.com>
# 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')

View File

@ -47,4 +47,7 @@ urlpatterns = [
re_path( re_path(
r'^notify/?', r'^notify/?',
views.StatelessNotifyView.as_view(), name='s_notify'), views.StatelessNotifyView.as_view(), name='s_notify'),
re_path(
r'^json/urls/(?P<key>[\w_-]{1,64})/?',
views.JsonUrlView.as_view(), name='json_urls'),
] ]

View File

@ -24,12 +24,15 @@
# THE SOFTWARE. # THE SOFTWARE.
from django.shortcuts import render from django.shortcuts import render
from django.http import HttpResponse from django.http import HttpResponse
from django.http import JsonResponse
from django.views import View from django.views import View
from django.conf import settings from django.conf import settings
from django.utils.decorators import method_decorator from django.utils.decorators import method_decorator
from django.views.decorators.cache import never_cache from django.views.decorators.cache import never_cache
from django.views.decorators.gzip import gzip_page from django.views.decorators.gzip import gzip_page
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django.core.serializers.json import DjangoJSONEncoder
from .utils import ConfigCache from .utils import ConfigCache
from .forms import AddByUrlForm from .forms import AddByUrlForm
from .forms import AddByConfigForm from .forms import AddByConfigForm
@ -57,6 +60,17 @@ MIME_IS_JSON = re.compile(
r'(text|application)/(x-)?json', re.I) 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): class ResponseCode(object):
""" """
These codes are based on those provided by the requests object These codes are based on those provided by the requests object
@ -108,9 +122,6 @@ class AddView(View):
""" """
Handle a POST request Handle a POST request
""" """
# Our default response type
content_type = 'text/plain; charset=utf-8'
# our content # our content
content = {} content = {}
if MIME_IS_FORM.match(request.content_type): if MIME_IS_FORM.match(request.content_type):
@ -133,13 +144,11 @@ class AddView(View):
# could not parse JSON response... # could not parse JSON response...
return HttpResponse( return HttpResponse(
_('Invalid JSON specified.'), _('Invalid JSON specified.'),
content_type=content_type,
status=ResponseCode.bad_request) status=ResponseCode.bad_request)
if not content: if not content:
return HttpResponse( return HttpResponse(
_('The message format is not supported.'), _('The message format is not supported.'),
content_type=content_type,
status=ResponseCode.bad_request) status=ResponseCode.bad_request)
# Create ourselves an apprise object to work with # Create ourselves an apprise object to work with
@ -151,8 +160,8 @@ class AddView(View):
# No URLs were loaded # No URLs were loaded
return HttpResponse( return HttpResponse(
_('No valid URLs were found.'), _('No valid URLs were found.'),
content_type=content_type, status=ResponseCode.bad_request,
status=ResponseCode.bad_request) )
if not ConfigCache.put( if not ConfigCache.put(
key, '\r\n'.join([s.url() for s in a_obj]), key, '\r\n'.join([s.url() for s in a_obj]),
@ -160,7 +169,6 @@ class AddView(View):
return HttpResponse( return HttpResponse(
_('The configuration could not be saved.'), _('The configuration could not be saved.'),
content_type=content_type,
status=ResponseCode.internal_server_error, status=ResponseCode.internal_server_error,
) )
@ -170,8 +178,8 @@ class AddView(View):
# Format must be one supported by apprise # Format must be one supported by apprise
return HttpResponse( return HttpResponse(
_('The format specified is invalid.'), _('The format specified is invalid.'),
content_type=content_type, status=ResponseCode.bad_request,
status=ResponseCode.bad_request) )
# prepare our apprise config object # prepare our apprise config object
ac_obj = apprise.AppriseConfig() ac_obj = apprise.AppriseConfig()
@ -185,11 +193,12 @@ class AddView(View):
if not ac_obj.add( if not ac_obj.add(
'file://{}?format={}'.format(f.name, fmt)): 'file://{}?format={}'.format(f.name, fmt)):
# Bad Configuration # Bad Configuration
return HttpResponse( return HttpResponse(
_('The configuration specified is invalid.'), _('The configuration specified is invalid.'),
content_type=content_type, status=ResponseCode.bad_request,
status=ResponseCode.bad_request) )
# Add our configuration # Add our configuration
a_obj.add(ac_obj) a_obj.add(ac_obj)
@ -199,34 +208,35 @@ class AddView(View):
# mis-configuration on the caller's part # mis-configuration on the caller's part
return HttpResponse( return HttpResponse(
_('No valid URL(s) were specified.'), _('No valid URL(s) were specified.'),
content_type=content_type, status=ResponseCode.bad_request,
status=ResponseCode.bad_request) )
except OSError: except OSError:
# We could not write the temporary file to disk # We could not write the temporary file to disk
return HttpResponse( return HttpResponse(
_('The configuration could not be loaded.'), _('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): if not ConfigCache.put(key, content['config'], fmt=fmt):
# Something went very wrong; return 500 # Something went very wrong; return 500
return HttpResponse( return HttpResponse(
_('An error occured saving configuration.'), _('An error occured saving configuration.'),
content_type=content_type,
status=ResponseCode.internal_server_error, status=ResponseCode.internal_server_error,
) )
else: else:
# No configuration specified; we're done # No configuration specified; we're done
return HttpResponse( return HttpResponse(
_('No configuration specified.'), _('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 # 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. # go ahead and write it to disk and alert our caller of the success.
return HttpResponse( return HttpResponse(
_('Successfully saved configuration.'), _('Successfully saved configuration.'),
content_type=content_type, status=ResponseCode.okay) status=ResponseCode.okay,
)
@method_decorator(never_cache, name='dispatch') @method_decorator(never_cache, name='dispatch')
@ -238,15 +248,11 @@ class DelView(View):
""" """
Handle a POST request Handle a POST request
""" """
# Our default response type
content_type = 'text/plain; charset=utf-8'
# Clear the key # Clear the key
result = ConfigCache.clear(key) result = ConfigCache.clear(key)
if result is None: if result is None:
return HttpResponse( return HttpResponse(
_('There was no configuration to remove.'), _('There was no configuration to remove.'),
content_type=content_type,
status=ResponseCode.no_content, status=ResponseCode.no_content,
) )
@ -254,14 +260,12 @@ class DelView(View):
# There was a failure at the os level # There was a failure at the os level
return HttpResponse( return HttpResponse(
_('The configuration could not be removed.'), _('The configuration could not be removed.'),
content_type=content_type,
status=ResponseCode.internal_server_error, status=ResponseCode.internal_server_error,
) )
# Removed content # Removed content
return HttpResponse( return HttpResponse(
_('Successfully removed configuration.'), _('Successfully removed configuration.'),
content_type=content_type,
status=ResponseCode.okay, status=ResponseCode.okay,
) )
@ -275,8 +279,6 @@ class GetView(View):
""" """
Handle a POST request Handle a POST request
""" """
# Our default response type
content_type = 'text/plain; charset=utf-8'
config, format = ConfigCache.get(key) config, format = ConfigCache.get(key)
if config is None: if config is None:
@ -290,28 +292,29 @@ class GetView(View):
# no content to return # no content to return
return HttpResponse( return HttpResponse(
_('There was no configuration found.'), _('There was no configuration found.'),
content_type=content_type,
status=ResponseCode.no_content, status=ResponseCode.no_content,
) )
# Something went very wrong; return 500 # Something went very wrong; return 500
return HttpResponse( return HttpResponse(
_('An error occured accessing configuration.'), _('An error occured accessing configuration.'),
content_type=content_type,
status=ResponseCode.internal_server_error, status=ResponseCode.internal_server_error,
) )
# Our configuration was retrieved; now our response varies on whether # Our configuration was retrieved; now our response varies on whether
# we are a YAML configuration or a TEXT based one. This allows us to # we are a YAML configuration or a TEXT based one. This allows us to
# be compatible with those using the AppriseConfig() library or the # be compatible with those using the AppriseConfig() library or the
# reference to it through the --config (-c) option in the CLI # reference to it through the --config (-c) option in the CLI.
if format == apprise.ConfigFormat.YAML: content_type = 'text/yaml; charset=utf-8' \
# update our return content type from the default text if format == apprise.ConfigFormat.YAML \
content_type = 'text/yaml; charset=utf-8' else 'text/html; charset=utf-8'
# Return our retrieved content # Return our retrieved content
return HttpResponse( 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') @method_decorator((gzip_page, never_cache), name='dispatch')
@ -323,9 +326,6 @@ class NotifyView(View):
""" """
Handle a POST request Handle a POST request
""" """
# Our default response type
content_type = 'text/plain; charset=utf-8'
# our content # our content
content = {} content = {}
if MIME_IS_FORM.match(request.content_type): if MIME_IS_FORM.match(request.content_type):
@ -344,14 +344,12 @@ class NotifyView(View):
# could not parse JSON response... # could not parse JSON response...
return HttpResponse( return HttpResponse(
_('Invalid JSON specified.'), _('Invalid JSON specified.'),
content_type=content_type,
status=ResponseCode.bad_request) status=ResponseCode.bad_request)
if not content: if not content:
# We could not handle the Content-Type # We could not handle the Content-Type
return HttpResponse( return HttpResponse(
_('The message format is not supported.'), _('The message format is not supported.'),
content_type=content_type,
status=ResponseCode.bad_request) status=ResponseCode.bad_request)
# Some basic error checking # Some basic error checking
@ -361,7 +359,6 @@ class NotifyView(View):
return HttpResponse( return HttpResponse(
_('An invalid payload was specified.'), _('An invalid payload was specified.'),
content_type=content_type,
status=ResponseCode.bad_request) status=ResponseCode.bad_request)
# If we get here, we have enough information to generate a notification # If we get here, we have enough information to generate a notification
@ -378,14 +375,12 @@ class NotifyView(View):
# no content to return # no content to return
return HttpResponse( return HttpResponse(
_('There was no configuration found.'), _('There was no configuration found.'),
content_type=content_type,
status=ResponseCode.no_content, status=ResponseCode.no_content,
) )
# Something went very wrong; return 500 # Something went very wrong; return 500
return HttpResponse( return HttpResponse(
_('An error occured accessing configuration.'), _('An error occured accessing configuration.'),
content_type=content_type,
status=ResponseCode.internal_server_error, status=ResponseCode.internal_server_error,
) )
@ -423,13 +418,13 @@ class NotifyView(View):
# We could not write the temporary file to disk # We could not write the temporary file to disk
return HttpResponse( return HttpResponse(
_('The configuration could not be loaded.'), _('The configuration could not be loaded.'),
content_type=content_type,
status=ResponseCode.internal_server_error) status=ResponseCode.internal_server_error)
# Return our retrieved content # Return our retrieved content
return HttpResponse( return HttpResponse(
_('Notification(s) sent.'), _('Notification(s) sent.'),
content_type=content_type, status=ResponseCode.okay) status=ResponseCode.okay
)
@method_decorator((gzip_page, never_cache), name='dispatch') @method_decorator((gzip_page, never_cache), name='dispatch')
@ -441,9 +436,6 @@ class StatelessNotifyView(View):
""" """
Handle a POST request Handle a POST request
""" """
# Our default response type
content_type = 'text/plain; charset=utf-8'
# our content # our content
content = {} content = {}
if MIME_IS_FORM.match(request.content_type): if MIME_IS_FORM.match(request.content_type):
@ -462,14 +454,12 @@ class StatelessNotifyView(View):
# could not parse JSON response... # could not parse JSON response...
return HttpResponse( return HttpResponse(
_('Invalid JSON specified.'), _('Invalid JSON specified.'),
content_type=content_type,
status=ResponseCode.bad_request) status=ResponseCode.bad_request)
if not content: if not content:
# We could not handle the Content-Type # We could not handle the Content-Type
return HttpResponse( return HttpResponse(
_('The message format is not supported.'), _('The message format is not supported.'),
content_type=content_type,
status=ResponseCode.bad_request) status=ResponseCode.bad_request)
if not content.get('urls') and settings.APPRISE_STATELESS_URLS: if not content.get('urls') and settings.APPRISE_STATELESS_URLS:
@ -484,7 +474,6 @@ class StatelessNotifyView(View):
return HttpResponse( return HttpResponse(
_('An invalid payload was specified.'), _('An invalid payload was specified.'),
content_type=content_type,
status=ResponseCode.bad_request) status=ResponseCode.bad_request)
# Prepare our apprise object # Prepare our apprise object
@ -495,7 +484,6 @@ class StatelessNotifyView(View):
if not len(a_obj): if not len(a_obj):
return HttpResponse( return HttpResponse(
_('There was no services to notify.'), _('There was no services to notify.'),
content_type=content_type,
status=ResponseCode.no_content, status=ResponseCode.no_content,
) )
@ -510,4 +498,112 @@ class StatelessNotifyView(View):
# Return our retrieved content # Return our retrieved content
return HttpResponse( return HttpResponse(
_('Notification(s) sent.'), _('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
)