mirror of
https://github.com/caronc/apprise-api.git
synced 2025-01-05 13:39:15 +01:00
Support a /json/urls/{key} query to provide loaded details (#4)
This commit is contained in:
parent
d571a8e186
commit
4b9c072d6b
@ -4,6 +4,8 @@ omit =
|
||||
*migrations/*,
|
||||
*settings*,
|
||||
*tests/*,
|
||||
lib/*,
|
||||
lib64/*,
|
||||
*urls.py,
|
||||
*core/wsgi.py,
|
||||
gunicorn.conf.py,
|
||||
|
43
README.md
43
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}`:<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/>
|
||||
|
||||
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.<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.
|
||||
| `/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}*.<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.
|
||||
| Path | Method | Description |
|
||||
|------------- | ------ | ----------- |
|
||||
| `/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}` | 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}*.<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/` | 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
|
||||
|
||||
- `{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:<br/> - 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:<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` | 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`).
|
||||
|
@ -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
|
||||
|
148
apprise_api/api/tests/test_json_urls.py
Normal file
148
apprise_api/api/tests/test_json_urls.py
Normal 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')
|
@ -47,4 +47,7 @@ urlpatterns = [
|
||||
re_path(
|
||||
r'^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'),
|
||||
]
|
||||
|
@ -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
|
||||
)
|
||||
|
Loading…
Reference in New Issue
Block a user