mirror of
https://github.com/caronc/apprise-api.git
synced 2025-01-07 06:29:43 +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/*,
|
*migrations/*,
|
||||||
*settings*,
|
*settings*,
|
||||||
*tests/*,
|
*tests/*,
|
||||||
|
lib/*,
|
||||||
|
lib64/*,
|
||||||
*urls.py,
|
*urls.py,
|
||||||
*core/wsgi.py,
|
*core/wsgi.py,
|
||||||
gunicorn.conf.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)
|
[![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`).
|
||||||
|
@ -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
|
||||||
|
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(
|
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'),
|
||||||
]
|
]
|
||||||
|
@ -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
|
||||||
|
)
|
||||||
|
Loading…
Reference in New Issue
Block a user