mirror of
https://github.com/caronc/apprise-api.git
synced 2025-08-18 10:29:39 +02:00
Initial commit
This commit is contained in:
0
apprise_api/api/__init__.py
Normal file
0
apprise_api/api/__init__.py
Normal file
29
apprise_api/api/apps.py
Normal file
29
apprise_api/api/apps.py
Normal file
@@ -0,0 +1,29 @@
|
||||
# -*- 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.apps import AppConfig
|
||||
|
||||
|
||||
class ApiConfig(AppConfig):
|
||||
name = 'api'
|
137
apprise_api/api/forms.py
Normal file
137
apprise_api/api/forms.py
Normal file
@@ -0,0 +1,137 @@
|
||||
# -*- 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.
|
||||
|
||||
import apprise
|
||||
from django import forms
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
# Define our potential configuration types
|
||||
CONFIG_FORMATS = (
|
||||
(apprise.ConfigFormat.TEXT, _('TEXT')),
|
||||
(apprise.ConfigFormat.YAML, _('YAML')),
|
||||
)
|
||||
|
||||
NOTIFICATION_TYPES = (
|
||||
(apprise.NotifyType.INFO, _('Info')),
|
||||
(apprise.NotifyType.SUCCESS, _('Success')),
|
||||
(apprise.NotifyType.WARNING, _('Warning')),
|
||||
(apprise.NotifyType.FAILURE, _('Failure')),
|
||||
)
|
||||
|
||||
|
||||
class AddByUrlForm(forms.Form):
|
||||
"""
|
||||
Form field for adding entries simply by passing in a string
|
||||
of one or more URLs that have been deliminted by either a
|
||||
comma and/or a space.
|
||||
|
||||
This content can just be directly fed straight into Apprise
|
||||
"""
|
||||
urls = forms.CharField(
|
||||
label=_('URLs'),
|
||||
widget=forms.TextInput(
|
||||
attrs={
|
||||
'placeholder': 'mailto://user:pass@domain.com, '
|
||||
'slack://tokena/tokenb/tokenc, ...'}),
|
||||
max_length=1024,
|
||||
)
|
||||
|
||||
|
||||
class AddByConfigForm(forms.Form):
|
||||
"""
|
||||
This is the reading in of a configuration file which contains
|
||||
potential asset information (if yaml file) and tag details.
|
||||
"""
|
||||
|
||||
format = forms.ChoiceField(
|
||||
label=_('Format'),
|
||||
choices=CONFIG_FORMATS,
|
||||
)
|
||||
|
||||
config = forms.CharField(
|
||||
label=_('Configuration'),
|
||||
widget=forms.Textarea(),
|
||||
max_length=4096,
|
||||
)
|
||||
|
||||
|
||||
class NotifyForm(forms.Form):
|
||||
"""
|
||||
This is the reading in of a configuration file which contains
|
||||
potential asset information (if yaml file) and tag details.
|
||||
"""
|
||||
|
||||
type = forms.ChoiceField(
|
||||
label=_('Type'),
|
||||
choices=NOTIFICATION_TYPES,
|
||||
initial=NOTIFICATION_TYPES[0][0],
|
||||
required=False,
|
||||
)
|
||||
|
||||
title = forms.CharField(
|
||||
label=_('Title'),
|
||||
widget=forms.TextInput(attrs={'placeholder': _('Optional Title')}),
|
||||
max_length=apprise.NotifyBase.title_maxlen,
|
||||
required=False,
|
||||
)
|
||||
|
||||
body = forms.CharField(
|
||||
label=_('Body'),
|
||||
widget=forms.Textarea(),
|
||||
max_length=apprise.NotifyBase.body_maxlen,
|
||||
)
|
||||
|
||||
tag = forms.ChoiceField(
|
||||
label=_('Tags'),
|
||||
widget=forms.TextInput(
|
||||
attrs={'placeholder': _('Optional_Tag1, Optional_Tag2, ...')}),
|
||||
required=False,
|
||||
)
|
||||
|
||||
def clean_type(self):
|
||||
"""
|
||||
We just ensure there is a type always set
|
||||
"""
|
||||
data = self.cleaned_data['type']
|
||||
if not data:
|
||||
# Always set a type
|
||||
data = apprise.NotifyType.INFO
|
||||
return data
|
||||
|
||||
|
||||
class NotifyByUrlForm(AddByUrlForm, NotifyForm):
|
||||
"""
|
||||
Same as the NotifyForm but additionally processes a string of URLs to
|
||||
notify directly.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class NotifyByConfigForm(AddByConfigForm, NotifyForm):
|
||||
"""
|
||||
Same as the NotifyForm but additionally process a configuration file as
|
||||
well.
|
||||
"""
|
||||
pass
|
0
apprise_api/api/migrations/__init__.py
Normal file
0
apprise_api/api/migrations/__init__.py
Normal file
0
apprise_api/api/models.py
Normal file
0
apprise_api/api/models.py
Normal file
61
apprise_api/api/templates/base.html
Normal file
61
apprise_api/api/templates/base.html
Normal file
@@ -0,0 +1,61 @@
|
||||
{% load static %}
|
||||
{% load i18n %}
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<!--Let browser know website is optimized for mobile-->
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<link rel="stylesheet" href="{% static 'css/materialize.min.css' %}"/>
|
||||
<link rel="stylesheet" href="{% static 'iconfont/material-icons.css' %}">
|
||||
<link rel="stylesheet" href="{% static 'css/base.css' %}"/>
|
||||
<link rel="shortcut icon" type="image/png" href="{% static 'favicon.ico' %}" />
|
||||
<script src="{% static 'js/materialize.min.js' %}"></script>
|
||||
<title>{% block title %}{% trans "Apprise API" %}{% endblock %}</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="content">
|
||||
<!-- Title -->
|
||||
<div class="nav row teal lighten-5 z-depth-2">
|
||||
<div class="col s12">
|
||||
<a href="{% url 'welcome' %}">
|
||||
<img class="left" src="{% static "logo.png" %}" alt="{% trans "Apprise Logo" %}" />
|
||||
</a>
|
||||
<h1>{% trans "Apprise API" %}</h1>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Page Layout here -->
|
||||
<div class="row">
|
||||
|
||||
<div class="col s3">
|
||||
<ul class="collection z-depth-1">
|
||||
<a class="collection-item" href="{% url 'config' 'apprise' %}"><i class="tiny material-icons">settings</i> {% trans "Configuration Manager" %}</a>
|
||||
</ul>
|
||||
<ul class="collection z-depth-1">
|
||||
<a class="collection-item" target="_blank" href="https://github.com/caronc/apprise/wiki#notification-services">📣 {% trans "Notification Services" %}</a>
|
||||
<a class="collection-item" target="_blank" href="https://github.com/caronc/apprise/wiki/config"><i class="tiny material-icons">local_library</i> {% trans "Configuration Help" %}</a>
|
||||
<a class="collection-item" target="_blank" href="https://github.com/caronc/apprise/wiki/Troubleshooting"><i class="tiny material-icons">build</i> {% trans "Troubleshooting" %}</a>
|
||||
<a class="collection-item" target="_blank" href="https://github.com/caronc/apprise/wiki/CLI_Usage"><i class="tiny material-icons">lightbulb_outline</i> {% trans "Using the CLI" %}</a>
|
||||
</ul>
|
||||
<ul class="collection z-depth-1">
|
||||
<a class="collection-item" target="_blank" href="https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=MHANV39UZNQ5E"><i class="tiny material-icons">favorite</i> {% trans "Support Apprise" %}</a>
|
||||
<a class="collection-item" target="_blank" href="https://github.com/sponsors/caronc"><i class="tiny material-icons">favorite</i> {% trans "Sponsor Developer" %}</a>
|
||||
</ul>
|
||||
{% block menu %}{% endblock %}
|
||||
</div>
|
||||
|
||||
<div class="col s9">
|
||||
{% block body %}{% endblock %}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
M.AutoInit();
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
76
apprise_api/api/templates/config.html
Normal file
76
apprise_api/api/templates/config.html
Normal file
@@ -0,0 +1,76 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load i18n %}
|
||||
{% block body %}
|
||||
<h3>{% trans "Management for:" %} <em>{{ key }}</em></h3>
|
||||
<div class="row">
|
||||
<div class="col s12">
|
||||
<ul class="tabs">
|
||||
<li class="tab col s4"><a class="active" href="#overview">{% trans "Overview" %}</a></li>
|
||||
<li class="tab col s4"><a href="#config">{%trans "Configuration" %}</a></li>
|
||||
<li class="tab col s4"><a href="#notify">{%trans "Notifications" %}</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div id="overview" class="col s12">
|
||||
<p>
|
||||
{% blocktrans %}
|
||||
Here is where you can store your configuration so that it can be accessed by Apprise. You can always refer to the <a href="https://github.com/caronc/apprise/wiki#notification-services">Apprise Wiki</a> if you're having troubles assembling your URL(s).
|
||||
You have chosen to associate your configuration with the key <code>{{key}}</code>. If anything was previously associated with this key, it will be replaced if you continue.
|
||||
{% endblocktrans %}
|
||||
</p>
|
||||
<p>
|
||||
{% blocktrans %}
|
||||
In the future you can return to this configuration screen at any time by placing the following into your browser:{% endblocktrans %}
|
||||
<br/><code>{{request.scheme}}://{{request.META.HTTP_HOST}}{{request.path}}</code>
|
||||
</p>
|
||||
<div class="section">
|
||||
{% blocktrans %}For example, the following command would cause apprise to retrieve the configuration loaded and send a test notification to all of your added services:{% endblocktrans %}
|
||||
<br/><code>apprise --body="Test Message" --tag=all --config={{request.scheme}}://{{request.META.HTTP_HOST}}{% url "get" key %}</code>
|
||||
</div>
|
||||
</div>
|
||||
<div id="config" class="col s12">
|
||||
|
||||
<div class="section">
|
||||
<h5>{% trans "Option 1: Add By URL" %}</h5>
|
||||
<p>
|
||||
{% blocktrans %}
|
||||
Use a comma and/or space to separate one Apprise URL from another.
|
||||
{% endblocktrans %}
|
||||
<form action="#" method="post">
|
||||
{{ form_url }}
|
||||
<button class="btn waves-effect waves-light" type="submit" name="action">{% trans "Submit" %}
|
||||
<i class="material-icons right">send</i>
|
||||
</button>
|
||||
</form>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h5>{% trans "Option 2: Add By Config" %}</h5>
|
||||
<p>
|
||||
{% blocktrans %}
|
||||
This option grants you a bit more flexability because you can additionally associate tags with your URLs. Those using YAML configuration can also alter the Apprise Asset object as well for a more customized look and feel.
|
||||
{% endblocktrans %}
|
||||
<form action="#" method="post">
|
||||
{{ form_cfg }}
|
||||
<button class="btn waves-effect waves-light" type="submit" name="action">{% trans "Submit" %}
|
||||
<i class="material-icons right">send</i>
|
||||
</button>
|
||||
</form>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div id="notify" class="col s12">
|
||||
<p>
|
||||
{% blocktrans %}
|
||||
You can send a notification using the loaded configuration:
|
||||
{% endblocktrans %}
|
||||
<form action="#" method="post">
|
||||
{{ form_notify }}
|
||||
<button class="btn waves-effect waves-light" type="submit" name="action">{% trans "Submit" %}
|
||||
<i class="material-icons right">send</i>
|
||||
</button>
|
||||
</form>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
198
apprise_api/api/templates/welcome.html
Normal file
198
apprise_api/api/templates/welcome.html
Normal file
@@ -0,0 +1,198 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load i18n %}
|
||||
{% block body %}
|
||||
<h4>{% trans "The Apprise API" %}</h4>
|
||||
<p>
|
||||
{% blocktrans %}
|
||||
<a target="_blank" href="https://github.com/caronc/apprise">Apprise</a> allows you to send a notification to almost
|
||||
all of the most popular notification services available to us today such as: <em>Telegram</em>, <em>Discord</em>,
|
||||
<em>Slack</em>, <em>Amazon SNS</em>, <em>Gotify</em>, etc.
|
||||
This API provides a simple gateway to directly access it via an HTTP interface.
|
||||
<ul>
|
||||
<li><i class="tiny material-icons">chevron_right</i>This project was designed to be incredibly light weight.</li>
|
||||
<li><i class="tiny material-icons">chevron_right</i>Configuration can be persistently stored for retrieval.</li>
|
||||
</ul>
|
||||
{% endblocktrans %}
|
||||
</p>
|
||||
|
||||
<div class="section">
|
||||
<h4>{% trans "Endpoints" %}</h4>
|
||||
<p>{% blocktrans %}All endpoints that expect posted data can be received in either JSON or in it's standard encoding.
|
||||
You must pass along the <code>Content-Type</code> as <code>application/json</code> in order for it to be interpreted
|
||||
properly.{% endblocktrans %}</p>
|
||||
<table class="highlighted">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{% trans "URL" %}</th>
|
||||
<th>{% trans "Description" %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><code>/add/<em>{% trans "KEY" %}</em></code></td>
|
||||
<td>
|
||||
{% blocktrans %}Used to add a new Apprise configuration or a set of URLs and associates them with the
|
||||
specified <em>KEY</em>. See the <a target="_blank"
|
||||
href="https://github.com/caronc/apprise/wiki#notification-services">Apprise Wiki</a> if you need help
|
||||
constructing your URLs.{% endblocktrans %}
|
||||
<div class="section">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{% trans "Parameter" %}</th>
|
||||
<th>{% trans "Description" %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>urls</td>
|
||||
<td>{% blocktrans %}Used to define one or more Apprise URL(s). Use a comma and/or space to separate
|
||||
one URL from the next.{% endblocktrans %}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>config</td>
|
||||
<td>{% blocktrans %}Provide the contents of either a YAML or TEXT based Apprise
|
||||
configuration.{% endblocktrans %}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>format</td>
|
||||
<td>{% blocktrans %}This field is only required if you've specified the config option. It's purpose is
|
||||
to tell the server which of the supported (Apprise) configuration types you are passing. Valid
|
||||
options are:{% endblocktrans %}
|
||||
<ol>
|
||||
<li><code>{% trans "yaml" %}</code></li>
|
||||
<li><code>{% trans "text" %}</code></li>
|
||||
</ol>
|
||||
<ul>
|
||||
<li>{% blocktrans %}You must specify either the <code>urls</code> parameter or the
|
||||
<code>config</code>.{% endblocktrans %}</li>
|
||||
<li>{% blocktrans %}The <code>urls</code> takes priority over the <code>config</code> if both were
|
||||
specified.{% endblocktrans %}</li>
|
||||
<li>{% blocktrans %}The <code>format</code> parameter is only required if the <code>config</code>
|
||||
parameter was also specified.{% endblocktrans %}</li>
|
||||
</ul>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<ul class="collapsible">
|
||||
<li>
|
||||
<div class="collapsible-header"><i class="material-icons">code</i>curl example</div>
|
||||
<div class="collapsible-body">
|
||||
<code>#
|
||||
{% blocktrans %}Load a single URL and assign it to the <em>KEY</em> of abc123{% endblocktrans %}<br/>
|
||||
curl -X POST -d '{"urls":"mailto://user:pass@gmail.com"}' \<br/>
|
||||
-H "Content-Type: application/json" \<br/>
|
||||
http://localhost:8000/add/<em>abc123</em></code>
|
||||
<code><br/>
|
||||
<br/>#{% blocktrans %}Load a simple TEXT config entry <em>KEY</em> of abc123{% endblocktrans %}<br/>
|
||||
curl -X POST -d '{"format":"text","config":"devops=mailto://user:pass@gmail.com"}' \<br/>
|
||||
-H "Content-Type: application/json" \<br/>
|
||||
http://localhost:8000/add/abc123/
|
||||
</code>
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div class="collapsible-header"><i class="material-icons">code</i>python example</div>
|
||||
<div class="collapsible-body"><code>Coming Soon</code></div>
|
||||
<li>
|
||||
<li>
|
||||
<div class="collapsible-header"><i class="material-icons">code</i>php example</div>
|
||||
<div class="collapsible-body"><code>Coming Soon</code></div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>/del/<em>{% trans "KEY" %}</em></code></td>
|
||||
<td>{% blocktrans %}There are no arguments required. If the <em>KEY</em> exists and has data associated with it,
|
||||
it will be removed.{% endblocktrans %}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>/get/<em>{% trans "KEY" %}</em></code></td>
|
||||
<td>{% blocktrans %}This feature can be used by Apprise itself. It provides a means of remotely fetching it's
|
||||
configuration.{% endblocktrans %}
|
||||
|
||||
<p><strong>{% trans "As an example:" %}</strong><br /><code>apprise --body="test message" --config={{ request.scheme }}://{{request.META.HTTP_HOST}}{{ request.path }}<em>{% trans "KEY" %}</em></p>
|
||||
<ul class="collapsible">
|
||||
<li>
|
||||
<div class="collapsible-header"><i class="material-icons">code</i>curl example</div>
|
||||
<div class="collapsible-body"><code>
|
||||
# {% blocktrans %}Load a single URL and assign it to the <em>KEY</em> of abc123{% endblocktrans %}</br>
|
||||
curl -X POST -H "Content-Type: application/json" \<br/>
|
||||
http://localhost:8000/get/<em>abc123</em></code>
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div class="collapsible-header"><i class="material-icons">code</i>python example</div>
|
||||
<div class="collapsible-body"><code>Coming Soon</code></div>
|
||||
<li>
|
||||
<li>
|
||||
<div class="collapsible-header"><i class="material-icons">code</i>php example</div>
|
||||
<div class="collapsible-body"><code>Coming Soon</code></div>
|
||||
</li>
|
||||
</ul>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>/notify/<em>{% trans "KEY" %}</em></code></td>
|
||||
<td>{% blocktrans %}Notifies the URLs associated with the specified <em>KEY</em>.{% endblocktrans %}
|
||||
<div class="section">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{% trans "Parameter" %}</th>
|
||||
<th>{% trans "Description" %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>body</td>
|
||||
<td>{% blocktrans %}Defines the message body. This field is required!{% endblocktrans %}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>title</td>
|
||||
<td>{% blocktrans %}The title to include in the notification. This is an optional field.{% endblocktrans %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>type</td>
|
||||
<td>{% blocktrans %}This optional field defines the notification type. The possible options
|
||||
are:{% endblocktrans %}
|
||||
<ol>
|
||||
<li><code>{% trans "info" %}</code> - <i>{% blocktrans %}this is the default option if a type isn't
|
||||
specified.{% endblocktrans %}</i></li>
|
||||
<li><code>{% trans "success" %}</code></li>
|
||||
<li><code>{% trans "warning" %}</code></li>
|
||||
<li><code>{% trans "failure" %}</code></li>
|
||||
</ol>
|
||||
</td>
|
||||
<tr>
|
||||
<td>tags</td>
|
||||
<td>{% blocktrans %}Apply tagging logic to the further filter your URLs. This is an optional
|
||||
field.{% endblocktrans %}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div class="section">
|
||||
<h5>{% trans "Endpoint Notes" %}</h5>
|
||||
<p>
|
||||
The <em>KEY</em> you plan to associate your configuration with:
|
||||
<ol>
|
||||
<li>Can not have spaces and/or special characters in it. Both a dash (<code>-</code>) and underscore
|
||||
(<code>_</code>) are the only exceptions to this rule.</li>
|
||||
<li>Must start with at least 2 alpha/numeric characters.</li>
|
||||
<li>Can not exceed 64 characters in total length.</li>
|
||||
</ol>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
0
apprise_api/api/tests/__init__.py
Normal file
0
apprise_api/api/tests/__init__.py
Normal file
185
apprise_api/api/tests/test_add.py
Normal file
185
apprise_api/api/tests/test_add.py
Normal file
@@ -0,0 +1,185 @@
|
||||
# -*- 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 apprise import ConfigFormat
|
||||
from unittest.mock import patch
|
||||
import json
|
||||
|
||||
|
||||
class AddTests(SimpleTestCase):
|
||||
|
||||
def test_add_invalid_key_status_code(self):
|
||||
"""
|
||||
Test GET requests to invalid key
|
||||
"""
|
||||
response = self.client.get('/add/**invalid-key**')
|
||||
assert response.status_code == 404
|
||||
|
||||
def test_save_config_by_urls(self):
|
||||
"""
|
||||
Test adding an configuration by URLs
|
||||
"""
|
||||
|
||||
# our key to use
|
||||
key = 'test_save_config_by_urls'
|
||||
|
||||
# GET returns 405 (not allowed)
|
||||
response = self.client.get('/add/{}'.format(key))
|
||||
assert response.status_code == 405
|
||||
|
||||
# no data
|
||||
response = self.client.post('/add/{}'.format(key))
|
||||
assert response.status_code == 400
|
||||
|
||||
# No entries specified
|
||||
response = self.client.post(
|
||||
'/add/{}'.format(key), {'urls': ''})
|
||||
assert response.status_code == 400
|
||||
|
||||
# Added successfully
|
||||
response = self.client.post(
|
||||
'/add/{}'.format(key), {'urls': 'mailto://user:pass@yahoo.ca'})
|
||||
assert response.status_code == 200
|
||||
|
||||
# URL is actually not a valid one (invalid Slack tokens specified
|
||||
# below)
|
||||
response = self.client.post(
|
||||
'/add/{}'.format(key), {'urls': 'slack://TokenA/TokenB/TokenC'})
|
||||
assert response.status_code == 400
|
||||
|
||||
# Test with JSON
|
||||
response = self.client.post(
|
||||
'/add/{}'.format(key),
|
||||
data=json.dumps({'urls': 'mailto://user:pass@yahoo.ca'}),
|
||||
content_type='application/json',
|
||||
)
|
||||
assert response.status_code == 200
|
||||
|
||||
# Invalid JSON
|
||||
response = self.client.post(
|
||||
'/add/{}'.format(key),
|
||||
data='{',
|
||||
content_type='application/json',
|
||||
)
|
||||
assert response.status_code == 400
|
||||
|
||||
# Test the handling of underlining disk/write exceptions
|
||||
with patch('gzip.open') as mock_open:
|
||||
mock_open.side_effect = OSError()
|
||||
# We'll fail to write our key now
|
||||
response = self.client.post(
|
||||
'/add/{}'.format(key),
|
||||
data=json.dumps({'urls': 'mailto://user:pass@yahoo.ca'}),
|
||||
content_type='application/json',
|
||||
)
|
||||
|
||||
# internal errors are correctly identified
|
||||
assert response.status_code == 500
|
||||
|
||||
def test_save_config_by_config(self):
|
||||
"""
|
||||
Test adding an configuration by a config file
|
||||
"""
|
||||
|
||||
# our key to use
|
||||
key = 'test_save_config_by_config'
|
||||
|
||||
# Empty Text Configuration
|
||||
config = """
|
||||
|
||||
""" # noqa W293
|
||||
response = self.client.post(
|
||||
'/add/{}'.format(key), {
|
||||
'format': ConfigFormat.TEXT, 'config': config})
|
||||
assert response.status_code == 400
|
||||
|
||||
# Valid Text Configuration
|
||||
config = """
|
||||
browser,media=notica://VToken
|
||||
home=mailto://user:pass@hotmail.com
|
||||
"""
|
||||
response = self.client.post(
|
||||
'/add/{}'.format(key),
|
||||
{'format': ConfigFormat.TEXT, 'config': config})
|
||||
assert response.status_code == 200
|
||||
|
||||
# Test with JSON
|
||||
response = self.client.post(
|
||||
'/add/{}'.format(key),
|
||||
data=json.dumps({'format': ConfigFormat.TEXT, 'config': config}),
|
||||
content_type='application/json',
|
||||
)
|
||||
assert response.status_code == 200
|
||||
|
||||
# Test invalid config format
|
||||
response = self.client.post(
|
||||
'/add/{}'.format(key),
|
||||
data=json.dumps({'format': 'INVALID', 'config': config}),
|
||||
content_type='application/json',
|
||||
)
|
||||
assert response.status_code == 400
|
||||
|
||||
with patch('tempfile._TemporaryFileWrapper') as mock_ntf:
|
||||
mock_ntf.side_effect = OSError()
|
||||
# we won't be able to write our retrieved configuration
|
||||
# to disk for processing; we'll get a 500 error
|
||||
response = self.client.post(
|
||||
'/add/{}'.format(key),
|
||||
data=json.dumps(
|
||||
{'format': ConfigFormat.TEXT, 'config': config}),
|
||||
content_type='application/json',
|
||||
)
|
||||
|
||||
# internal errors are correctly identified
|
||||
assert response.status_code == 500
|
||||
|
||||
# Test the handling of underlining disk/write exceptions
|
||||
with patch('gzip.open') as mock_open:
|
||||
mock_open.side_effect = OSError()
|
||||
# We'll fail to write our key now
|
||||
response = self.client.post(
|
||||
'/add/{}'.format(key),
|
||||
data=json.dumps(
|
||||
{'format': ConfigFormat.TEXT, 'config': config}),
|
||||
content_type='application/json',
|
||||
)
|
||||
|
||||
# internal errors are correctly identified
|
||||
assert response.status_code == 500
|
||||
|
||||
def test_save_with_bad_input(self):
|
||||
"""
|
||||
Test adding with bad input in general
|
||||
"""
|
||||
|
||||
# our key to use
|
||||
key = 'test_save_with_bad_input'
|
||||
# Test with JSON
|
||||
response = self.client.post(
|
||||
'/add/{}'.format(key),
|
||||
data=json.dumps({'garbage': 'input'}),
|
||||
content_type='application/json',
|
||||
)
|
||||
assert response.status_code == 400
|
105
apprise_api/api/tests/test_config_cache.py
Normal file
105
apprise_api/api/tests/test_config_cache.py
Normal file
@@ -0,0 +1,105 @@
|
||||
# -*- 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.
|
||||
import os
|
||||
from ..utils import AppriseConfigCache
|
||||
from apprise import ConfigFormat
|
||||
from unittest.mock import patch
|
||||
import errno
|
||||
|
||||
|
||||
def test_apprise_config_io(tmpdir):
|
||||
"""
|
||||
Test Apprise Config Disk Put/Get
|
||||
"""
|
||||
content = 'mailto://test:pass@gmail.com'
|
||||
key = 'test_apprise_config_io'
|
||||
|
||||
# Create our object to work with
|
||||
acc_obj = AppriseConfigCache(str(tmpdir))
|
||||
|
||||
# Verify that the content doesn't already exist
|
||||
assert acc_obj.get(key) == (None, '')
|
||||
|
||||
# Write our content assigned to our key
|
||||
assert acc_obj.put(key, content, ConfigFormat.TEXT)
|
||||
|
||||
# Test the handling of underlining disk/write exceptions
|
||||
with patch('gzip.open') as mock_open:
|
||||
mock_open.side_effect = OSError()
|
||||
# We'll fail to write our key now
|
||||
assert not acc_obj.put(key, content, ConfigFormat.TEXT)
|
||||
|
||||
# Get path details
|
||||
conf_dir, _ = acc_obj.path(key)
|
||||
|
||||
# List content of directory
|
||||
contents = os.listdir(conf_dir)
|
||||
|
||||
# There should be just 1 new file in this directory
|
||||
assert len(contents) == 1
|
||||
assert contents[0].endswith('.{}'.format(ConfigFormat.TEXT))
|
||||
|
||||
# Verify that the content is retrievable
|
||||
assert acc_obj.get(key) == (content, ConfigFormat.TEXT)
|
||||
|
||||
# Test the handling of underlining disk/read exceptions
|
||||
with patch('gzip.open') as mock_open:
|
||||
mock_open.side_effect = OSError()
|
||||
# We'll fail to read our key now
|
||||
assert acc_obj.get(key) == (None, None)
|
||||
|
||||
# Tidy up our content
|
||||
assert acc_obj.clear(key) is True
|
||||
|
||||
# But the second time is okay as it no longer exists
|
||||
assert acc_obj.clear(key) is None
|
||||
|
||||
with patch('os.remove') as mock_remove:
|
||||
mock_remove.side_effect = OSError(errno.EPERM)
|
||||
# OSError
|
||||
assert acc_obj.clear(key) is False
|
||||
|
||||
# Now test with YAML file
|
||||
content = """
|
||||
version: 1
|
||||
|
||||
urls:
|
||||
- windows://
|
||||
"""
|
||||
|
||||
# Write our content assigned to our key
|
||||
# This should gracefully clear the TEXT entry that was
|
||||
# previously in the spot
|
||||
assert acc_obj.put(key, content, ConfigFormat.YAML)
|
||||
|
||||
# List content of directory
|
||||
contents = os.listdir(conf_dir)
|
||||
|
||||
# There should STILL be just 1 new file in this directory
|
||||
assert len(contents) == 1
|
||||
assert contents[0].endswith('.{}'.format(ConfigFormat.YAML))
|
||||
|
||||
# Verify that the content is retrievable
|
||||
assert acc_obj.get(key) == (content, ConfigFormat.YAML)
|
61
apprise_api/api/tests/test_get.py
Normal file
61
apprise_api/api/tests/test_get.py
Normal file
@@ -0,0 +1,61 @@
|
||||
# -*- 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
|
||||
|
||||
|
||||
class GetTests(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_get_config(self):
|
||||
"""
|
||||
Test retrieving configuration
|
||||
"""
|
||||
|
||||
# our key to use
|
||||
key = 'test_get_config'
|
||||
|
||||
# GET returns 405 (not allowed)
|
||||
response = self.client.get('/get/{}'.format(key))
|
||||
assert response.status_code == 405
|
||||
|
||||
# No content saved to the location yet
|
||||
response = self.client.post('/get/{}'.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
|
||||
|
||||
# Now we should be able to see our content
|
||||
response = self.client.post('/get/{}'.format(key))
|
||||
assert response.status_code == 200
|
47
apprise_api/api/tests/test_manager.py
Normal file
47
apprise_api/api/tests/test_manager.py
Normal file
@@ -0,0 +1,47 @@
|
||||
# -*- 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
|
||||
|
||||
|
||||
class ManagerPageTests(SimpleTestCase):
|
||||
"""
|
||||
Manager Webpage testing
|
||||
"""
|
||||
|
||||
def test_manage_status_code(self):
|
||||
"""
|
||||
General testing of management page
|
||||
"""
|
||||
# No key was specified
|
||||
response = self.client.get('/cfg/')
|
||||
assert response.status_code == 404
|
||||
|
||||
# An invalid key was specified
|
||||
response = self.client.get('/cfg/**invalid-key**')
|
||||
assert response.status_code == 404
|
||||
|
||||
# An invalid key was specified
|
||||
response = self.client.get('/cfg/valid-key')
|
||||
assert response.status_code == 200
|
197
apprise_api/api/tests/test_notify.py
Normal file
197
apprise_api/api/tests/test_notify.py
Normal file
@@ -0,0 +1,197 @@
|
||||
# -*- 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
|
||||
from ..forms import NotifyForm
|
||||
import json
|
||||
import apprise
|
||||
|
||||
|
||||
class NotifyTests(SimpleTestCase):
|
||||
"""
|
||||
Test notifications
|
||||
"""
|
||||
|
||||
@patch('apprise.Apprise.notify')
|
||||
def test_notify_by_loaded_urls(self, mock_notify):
|
||||
"""
|
||||
Test adding a simple notification and notifying it
|
||||
"""
|
||||
|
||||
# Set our return value
|
||||
mock_notify.return_value = True
|
||||
|
||||
# our key to use
|
||||
key = 'test_notify_by_loaded_urls'
|
||||
|
||||
# Add some content
|
||||
response = self.client.post(
|
||||
'/add/{}'.format(key),
|
||||
{'urls': 'mailto://user:pass@yahoo.ca'})
|
||||
assert response.status_code == 200
|
||||
|
||||
# Preare our form data
|
||||
form_data = {
|
||||
'body': 'test notifiction',
|
||||
}
|
||||
|
||||
# At a minimum, just a body is required
|
||||
form = NotifyForm(data=form_data)
|
||||
assert form.is_valid()
|
||||
|
||||
# we always set a type if one wasn't done so already
|
||||
assert form.cleaned_data['type'] == apprise.NotifyType.INFO
|
||||
|
||||
# Send our notification
|
||||
response = self.client.post(
|
||||
'/notify/{}'.format(key), form.cleaned_data)
|
||||
assert response.status_code == 200
|
||||
assert mock_notify.call_count == 1
|
||||
|
||||
@patch('apprise.Apprise.notify')
|
||||
def test_notify_by_loaded_urls_with_json(self, mock_notify):
|
||||
"""
|
||||
Test adding a simple notification and notifying it using JSON
|
||||
"""
|
||||
|
||||
# Set our return value
|
||||
mock_notify.return_value = True
|
||||
|
||||
# our key to use
|
||||
key = 'test_notify_by_loaded_urls_with_json'
|
||||
|
||||
# Add some content
|
||||
response = self.client.post(
|
||||
'/add/{}'.format(key),
|
||||
{'urls': 'mailto://user:pass@yahoo.ca'})
|
||||
assert response.status_code == 200
|
||||
|
||||
# Preare our JSON data
|
||||
json_data = {
|
||||
'body': 'test notifiction',
|
||||
'type': apprise.NotifyType.WARNING,
|
||||
}
|
||||
|
||||
# Send our notification as a JSON object
|
||||
response = self.client.post(
|
||||
'/notify/{}'.format(key),
|
||||
data=json.dumps(json_data),
|
||||
content_type='application/json',
|
||||
)
|
||||
|
||||
# Still supported
|
||||
assert response.status_code == 200
|
||||
assert mock_notify.call_count == 1
|
||||
|
||||
# Reset our count
|
||||
mock_notify.reset_mock()
|
||||
|
||||
# Test referencing a key that doesn't exist
|
||||
response = self.client.post(
|
||||
'/notify/non-existant-key',
|
||||
data=json.dumps(json_data),
|
||||
content_type='application/json',
|
||||
)
|
||||
|
||||
# Nothing notified
|
||||
assert response.status_code == 204
|
||||
assert mock_notify.call_count == 0
|
||||
|
||||
# Test sending a garbage JSON object
|
||||
response = self.client.post(
|
||||
'/notify/{}'.format(key),
|
||||
data="{",
|
||||
content_type='application/json',
|
||||
)
|
||||
|
||||
assert response.status_code == 400
|
||||
assert mock_notify.call_count == 0
|
||||
|
||||
# Test sending with an invalid content type
|
||||
response = self.client.post(
|
||||
'/notify/{}'.format(key),
|
||||
data="{}",
|
||||
content_type='application/xml',
|
||||
)
|
||||
|
||||
assert response.status_code == 400
|
||||
assert mock_notify.call_count == 0
|
||||
|
||||
# Test sending without any content at all
|
||||
response = self.client.post(
|
||||
'/notify/{}'.format(key),
|
||||
data="{}",
|
||||
content_type='application/json',
|
||||
)
|
||||
|
||||
assert response.status_code == 400
|
||||
assert mock_notify.call_count == 0
|
||||
|
||||
# Test sending without a body
|
||||
json_data = {
|
||||
'type': apprise.NotifyType.WARNING,
|
||||
}
|
||||
|
||||
response = self.client.post(
|
||||
'/notify/{}'.format(key),
|
||||
data=json.dumps(json_data),
|
||||
content_type='application/json',
|
||||
)
|
||||
|
||||
assert response.status_code == 400
|
||||
assert mock_notify.call_count == 0
|
||||
|
||||
# Test inability to prepare writing config to disk
|
||||
json_data = {
|
||||
'body': 'test message'
|
||||
}
|
||||
|
||||
with patch('tempfile._TemporaryFileWrapper') as mock_ntf:
|
||||
mock_ntf.side_effect = OSError()
|
||||
# we won't be able to write our retrieved configuration
|
||||
# to disk for processing; we'll get a 500 error
|
||||
response = self.client.post(
|
||||
'/notify/{}'.format(key),
|
||||
data=json.dumps(json_data),
|
||||
content_type='application/json',
|
||||
)
|
||||
|
||||
# internal errors are correctly identified
|
||||
assert response.status_code == 500
|
||||
assert mock_notify.call_count == 0
|
||||
|
||||
# Test the handling of underlining disk/write exceptions
|
||||
with patch('gzip.open') as mock_open:
|
||||
mock_open.side_effect = OSError()
|
||||
# We'll fail to write our key now
|
||||
response = self.client.post(
|
||||
'/notify/{}'.format(key),
|
||||
data=json.dumps(json_data),
|
||||
content_type='application/json',
|
||||
)
|
||||
|
||||
# internal errors are correctly identified
|
||||
assert response.status_code == 500
|
||||
assert mock_notify.call_count == 0
|
32
apprise_api/api/tests/test_welcome.py
Normal file
32
apprise_api/api/tests/test_welcome.py
Normal file
@@ -0,0 +1,32 @@
|
||||
# -*- 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
|
||||
|
||||
|
||||
class WelcomePageTests(SimpleTestCase):
|
||||
|
||||
def test_welcome_page_status_code(self):
|
||||
response = self.client.get('/')
|
||||
assert response.status_code == 200
|
47
apprise_api/api/urls.py
Normal file
47
apprise_api/api/urls.py
Normal file
@@ -0,0 +1,47 @@
|
||||
# -*- 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.urls import re_path
|
||||
from . import views
|
||||
|
||||
urlpatterns = [
|
||||
re_path(
|
||||
r'^$',
|
||||
views.WelcomeView.as_view(), name='welcome'),
|
||||
re_path(
|
||||
r'^cfg/(?P<key>[\w_-]{1,64})/?',
|
||||
views.ConfigView.as_view(), name='config'),
|
||||
re_path(
|
||||
r'^add/(?P<key>[\w_-]{1,64})/?',
|
||||
views.AddView.as_view(), name='add'),
|
||||
re_path(
|
||||
r'^del/(?P<key>[\w_-]{1,64})/?',
|
||||
views.DelView.as_view(), name='del'),
|
||||
re_path(
|
||||
r'^get/(?P<key>[\w_-]{1,64})/?',
|
||||
views.GetView.as_view(), name='get'),
|
||||
re_path(
|
||||
r'^notify/(?P<key>[\w_-]{1,64})/?',
|
||||
views.NotifyView.as_view(), name='notify'),
|
||||
]
|
196
apprise_api/api/utils.py
Normal file
196
apprise_api/api/utils.py
Normal file
@@ -0,0 +1,196 @@
|
||||
# -*- 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.
|
||||
import os
|
||||
import tempfile
|
||||
import shutil
|
||||
import gzip
|
||||
import apprise
|
||||
import hashlib
|
||||
import errno
|
||||
|
||||
from django.conf import settings
|
||||
|
||||
|
||||
class AppriseConfigCache(object):
|
||||
"""
|
||||
Designed to make it easy to store/read contact back from disk in a cache
|
||||
type structure that is fast.
|
||||
"""
|
||||
|
||||
def __init__(self, cache_root, salt="apprise"):
|
||||
"""
|
||||
Works relative to the cache_root
|
||||
"""
|
||||
self.root = cache_root
|
||||
self.salt = salt.encode()
|
||||
|
||||
def put(self, key, content, fmt):
|
||||
"""
|
||||
Based on the key specified, content is written to disk (compressed)
|
||||
|
||||
key: is an alphanumeric string needed to write and read back this
|
||||
file being written.
|
||||
content: the content to be written to disk
|
||||
fmt: the content config format (of type apprise.ConfigFormat)
|
||||
|
||||
"""
|
||||
# There isn't a lot of error handling done here as it is presumed most
|
||||
# of the checking has been done higher up.
|
||||
|
||||
# First two characters are reserved for cache level directory writing.
|
||||
path, filename = self.path(key)
|
||||
os.makedirs(path, exist_ok=True)
|
||||
|
||||
# Write our file to a temporary file
|
||||
_, tmp_path = tempfile.mkstemp(suffix='.tmp', dir=path)
|
||||
try:
|
||||
with gzip.open(tmp_path, 'wb') as f:
|
||||
# Write our content to disk
|
||||
f.write(content.encode())
|
||||
|
||||
except OSError:
|
||||
# Handle failure
|
||||
os.remove(tmp_path)
|
||||
return False
|
||||
|
||||
# If we reach here we successfully wrote the content. We now safely
|
||||
# move our configuration into place. The following writes our content
|
||||
# to disk as /xx/key.fmt
|
||||
shutil.move(tmp_path, os.path.join(
|
||||
path, '{}.{}'.format(filename, fmt)))
|
||||
|
||||
# perform tidy of any other lingering files of other type in case
|
||||
# configuration changed from TEXT -> YAML or YAML -> TEXT
|
||||
if self.clear(key, set(apprise.CONFIG_FORMATS) - {fmt}) is False:
|
||||
# We couldn't remove an existing entry; clear what we just created
|
||||
self.clear(key, {fmt})
|
||||
# fail
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def get(self, key):
|
||||
"""
|
||||
Based on the key specified, content is written to disk (compressed)
|
||||
|
||||
key: is an alphanumeric string needed to write and read back this
|
||||
file being written.
|
||||
|
||||
The function returns a tuple of (content, fmt) where the content
|
||||
is the uncompressed content found in the file and fmt is the
|
||||
content representation (of type apprise.ConfigFormat).
|
||||
|
||||
If no data was found, then (None, None) is returned.
|
||||
"""
|
||||
|
||||
# There isn't a lot of error handling done here as it is presumed most
|
||||
# of the checking has been done higher up.
|
||||
|
||||
# First two characters are reserved for cache level directory writing.
|
||||
path, filename = self.path(key)
|
||||
|
||||
# prepare our format to return
|
||||
fmt = None
|
||||
|
||||
# Test the only possible hashed files we expect to find
|
||||
text_file = os.path.join(
|
||||
path, '{}.{}'.format(filename, apprise.ConfigFormat.TEXT))
|
||||
yaml_file = os.path.join(
|
||||
path, '{}.{}'.format(filename, apprise.ConfigFormat.YAML))
|
||||
|
||||
if os.path.isfile(text_file):
|
||||
fmt = apprise.ConfigFormat.TEXT
|
||||
path = text_file
|
||||
|
||||
elif os.path.isfile(yaml_file):
|
||||
fmt = apprise.ConfigFormat.YAML
|
||||
path = yaml_file
|
||||
|
||||
else:
|
||||
# Not found; we set the fmt to something other than none as
|
||||
# an indication for the upstream handling to know that we didn't
|
||||
# fail on error
|
||||
return (None, '')
|
||||
|
||||
# Initialize our content
|
||||
content = None
|
||||
try:
|
||||
with gzip.open(path, 'rb') as f:
|
||||
# Write our content to disk
|
||||
content = f.read().decode()
|
||||
|
||||
except OSError:
|
||||
# all none return means to let upstream know we had a hard failure
|
||||
return (None, None)
|
||||
|
||||
# return our read content
|
||||
return (content, fmt)
|
||||
|
||||
def clear(self, key, formats=None):
|
||||
"""
|
||||
Removes any content associated with the specified key should it
|
||||
exist.
|
||||
|
||||
None is returned if there was nothing to clear
|
||||
True is returned if content was cleared
|
||||
False is returned if an internal error prevented data from being
|
||||
cleared
|
||||
"""
|
||||
# Default our response None
|
||||
response = None
|
||||
|
||||
if formats is None:
|
||||
formats = apprise.CONFIG_FORMATS
|
||||
|
||||
path, filename = self.path(key)
|
||||
for fmt in formats:
|
||||
# Eliminate any existing content if present
|
||||
try:
|
||||
# Handle failure
|
||||
os.remove(os.path.join(path, '{}.{}'.format(filename, fmt)))
|
||||
|
||||
# If we reach here, an element was removed
|
||||
response = True
|
||||
|
||||
except OSError as e:
|
||||
if e.errno != errno.ENOENT:
|
||||
# We were unable to remove the file
|
||||
response = False
|
||||
|
||||
return response
|
||||
|
||||
def path(self, key):
|
||||
"""
|
||||
returns the path and filename content should be written to based on the
|
||||
specified key
|
||||
"""
|
||||
encoded_key = hashlib.sha224(self.salt + key.encode()).hexdigest()
|
||||
path = os.path.join(self.root, encoded_key[0:2])
|
||||
return (path, encoded_key[2:])
|
||||
|
||||
|
||||
# Initialize our singleton
|
||||
ConfigCache = AppriseConfigCache(
|
||||
settings.APPRISE_CONFIG_DIR, salt=settings.SECRET_KEY)
|
419
apprise_api/api/views.py
Normal file
419
apprise_api/api/views.py
Normal file
@@ -0,0 +1,419 @@
|
||||
# -*- 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.shortcuts import render
|
||||
from django.http import HttpResponse
|
||||
from django.views import View
|
||||
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 ugettext_lazy as _
|
||||
from .utils import ConfigCache
|
||||
from .forms import AddByUrlForm
|
||||
from .forms import AddByConfigForm
|
||||
from .forms import NotifyForm
|
||||
|
||||
from tempfile import NamedTemporaryFile
|
||||
import apprise
|
||||
import json
|
||||
import re
|
||||
|
||||
# Content-Type Parsing
|
||||
FORM_CTYPE_RE = re.compile('^(.*form-(data|urlencoded))$', re.I)
|
||||
JSON_CTYPE_RE = re.compile('^.*json$', re.I)
|
||||
|
||||
|
||||
class ResponseCode(object):
|
||||
"""
|
||||
These codes are based on those provided by the requests object
|
||||
"""
|
||||
okay = 200
|
||||
no_content = 204
|
||||
bad_request = 400
|
||||
not_found = 404
|
||||
method_not_allowed = 405
|
||||
internal_server_error = 500
|
||||
|
||||
|
||||
class WelcomeView(View):
|
||||
"""
|
||||
A simple welcome/index page
|
||||
"""
|
||||
template_name = 'welcome.html'
|
||||
|
||||
def get(self, request):
|
||||
return render(request, self.template_name, {})
|
||||
|
||||
|
||||
@method_decorator(never_cache, name='dispatch')
|
||||
class ConfigView(View):
|
||||
"""
|
||||
A Django view used to manage configuration
|
||||
"""
|
||||
template_name = 'config.html'
|
||||
|
||||
def get(self, request, key):
|
||||
"""
|
||||
Handle a GET request
|
||||
"""
|
||||
return render(request, self.template_name, {
|
||||
'key': key,
|
||||
'form_url': AddByUrlForm(),
|
||||
'form_cfg': AddByConfigForm(),
|
||||
'form_notify': NotifyForm(),
|
||||
})
|
||||
|
||||
|
||||
@method_decorator(never_cache, name='dispatch')
|
||||
class AddView(View):
|
||||
"""
|
||||
A Django view used to store Apprise configuration
|
||||
"""
|
||||
|
||||
def post(self, request, key):
|
||||
"""
|
||||
Handle a POST request
|
||||
"""
|
||||
# Our default response type
|
||||
content_type = 'text/plain'
|
||||
|
||||
# our content
|
||||
content = {}
|
||||
if FORM_CTYPE_RE.match(request.content_type):
|
||||
content = {}
|
||||
form = AddByConfigForm(request.POST)
|
||||
if form.is_valid():
|
||||
content.update(form.clean())
|
||||
|
||||
form = AddByUrlForm(request.POST)
|
||||
if form.is_valid():
|
||||
content.update(form.clean())
|
||||
|
||||
elif JSON_CTYPE_RE.match(request.content_type):
|
||||
# Prepare our default response
|
||||
try:
|
||||
# load our JSON content
|
||||
content = json.loads(request.body)
|
||||
|
||||
except (AttributeError, ValueError):
|
||||
# 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
|
||||
a_obj = apprise.Apprise()
|
||||
if 'urls' in content:
|
||||
# Load our content
|
||||
a_obj.add(content['urls'])
|
||||
if not len(a_obj):
|
||||
# No URLs were loaded
|
||||
return HttpResponse(
|
||||
_('No valid URLs were found.'),
|
||||
content_type=content_type,
|
||||
status=ResponseCode.bad_request)
|
||||
|
||||
if not ConfigCache.put(
|
||||
key, '\r\n'.join([s.url() for s in a_obj]),
|
||||
apprise.ConfigFormat.TEXT):
|
||||
|
||||
return HttpResponse(
|
||||
_('The configuration could not be saved.'),
|
||||
content_type=content_type,
|
||||
status=ResponseCode.internal_server_error,
|
||||
)
|
||||
|
||||
elif 'config' in content:
|
||||
fmt = content.get('format', '').lower()
|
||||
if fmt not in apprise.CONFIG_FORMATS:
|
||||
# Format must be one supported by apprise
|
||||
return HttpResponse(
|
||||
_('The format specified is invalid.'),
|
||||
content_type=content_type,
|
||||
status=ResponseCode.bad_request)
|
||||
|
||||
# prepare our apprise config object
|
||||
ac_obj = apprise.AppriseConfig()
|
||||
|
||||
try:
|
||||
# Write our file to a temporary file
|
||||
with NamedTemporaryFile() as f:
|
||||
# Write our content to disk
|
||||
f.write(content['config'].encode())
|
||||
f.flush()
|
||||
|
||||
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)
|
||||
|
||||
# Add our configuration
|
||||
a_obj.add(ac_obj)
|
||||
|
||||
if not len(a_obj):
|
||||
# No specified URL(s) were loaded due to
|
||||
# mis-configuration on the caller's part
|
||||
return HttpResponse(
|
||||
_('No valid URL(s) were specified.'),
|
||||
content_type=content_type,
|
||||
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)
|
||||
|
||||
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)
|
||||
|
||||
# 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)
|
||||
|
||||
|
||||
@method_decorator(never_cache, name='dispatch')
|
||||
class DelView(View):
|
||||
"""
|
||||
A Django view for removing content associated with a key
|
||||
"""
|
||||
def post(self, request, key):
|
||||
"""
|
||||
Handle a POST request
|
||||
"""
|
||||
# Our default response type
|
||||
content_type = 'text/plain'
|
||||
|
||||
# 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,
|
||||
)
|
||||
|
||||
elif result is False:
|
||||
# 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,
|
||||
)
|
||||
|
||||
|
||||
@method_decorator((gzip_page, never_cache), name='dispatch')
|
||||
class GetView(View):
|
||||
"""
|
||||
A Django view used to retrieve previously stored Apprise configuration
|
||||
"""
|
||||
def post(self, request, key):
|
||||
"""
|
||||
Handle a POST request
|
||||
"""
|
||||
# Our default response type
|
||||
content_type = 'text/plain'
|
||||
|
||||
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 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'
|
||||
|
||||
# Return our retrieved content
|
||||
return HttpResponse(
|
||||
config, content_type=content_type, status=ResponseCode.okay)
|
||||
|
||||
|
||||
@method_decorator((gzip_page, never_cache), name='dispatch')
|
||||
class NotifyView(View):
|
||||
"""
|
||||
A Django view for sending a notification
|
||||
"""
|
||||
def post(self, request, key):
|
||||
"""
|
||||
Handle a POST request
|
||||
"""
|
||||
# Our default response type
|
||||
content_type = 'text/plain'
|
||||
|
||||
# our content
|
||||
content = {}
|
||||
if FORM_CTYPE_RE.match(request.content_type):
|
||||
content = {}
|
||||
form = NotifyForm(request.POST)
|
||||
if form.is_valid():
|
||||
content.update(form.clean())
|
||||
|
||||
elif JSON_CTYPE_RE.match(request.content_type):
|
||||
# Prepare our default response
|
||||
try:
|
||||
# load our JSON content
|
||||
content = json.loads(request.body)
|
||||
|
||||
except (AttributeError, ValueError):
|
||||
# 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
|
||||
if not content.get('body') or \
|
||||
content.get('type', apprise.NotifyType.INFO) \
|
||||
not in apprise.NOTIFY_TYPES:
|
||||
|
||||
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
|
||||
# with.
|
||||
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 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,
|
||||
)
|
||||
|
||||
# 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)
|
||||
|
||||
# Perform our notification at this point
|
||||
a_obj.notify(
|
||||
content.get('body'),
|
||||
title=content.get('title', ''),
|
||||
notify_type=content.get('type', apprise.NotifyType.INFO),
|
||||
tag=content.get('tag'),
|
||||
)
|
||||
|
||||
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)
|
||||
|
||||
# Return our retrieved content
|
||||
return HttpResponse(
|
||||
_('Notification(s) sent.'),
|
||||
content_type=content_type, status=ResponseCode.okay)
|
Reference in New Issue
Block a user