mirror of
https://github.com/caronc/apprise-api.git
synced 2024-12-04 14:03:16 +01:00
Initial commit
This commit is contained in:
commit
4a8921abb8
2
.github/FUNDING.yml
vendored
Normal file
2
.github/FUNDING.yml
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
github: caronc
|
||||
custom: ['https://www.paypal.me/lead2gold', 'https://beerpay.io/caronc/apprise']
|
22
.github/ISSUE_TEMPLATE/1_bug_report.md
vendored
Normal file
22
.github/ISSUE_TEMPLATE/1_bug_report.md
vendored
Normal file
@ -0,0 +1,22 @@
|
||||
---
|
||||
name: 🐛 Bug Report
|
||||
about: Report any errors and problems
|
||||
title: ''
|
||||
labels: 'bug'
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
:beetle: **Describe the bug**
|
||||
<!-- A clear and concise description of the problem. -->
|
||||
|
||||
:bulb: **Screenshots and Logs**
|
||||
<!-- If applicable, add screenshots or share logs help explain your problem. -->
|
||||
<!-- be careful not to reveal anything personal in the logs such as a password or api key. -->
|
||||
|
||||
:computer: **Your System Details:**
|
||||
- OS: [e.g. RedHat v8.0]
|
||||
- Python Version: [e.g. Python v2.7]
|
||||
|
||||
:crystal_ball: **Additional context**
|
||||
<!-- Is the issue easy to reproduce? if so how? -->
|
15
.github/ISSUE_TEMPLATE/2_enhancement_request.md
vendored
Normal file
15
.github/ISSUE_TEMPLATE/2_enhancement_request.md
vendored
Normal file
@ -0,0 +1,15 @@
|
||||
---
|
||||
name: 💡 Enhancement Request
|
||||
about: Got a great idea? Let us know!
|
||||
title: ''
|
||||
labels: 'enhancement'
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
:bulb: **The Idea**
|
||||
<!-- Share your thoughts; try to be detailed if you can -->
|
||||
|
||||
:hammer: **Breaking Feature**
|
||||
<!-- Would your idea disrupt or drastically change the flow
|
||||
of Apprise or how it currently works? If so explain it here. -->
|
11
.github/ISSUE_TEMPLATE/3_question.md
vendored
Normal file
11
.github/ISSUE_TEMPLATE/3_question.md
vendored
Normal file
@ -0,0 +1,11 @@
|
||||
---
|
||||
name: ❓ Support Question
|
||||
about: Ask a question about Apprise
|
||||
title: ''
|
||||
labels: 'question'
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
:question: **Question**
|
||||
<!-- Go ahead and ask your question here :) -->
|
9
.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal file
9
.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal file
@ -0,0 +1,9 @@
|
||||
## Description:
|
||||
**Related issue (if applicable):** fixes #<!--apprise issue number goes here-->
|
||||
|
||||
## Checklist
|
||||
<!-- The following must be completed or your PR can't be merged -->
|
||||
* [ ] The code change is tested and works locally.
|
||||
* [ ] There is no commented out code in this PR.
|
||||
* [ ] No lint errors (use `flake8`)
|
||||
* [ ] tests added
|
70
.gitignore
vendored
Normal file
70
.gitignore
vendored
Normal file
@ -0,0 +1,70 @@
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
|
||||
# vi swap files
|
||||
.*.sw?
|
||||
|
||||
# Distribution / packaging / virtualenv
|
||||
.Python
|
||||
env/
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib64/
|
||||
lib/
|
||||
include/
|
||||
bin/
|
||||
parts/
|
||||
sdist/
|
||||
pyvenv.cfg
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
pip-selfcheck.json
|
||||
|
||||
# PyInstaller
|
||||
# Usually these files are written by a python script from a template
|
||||
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||
*.manifest
|
||||
*.spec
|
||||
|
||||
# Allow RPM SPEC files despite pyInstaller ignore
|
||||
!packaging/redhat/*.spec
|
||||
|
||||
# Installer logs
|
||||
pip-log.txt
|
||||
pip-delete-this-directory.txt
|
||||
|
||||
# Unit test / coverage reports
|
||||
htmlcov/
|
||||
.tox/
|
||||
.coverage
|
||||
.coverage.*
|
||||
.cache
|
||||
nosetests.xml
|
||||
coverage.xml
|
||||
*,cover
|
||||
.hypothesis/
|
||||
|
||||
# Translations
|
||||
*.mo
|
||||
|
||||
# Django stuff:
|
||||
*.log
|
||||
|
||||
# PyBuilder
|
||||
target/
|
||||
|
||||
#Ipython Notebook
|
||||
.ipynb_checkpoints
|
||||
|
||||
#PyCharm
|
||||
.idea
|
||||
|
||||
# Apprise Gateway Variable/Test Configuration
|
||||
apprise_gw/var/*
|
22
Dockerfile-gunicorn
Normal file
22
Dockerfile-gunicorn
Normal file
@ -0,0 +1,22 @@
|
||||
# pull official base image
|
||||
FROM python:3.8.0-alpine
|
||||
|
||||
# set work directory
|
||||
WORKDIR /opt/apprise
|
||||
|
||||
# set environment variables
|
||||
ENV PYTHONDONTWRITEBYTECODE 1
|
||||
ENV PYTHONUNBUFFERED 1
|
||||
ENV APPRISE_CONFIG_DIR /var/apprise/config
|
||||
|
||||
# install dependencies
|
||||
RUN pip install --upgrade pip
|
||||
COPY ./requirements.txt etc/requirements.txt
|
||||
RUN pip install -r etc/requirements.txt \
|
||||
gunicorn
|
||||
|
||||
# copy project
|
||||
COPY apprise_api/ webapp
|
||||
|
||||
EXPOSE 8000
|
||||
CMD ["gunicorn", "-c", "/opt/apprise/webapp/gunicorn.conf.py", "core.wsgi"]
|
9
Dockerfile-nginx
Normal file
9
Dockerfile-nginx
Normal file
@ -0,0 +1,9 @@
|
||||
# pull official base image
|
||||
FROM nginx
|
||||
|
||||
# Copy our customized NginX configuration (for container usage)
|
||||
COPY apprise_api/nginx.conf /etc/nginx/nginx.conf
|
||||
COPY apprise_api/apprise_api.conf /etc/nginx/conf.d/default.conf
|
||||
|
||||
# Copy our static content in place
|
||||
COPY apprise_api/static /usr/share/nginx/html/s/
|
21
LICENSE
Normal file
21
LICENSE
Normal file
@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2019 Chris Caron <lead2gold@gmail.com>
|
||||
|
||||
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.
|
150
README.md
Normal file
150
README.md
Normal file
@ -0,0 +1,150 @@
|
||||
# Apprise API
|
||||
|
||||
Take advantage of [Apprise](https://github.com/caronc/apprise) through your network with a user-friendly API.
|
||||
|
||||
- Send notifications to more then 50+ services.
|
||||
- An incredibly lightweight gateway to Apprise.
|
||||
- A production ready micro-service at your disposal.
|
||||
|
||||
Apprise API was designed easily fit into existing (and new) eco-systems that are looking for a simple notification solution.
|
||||
|
||||
[![Paypal](https://img.shields.io/badge/paypal-donate-green.svg)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=MHANV39UZNQ5E)
|
||||
[![Discord](https://img.shields.io/discord/558793703356104724.svg?colorB=7289DA&label=Discord&logo=Discord&logoColor=7289DA&style=flat-square)](https://discord.gg/MMPeN2D)
|
||||
|
||||
## API Details
|
||||
|
||||
| Path | Description |
|
||||
|------------- | ----------- |
|
||||
| `/add/{KEY}` | Saves Apprise Configuration (or set of URLs) to the persistent store.<br/>*Parameters*<br/>:small_red_triangle: **urls**: Used to define one or more Apprise URL(s). Use a comma and/or space to separate one URL from the next.<br/>:small_red_triangle: **config**: Provide the contents of either a YAML or TEXT based Apprise configuration.<br/>:small_red_triangle: **format**: This field is only required if you've specified the _config_ parameter. Used to tell the server which of the supported (Apprise) configuration types you are passing. Valid options are _text_ and _yaml_.
|
||||
| `/del/{KEY}` | Removes Apprise Configuration from the persistent store.
|
||||
| `/get/{KEY}` | Returns the Apprise Configuration from the persistent store. This can be directly used with the *Apprise CLI* and/or the *AppriseConfig()* object ([see here for details](https://github.com/caronc/apprise/wiki/config)).
|
||||
| `/notify/{KEY}` | Sends a notification based on the Apprise Configuration associated with the specified *{KEY}*.<br/>*Parameters*<br/>:small_red_triangle: **body**: Your message body. This is the *only* required field.<br/>:small_red_triangle: **title**: Optionally define a title to go along with the *body*.<br/>:small_red_triangle: **type**: Defines the message type you want to send as. The valid options are `info`, `success`, `warning`, and `error`. If no *type* is specified then `info` is the default value used.<br/>:small_red_triangle: **tag**: Optionally notify only those tagged accordingly.
|
||||
|
||||
### API Notes
|
||||
|
||||
- `{KEY}` must be 1-64 alphanumeric characters in length. In addition to this, the underscore (`_`) and dash (`-`) are also accepted.
|
||||
- You must `POST` to URLs defined above in order for them to respond.
|
||||
- Specify the `Content-Type` of `application/json` to use the JSON support.
|
||||
- There is no authentication required to use this API; this is by design. It's intention is to be a lightweight and fast micro-service parked behind the systems designed to handle security.
|
||||
- There are no persistent store dependencies for the purpose of simplicity. Configuration is hashed and written straight to disk.
|
||||
|
||||
### Environment Variables
|
||||
|
||||
The use of environment variables allow you to provide over-rides to default settings.
|
||||
|
||||
| Variable | Description |
|
||||
|--------------------- | ----------- |
|
||||
| `APPRISE_CONFIG_DIR` | Defines the persistent store location of all configuration files saved. By default:<br/> - Content is written to the `apprise_api/var/config` directory when just using the _Django_ `manage runserver` script.
|
||||
| `SECRET_KEY` | A Django variable acting as a *salt* for most things that require security. This API uses it for the hash sequences when writing the configuration files to disk.
|
||||
| `ALLOWED_HOSTS` | A list of strings representing the host/domain names that this API can serve. This is a security measure to prevent HTTP Host header attacks, which are possible even under many seemingly-safe web server configurations. By default this is set to `*` allowing any host. Use space to delimit more then one host.
|
||||
| `DEBUG` | This defaults to `False` however can be set to `True`if defined with a non-zero value (such as `1`).
|
||||
|
||||
|
||||
## Container Support
|
||||
|
||||
A `docker-compose.yml` file is already set up to grant you an instant production ready simulated environment:
|
||||
|
||||
```bash
|
||||
# Docker Compose
|
||||
docker-compose up
|
||||
```
|
||||
|
||||
You can now access the API at: `http://localhost:8000/` from your browser.
|
||||
|
||||
## Development Environment
|
||||
|
||||
The following should get you a working development environment to test with:
|
||||
|
||||
```bash
|
||||
# Create a virtual environment in the same directory you
|
||||
# cloned this repository to:
|
||||
python -m venv .
|
||||
|
||||
# Activate it now:
|
||||
. ./bin/activate
|
||||
|
||||
# install dependencies
|
||||
pip install -r dev-requirements.txt -r requirements.txt
|
||||
|
||||
# Run a dev server (debug mode):
|
||||
./manage.py runserver
|
||||
```
|
||||
|
||||
You can now access the API at: `http://localhost:8000/` from your browser.
|
||||
|
||||
Some other useful development notes:
|
||||
|
||||
```bash
|
||||
# Check for any lint errors
|
||||
flake8 apprise_api
|
||||
|
||||
# Run unit tests
|
||||
pytest apprise_api
|
||||
```
|
||||
|
||||
## Micro-Service Integration
|
||||
|
||||
Perhaps you run your own service and the only goal you have is to add notification support to it. Here is an example:
|
||||
```python
|
||||
import requests
|
||||
import os
|
||||
|
||||
# Get your URLs from your end users. They just need
|
||||
# to be a comma/space separated list (if there is more than one).
|
||||
# Perhaps they're located in an environment variable:
|
||||
urls = os.environ.get('NOTIFICATION_URLS', 'windows://')
|
||||
if urls:
|
||||
# Think of a key that best describes your purpose and/or program.
|
||||
# Alternatively; you can make the key based on the users so they
|
||||
# can each store their configuration.
|
||||
key = 'my-program-name'
|
||||
|
||||
# POST our data:
|
||||
requests.post(
|
||||
'http://localhost:8000/add/{}'.format(key),
|
||||
data={'urls': urls},
|
||||
)
|
||||
```
|
||||
|
||||
Now when you want to trigger a notification (sent from the Apprise API server), just do the following:
|
||||
|
||||
```python
|
||||
# The minimum notify requirements are to have just provided the 'body':
|
||||
requests.post(
|
||||
'http://localhost:8000/notify/{}'.format(key),
|
||||
data={'body': 'test message'},
|
||||
)
|
||||
```
|
||||
|
||||
## Apprise Integration
|
||||
|
||||
### Apprise CLI Pull Example
|
||||
|
||||
A scenario where you want to poll the API for your configuration:
|
||||
|
||||
```bash
|
||||
# A simple example of the Apprise CLI
|
||||
# pulling down previously stored configuration
|
||||
apprise -body="test message" --config=http://localhost:8000/get/{KEY}
|
||||
```
|
||||
|
||||
### AppriseConfig() Pull Example
|
||||
|
||||
Using the Apprise Library through Python, you can easily pull your saved configuration off of the API to use for future notifications.
|
||||
|
||||
```python
|
||||
import apprise
|
||||
|
||||
# Point our configuration to this API server:
|
||||
config = apprise.AppriseConfig()
|
||||
config.add('http://localhost:8000/get/{KEY}')
|
||||
|
||||
# Create our Apprise Instance
|
||||
a = apprise.Apprise()
|
||||
|
||||
# Store our new configuration
|
||||
a.add(config)
|
||||
|
||||
# Send a test message
|
||||
a.notify('test message')
|
||||
```
|
10
apprise_api/.coveragerc
Normal file
10
apprise_api/.coveragerc
Normal file
@ -0,0 +1,10 @@
|
||||
[run]
|
||||
omit =
|
||||
*apps.py,
|
||||
*migrations/*,
|
||||
*settings*,
|
||||
*tests/*,
|
||||
*urls.py,
|
||||
*wsgi/*,
|
||||
gunicorn.conf.py,
|
||||
manage.py
|
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)
|
36
apprise_api/apprise_api.conf
Normal file
36
apprise_api/apprise_api.conf
Normal file
@ -0,0 +1,36 @@
|
||||
|
||||
# The backend API server
|
||||
upstream apprise_backend {
|
||||
server backend:8000 fail_timeout=0;
|
||||
}
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
server_name _;
|
||||
charset utf-8;
|
||||
|
||||
# Main Website
|
||||
location / {
|
||||
include uwsgi_params;
|
||||
proxy_set_header Host $http_host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_pass http://apprise_backend;
|
||||
}
|
||||
|
||||
# Static Content
|
||||
location /s/ {
|
||||
root /usr/share/nginx/html;
|
||||
index index.html;
|
||||
}
|
||||
|
||||
# 404 error handling
|
||||
error_page 404 /404.html;
|
||||
|
||||
# redirect server error pages to the static page /50x.html
|
||||
error_page 500 502 503 504 /50x.html;
|
||||
location = /50x.html {
|
||||
root /usr/share/nginx/html;
|
||||
}
|
||||
}
|
0
apprise_api/core/__init__.py
Normal file
0
apprise_api/core/__init__.py
Normal file
85
apprise_api/core/settings/__init__.py
Normal file
85
apprise_api/core/settings/__init__.py
Normal file
@ -0,0 +1,85 @@
|
||||
# -*- 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
|
||||
|
||||
# Base Directory (relative to settings)
|
||||
BASE_DIR = os.path.dirname(
|
||||
os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
|
||||
# SECURITY WARNING: keep the secret key used in production secret!
|
||||
SECRET_KEY = os.environ.get(
|
||||
'SECRET_KEY', '+reua88v8rs4j!bcfdtinb-f0edxazf!$x_q1g7jtgckxd7gi=')
|
||||
|
||||
# SECURITY WARNING: don't run with debug turned on in production!
|
||||
# If you want to run this app in DEBUG mode, run the following:
|
||||
#
|
||||
# ./manage.py runserver --settings=core.settings.debug
|
||||
#
|
||||
# Or alternatively run:
|
||||
#
|
||||
# export DJANGO_SETTINGS_MODULE=core.settings.debug
|
||||
# ./manage.py runserver
|
||||
DEBUG = bool(os.environ.get("DEBUG", False))
|
||||
|
||||
# allow all hosts by default otherwise read from the
|
||||
# ALLOWED_HOSTS environment variable
|
||||
ALLOWED_HOSTS = os.environ.get('ALLOWED_HOSTS', '*').split(' ')
|
||||
|
||||
# Application definition
|
||||
INSTALLED_APPS = [
|
||||
'django.contrib.contenttypes',
|
||||
'django.contrib.staticfiles',
|
||||
|
||||
# Apprise API
|
||||
'api',
|
||||
]
|
||||
|
||||
MIDDLEWARE = [
|
||||
'django.middleware.common.CommonMiddleware',
|
||||
]
|
||||
|
||||
ROOT_URLCONF = 'core.urls'
|
||||
|
||||
TEMPLATES = [
|
||||
{
|
||||
'BACKEND': 'django.template.backends.django.DjangoTemplates',
|
||||
'DIRS': [],
|
||||
'APP_DIRS': True,
|
||||
'OPTIONS': {
|
||||
'context_processors': [
|
||||
'django.template.context_processors.request',
|
||||
],
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
WSGI_APPLICATION = 'core.wsgi.application'
|
||||
|
||||
# Static files relative path (CSS, JavaScript, Images)
|
||||
STATIC_URL = '/s/'
|
||||
|
||||
# The location to store Apprise configuration files
|
||||
APPRISE_CONFIG_DIR = os.environ.get(
|
||||
'APPRISE_CONFIG_DIR', os.path.join(BASE_DIR, 'var', 'config'))
|
43
apprise_api/core/settings/debug/__init__.py
Normal file
43
apprise_api/core/settings/debug/__init__.py
Normal file
@ -0,0 +1,43 @@
|
||||
# -*- 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.
|
||||
|
||||
# To create a valid debug settings.py we need to intentionally pollute our
|
||||
# file with all of the content found in the master configuration.
|
||||
import os
|
||||
from .. import * # noqa F403
|
||||
|
||||
# Debug is always on when running in debug mode
|
||||
DEBUG = True
|
||||
|
||||
# Allowed hosts is not required in debug mode
|
||||
ALLOWED_HOSTS = []
|
||||
|
||||
# Over-ride the default URLConf for debugging
|
||||
ROOT_URLCONF = 'core.settings.debug.urls'
|
||||
|
||||
# Our static paths directory for serving
|
||||
STATICFILES_DIRS = (
|
||||
os.path.join(BASE_DIR, 'static'), # noqa F405
|
||||
)
|
30
apprise_api/core/settings/debug/urls.py
Normal file
30
apprise_api/core/settings/debug/urls.py
Normal file
@ -0,0 +1,30 @@
|
||||
# -*- 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.conf import settings
|
||||
from django.conf.urls.static import static
|
||||
from ...urls import * # noqa F403
|
||||
|
||||
# Extend our patterns
|
||||
urlpatterns += static(settings.STATIC_URL) # noqa F405
|
41
apprise_api/core/settings/pytest/__init__.py
Normal file
41
apprise_api/core/settings/pytest/__init__.py
Normal file
@ -0,0 +1,41 @@
|
||||
# -*- 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.
|
||||
|
||||
# To create a valid debug settings.py we need to intentionally pollute our
|
||||
# file with all of the content found in the master configuration.
|
||||
from tempfile import TemporaryDirectory
|
||||
from .. import * # noqa F403
|
||||
|
||||
# Debug is always on when running in debug mode
|
||||
DEBUG = True
|
||||
|
||||
# Allowed hosts is not required in debug mode
|
||||
ALLOWED_HOSTS = []
|
||||
|
||||
# A temporary directory to work in for unit testing
|
||||
APPRISE_CONFIG_DIR = TemporaryDirectory().name
|
||||
|
||||
# Setup our runner
|
||||
TEST_RUNNER = 'core.settings.pytest.runner.PytestTestRunner'
|
59
apprise_api/core/settings/pytest/runner.py
Normal file
59
apprise_api/core/settings/pytest/runner.py
Normal file
@ -0,0 +1,59 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# -*- 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.
|
||||
|
||||
# To create a valid debug settings.py we need to intentionally pollute our
|
||||
# file with all of the content found in the master configuration.
|
||||
|
||||
|
||||
class PytestTestRunner(object):
|
||||
"""Runs pytest to discover and run tests."""
|
||||
|
||||
def __init__(self, verbosity=1, failfast=False, keepdb=False, **kwargs):
|
||||
self.verbosity = verbosity
|
||||
self.failfast = failfast
|
||||
self.keepdb = keepdb
|
||||
|
||||
def run_tests(self, test_labels):
|
||||
"""Run pytest and return the exitcode.
|
||||
|
||||
It translates some of Django's test command option to pytest's.
|
||||
"""
|
||||
import pytest
|
||||
|
||||
argv = []
|
||||
if self.verbosity == 0:
|
||||
argv.append('--quiet')
|
||||
if self.verbosity == 2:
|
||||
argv.append('--verbose')
|
||||
if self.verbosity == 3:
|
||||
argv.append('-vv')
|
||||
if self.failfast:
|
||||
argv.append('--exitfirst')
|
||||
if self.keepdb:
|
||||
argv.append('--reuse-db')
|
||||
|
||||
argv.extend(test_labels)
|
||||
return pytest.main(argv)
|
32
apprise_api/core/urls.py
Normal file
32
apprise_api/core/urls.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.urls import path
|
||||
from django.conf.urls import include
|
||||
|
||||
from api import urls as api_urls
|
||||
|
||||
urlpatterns = [
|
||||
path('', include(api_urls)),
|
||||
]
|
29
apprise_api/core/wsgi.py
Normal file
29
apprise_api/core/wsgi.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.
|
||||
import os
|
||||
from django.core.wsgi import get_wsgi_application
|
||||
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'core.settings')
|
||||
application = get_wsgi_application()
|
50
apprise_api/gunicorn.conf.py
Normal file
50
apprise_api/gunicorn.conf.py
Normal file
@ -0,0 +1,50 @@
|
||||
# -*- 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 multiprocessing
|
||||
|
||||
# This file is launched with the call:
|
||||
# gunicorn --config <this file> core.wsgi:application
|
||||
|
||||
raw_env = [
|
||||
'LANG=en_US.UTF-8',
|
||||
'DJANGO_SETTINGS_MODULE=core.settings',
|
||||
]
|
||||
|
||||
# This is the path as prepared in the docker compose
|
||||
pythonpath = '/opt/apprise/webapp'
|
||||
|
||||
# bind to port 8000
|
||||
bind = [
|
||||
'0.0.0.0:8000',
|
||||
]
|
||||
|
||||
# Workers are relative to the number of CPU's provided by hosting server
|
||||
workers = multiprocessing.cpu_count() * 2 + 1
|
||||
|
||||
# Logging
|
||||
# '-' means log to stdout.
|
||||
errorlog = '-'
|
||||
accesslog = '-'
|
||||
loglevel = 'warn'
|
44
apprise_api/manage.py
Executable file
44
apprise_api/manage.py
Executable file
@ -0,0 +1,44 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- 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 sys
|
||||
|
||||
|
||||
def main():
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'core.settings.debug')
|
||||
try:
|
||||
from django.core.management import execute_from_command_line
|
||||
except ImportError as exc:
|
||||
raise ImportError(
|
||||
"Couldn't import Django. Are you sure it's installed and "
|
||||
"available on your PYTHONPATH environment variable? Did you "
|
||||
"forget to activate a virtual environment?"
|
||||
) from exc
|
||||
execute_from_command_line(sys.argv)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
31
apprise_api/nginx.conf
Normal file
31
apprise_api/nginx.conf
Normal file
@ -0,0 +1,31 @@
|
||||
#
|
||||
# Apprise Docker NginX Configuration
|
||||
#
|
||||
error_log /dev/stdout info;
|
||||
|
||||
user nginx;
|
||||
worker_processes 1;
|
||||
pid /var/run/nginx.pid;
|
||||
|
||||
|
||||
events {
|
||||
worker_connections 1024;
|
||||
}
|
||||
|
||||
http {
|
||||
include /etc/nginx/mime.types;
|
||||
default_type application/octet-stream;
|
||||
|
||||
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
|
||||
'$status $body_bytes_sent "$http_referer" '
|
||||
'"$http_user_agent" "$http_x_forwarded_for"';
|
||||
|
||||
access_log /dev/stdout main;
|
||||
sendfile on;
|
||||
#tcp_nopush on;
|
||||
|
||||
keepalive_timeout 65;
|
||||
gzip on;
|
||||
|
||||
include /etc/nginx/conf.d/*.conf;
|
||||
}
|
4
apprise_api/pytest.ini
Normal file
4
apprise_api/pytest.ini
Normal file
@ -0,0 +1,4 @@
|
||||
[pytest]
|
||||
DJANGO_SETTINGS_MODULE = core.settings.pytest
|
||||
addopts = --nomigrations --cov=. --cov-report=html
|
||||
python_files = tests.py test_*.py *_tests.py
|
61
apprise_api/static/css/base.css
Normal file
61
apprise_api/static/css/base.css
Normal file
@ -0,0 +1,61 @@
|
||||
.nav h1 {
|
||||
margin: 0;
|
||||
font-size: 3.1rem;
|
||||
font-weight: bold;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
code {
|
||||
background-color: #eee;
|
||||
font-family: monospace;
|
||||
white-space: normal;
|
||||
padding: 0.2rem;
|
||||
}
|
||||
|
||||
table code {
|
||||
background-color: inherit;
|
||||
}
|
||||
|
||||
h1, h2, h3, h4, h5 {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
td, th {
|
||||
vertical-align: top;
|
||||
padding-top: 0;
|
||||
}
|
||||
.api-details ol {
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
em {
|
||||
color: #004d40;
|
||||
font-weight:bold;
|
||||
}
|
||||
|
||||
textarea {
|
||||
height: 16rem;
|
||||
}
|
||||
|
||||
.collapsible-body {
|
||||
padding: 1rem 2rem;
|
||||
}
|
||||
|
||||
.tabs .tab a{
|
||||
color:#2bbbad;
|
||||
}
|
||||
.collection a.collection-item:not(.active):hover,
|
||||
.tabs .tab a:focus, .tabs .tab a:focus.active {
|
||||
background-color: #eee;
|
||||
}
|
||||
.tabs .tab a:hover,.tabs .tab a.active {
|
||||
background-color:transparent;
|
||||
color:#2bbbad;
|
||||
}
|
||||
.tabs .tab.disabled a,.tabs .tab.disabled a:hover {
|
||||
color:rgba(102,147,153,0.7);
|
||||
}
|
||||
.tabs .indicator {
|
||||
background-color:#004d40;
|
||||
}
|
9067
apprise_api/static/css/materialize.css
vendored
Normal file
9067
apprise_api/static/css/materialize.css
vendored
Normal file
File diff suppressed because it is too large
Load Diff
13
apprise_api/static/css/materialize.min.css
vendored
Normal file
13
apprise_api/static/css/materialize.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
BIN
apprise_api/static/favicon.ico
Normal file
BIN
apprise_api/static/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 6.6 KiB |
BIN
apprise_api/static/iconfont/MaterialIcons-Regular.eot
Normal file
BIN
apprise_api/static/iconfont/MaterialIcons-Regular.eot
Normal file
Binary file not shown.
1
apprise_api/static/iconfont/MaterialIcons-Regular.ijmap
Normal file
1
apprise_api/static/iconfont/MaterialIcons-Regular.ijmap
Normal file
File diff suppressed because one or more lines are too long
2373
apprise_api/static/iconfont/MaterialIcons-Regular.svg
Normal file
2373
apprise_api/static/iconfont/MaterialIcons-Regular.svg
Normal file
File diff suppressed because it is too large
Load Diff
After Width: | Height: | Size: 275 KiB |
BIN
apprise_api/static/iconfont/MaterialIcons-Regular.ttf
Normal file
BIN
apprise_api/static/iconfont/MaterialIcons-Regular.ttf
Normal file
Binary file not shown.
BIN
apprise_api/static/iconfont/MaterialIcons-Regular.woff
Normal file
BIN
apprise_api/static/iconfont/MaterialIcons-Regular.woff
Normal file
Binary file not shown.
BIN
apprise_api/static/iconfont/MaterialIcons-Regular.woff2
Normal file
BIN
apprise_api/static/iconfont/MaterialIcons-Regular.woff2
Normal file
Binary file not shown.
9
apprise_api/static/iconfont/README.md
Normal file
9
apprise_api/static/iconfont/README.md
Normal file
@ -0,0 +1,9 @@
|
||||
The recommended way to use the Material Icons font is by linking to the web font hosted on Google Fonts:
|
||||
|
||||
```html
|
||||
<link href="https://fonts.googleapis.com/icon?family=Material+Icons"
|
||||
rel="stylesheet">
|
||||
```
|
||||
|
||||
Read more in our full usage guide:
|
||||
http://google.github.io/material-design-icons/#icon-font-for-the-web
|
932
apprise_api/static/iconfont/codepoints
Normal file
932
apprise_api/static/iconfont/codepoints
Normal file
@ -0,0 +1,932 @@
|
||||
3d_rotation e84d
|
||||
ac_unit eb3b
|
||||
access_alarm e190
|
||||
access_alarms e191
|
||||
access_time e192
|
||||
accessibility e84e
|
||||
accessible e914
|
||||
account_balance e84f
|
||||
account_balance_wallet e850
|
||||
account_box e851
|
||||
account_circle e853
|
||||
adb e60e
|
||||
add e145
|
||||
add_a_photo e439
|
||||
add_alarm e193
|
||||
add_alert e003
|
||||
add_box e146
|
||||
add_circle e147
|
||||
add_circle_outline e148
|
||||
add_location e567
|
||||
add_shopping_cart e854
|
||||
add_to_photos e39d
|
||||
add_to_queue e05c
|
||||
adjust e39e
|
||||
airline_seat_flat e630
|
||||
airline_seat_flat_angled e631
|
||||
airline_seat_individual_suite e632
|
||||
airline_seat_legroom_extra e633
|
||||
airline_seat_legroom_normal e634
|
||||
airline_seat_legroom_reduced e635
|
||||
airline_seat_recline_extra e636
|
||||
airline_seat_recline_normal e637
|
||||
airplanemode_active e195
|
||||
airplanemode_inactive e194
|
||||
airplay e055
|
||||
airport_shuttle eb3c
|
||||
alarm e855
|
||||
alarm_add e856
|
||||
alarm_off e857
|
||||
alarm_on e858
|
||||
album e019
|
||||
all_inclusive eb3d
|
||||
all_out e90b
|
||||
android e859
|
||||
announcement e85a
|
||||
apps e5c3
|
||||
archive e149
|
||||
arrow_back e5c4
|
||||
arrow_downward e5db
|
||||
arrow_drop_down e5c5
|
||||
arrow_drop_down_circle e5c6
|
||||
arrow_drop_up e5c7
|
||||
arrow_forward e5c8
|
||||
arrow_upward e5d8
|
||||
art_track e060
|
||||
aspect_ratio e85b
|
||||
assessment e85c
|
||||
assignment e85d
|
||||
assignment_ind e85e
|
||||
assignment_late e85f
|
||||
assignment_return e860
|
||||
assignment_returned e861
|
||||
assignment_turned_in e862
|
||||
assistant e39f
|
||||
assistant_photo e3a0
|
||||
attach_file e226
|
||||
attach_money e227
|
||||
attachment e2bc
|
||||
audiotrack e3a1
|
||||
autorenew e863
|
||||
av_timer e01b
|
||||
backspace e14a
|
||||
backup e864
|
||||
battery_alert e19c
|
||||
battery_charging_full e1a3
|
||||
battery_full e1a4
|
||||
battery_std e1a5
|
||||
battery_unknown e1a6
|
||||
beach_access eb3e
|
||||
beenhere e52d
|
||||
block e14b
|
||||
bluetooth e1a7
|
||||
bluetooth_audio e60f
|
||||
bluetooth_connected e1a8
|
||||
bluetooth_disabled e1a9
|
||||
bluetooth_searching e1aa
|
||||
blur_circular e3a2
|
||||
blur_linear e3a3
|
||||
blur_off e3a4
|
||||
blur_on e3a5
|
||||
book e865
|
||||
bookmark e866
|
||||
bookmark_border e867
|
||||
border_all e228
|
||||
border_bottom e229
|
||||
border_clear e22a
|
||||
border_color e22b
|
||||
border_horizontal e22c
|
||||
border_inner e22d
|
||||
border_left e22e
|
||||
border_outer e22f
|
||||
border_right e230
|
||||
border_style e231
|
||||
border_top e232
|
||||
border_vertical e233
|
||||
branding_watermark e06b
|
||||
brightness_1 e3a6
|
||||
brightness_2 e3a7
|
||||
brightness_3 e3a8
|
||||
brightness_4 e3a9
|
||||
brightness_5 e3aa
|
||||
brightness_6 e3ab
|
||||
brightness_7 e3ac
|
||||
brightness_auto e1ab
|
||||
brightness_high e1ac
|
||||
brightness_low e1ad
|
||||
brightness_medium e1ae
|
||||
broken_image e3ad
|
||||
brush e3ae
|
||||
bubble_chart e6dd
|
||||
bug_report e868
|
||||
build e869
|
||||
burst_mode e43c
|
||||
business e0af
|
||||
business_center eb3f
|
||||
cached e86a
|
||||
cake e7e9
|
||||
call e0b0
|
||||
call_end e0b1
|
||||
call_made e0b2
|
||||
call_merge e0b3
|
||||
call_missed e0b4
|
||||
call_missed_outgoing e0e4
|
||||
call_received e0b5
|
||||
call_split e0b6
|
||||
call_to_action e06c
|
||||
camera e3af
|
||||
camera_alt e3b0
|
||||
camera_enhance e8fc
|
||||
camera_front e3b1
|
||||
camera_rear e3b2
|
||||
camera_roll e3b3
|
||||
cancel e5c9
|
||||
card_giftcard e8f6
|
||||
card_membership e8f7
|
||||
card_travel e8f8
|
||||
casino eb40
|
||||
cast e307
|
||||
cast_connected e308
|
||||
center_focus_strong e3b4
|
||||
center_focus_weak e3b5
|
||||
change_history e86b
|
||||
chat e0b7
|
||||
chat_bubble e0ca
|
||||
chat_bubble_outline e0cb
|
||||
check e5ca
|
||||
check_box e834
|
||||
check_box_outline_blank e835
|
||||
check_circle e86c
|
||||
chevron_left e5cb
|
||||
chevron_right e5cc
|
||||
child_care eb41
|
||||
child_friendly eb42
|
||||
chrome_reader_mode e86d
|
||||
class e86e
|
||||
clear e14c
|
||||
clear_all e0b8
|
||||
close e5cd
|
||||
closed_caption e01c
|
||||
cloud e2bd
|
||||
cloud_circle e2be
|
||||
cloud_done e2bf
|
||||
cloud_download e2c0
|
||||
cloud_off e2c1
|
||||
cloud_queue e2c2
|
||||
cloud_upload e2c3
|
||||
code e86f
|
||||
collections e3b6
|
||||
collections_bookmark e431
|
||||
color_lens e3b7
|
||||
colorize e3b8
|
||||
comment e0b9
|
||||
compare e3b9
|
||||
compare_arrows e915
|
||||
computer e30a
|
||||
confirmation_number e638
|
||||
contact_mail e0d0
|
||||
contact_phone e0cf
|
||||
contacts e0ba
|
||||
content_copy e14d
|
||||
content_cut e14e
|
||||
content_paste e14f
|
||||
control_point e3ba
|
||||
control_point_duplicate e3bb
|
||||
copyright e90c
|
||||
create e150
|
||||
create_new_folder e2cc
|
||||
credit_card e870
|
||||
crop e3be
|
||||
crop_16_9 e3bc
|
||||
crop_3_2 e3bd
|
||||
crop_5_4 e3bf
|
||||
crop_7_5 e3c0
|
||||
crop_din e3c1
|
||||
crop_free e3c2
|
||||
crop_landscape e3c3
|
||||
crop_original e3c4
|
||||
crop_portrait e3c5
|
||||
crop_rotate e437
|
||||
crop_square e3c6
|
||||
dashboard e871
|
||||
data_usage e1af
|
||||
date_range e916
|
||||
dehaze e3c7
|
||||
delete e872
|
||||
delete_forever e92b
|
||||
delete_sweep e16c
|
||||
description e873
|
||||
desktop_mac e30b
|
||||
desktop_windows e30c
|
||||
details e3c8
|
||||
developer_board e30d
|
||||
developer_mode e1b0
|
||||
device_hub e335
|
||||
devices e1b1
|
||||
devices_other e337
|
||||
dialer_sip e0bb
|
||||
dialpad e0bc
|
||||
directions e52e
|
||||
directions_bike e52f
|
||||
directions_boat e532
|
||||
directions_bus e530
|
||||
directions_car e531
|
||||
directions_railway e534
|
||||
directions_run e566
|
||||
directions_subway e533
|
||||
directions_transit e535
|
||||
directions_walk e536
|
||||
disc_full e610
|
||||
dns e875
|
||||
do_not_disturb e612
|
||||
do_not_disturb_alt e611
|
||||
do_not_disturb_off e643
|
||||
do_not_disturb_on e644
|
||||
dock e30e
|
||||
domain e7ee
|
||||
done e876
|
||||
done_all e877
|
||||
donut_large e917
|
||||
donut_small e918
|
||||
drafts e151
|
||||
drag_handle e25d
|
||||
drive_eta e613
|
||||
dvr e1b2
|
||||
edit e3c9
|
||||
edit_location e568
|
||||
eject e8fb
|
||||
email e0be
|
||||
enhanced_encryption e63f
|
||||
equalizer e01d
|
||||
error e000
|
||||
error_outline e001
|
||||
euro_symbol e926
|
||||
ev_station e56d
|
||||
event e878
|
||||
event_available e614
|
||||
event_busy e615
|
||||
event_note e616
|
||||
event_seat e903
|
||||
exit_to_app e879
|
||||
expand_less e5ce
|
||||
expand_more e5cf
|
||||
explicit e01e
|
||||
explore e87a
|
||||
exposure e3ca
|
||||
exposure_neg_1 e3cb
|
||||
exposure_neg_2 e3cc
|
||||
exposure_plus_1 e3cd
|
||||
exposure_plus_2 e3ce
|
||||
exposure_zero e3cf
|
||||
extension e87b
|
||||
face e87c
|
||||
fast_forward e01f
|
||||
fast_rewind e020
|
||||
favorite e87d
|
||||
favorite_border e87e
|
||||
featured_play_list e06d
|
||||
featured_video e06e
|
||||
feedback e87f
|
||||
fiber_dvr e05d
|
||||
fiber_manual_record e061
|
||||
fiber_new e05e
|
||||
fiber_pin e06a
|
||||
fiber_smart_record e062
|
||||
file_download e2c4
|
||||
file_upload e2c6
|
||||
filter e3d3
|
||||
filter_1 e3d0
|
||||
filter_2 e3d1
|
||||
filter_3 e3d2
|
||||
filter_4 e3d4
|
||||
filter_5 e3d5
|
||||
filter_6 e3d6
|
||||
filter_7 e3d7
|
||||
filter_8 e3d8
|
||||
filter_9 e3d9
|
||||
filter_9_plus e3da
|
||||
filter_b_and_w e3db
|
||||
filter_center_focus e3dc
|
||||
filter_drama e3dd
|
||||
filter_frames e3de
|
||||
filter_hdr e3df
|
||||
filter_list e152
|
||||
filter_none e3e0
|
||||
filter_tilt_shift e3e2
|
||||
filter_vintage e3e3
|
||||
find_in_page e880
|
||||
find_replace e881
|
||||
fingerprint e90d
|
||||
first_page e5dc
|
||||
fitness_center eb43
|
||||
flag e153
|
||||
flare e3e4
|
||||
flash_auto e3e5
|
||||
flash_off e3e6
|
||||
flash_on e3e7
|
||||
flight e539
|
||||
flight_land e904
|
||||
flight_takeoff e905
|
||||
flip e3e8
|
||||
flip_to_back e882
|
||||
flip_to_front e883
|
||||
folder e2c7
|
||||
folder_open e2c8
|
||||
folder_shared e2c9
|
||||
folder_special e617
|
||||
font_download e167
|
||||
format_align_center e234
|
||||
format_align_justify e235
|
||||
format_align_left e236
|
||||
format_align_right e237
|
||||
format_bold e238
|
||||
format_clear e239
|
||||
format_color_fill e23a
|
||||
format_color_reset e23b
|
||||
format_color_text e23c
|
||||
format_indent_decrease e23d
|
||||
format_indent_increase e23e
|
||||
format_italic e23f
|
||||
format_line_spacing e240
|
||||
format_list_bulleted e241
|
||||
format_list_numbered e242
|
||||
format_paint e243
|
||||
format_quote e244
|
||||
format_shapes e25e
|
||||
format_size e245
|
||||
format_strikethrough e246
|
||||
format_textdirection_l_to_r e247
|
||||
format_textdirection_r_to_l e248
|
||||
format_underlined e249
|
||||
forum e0bf
|
||||
forward e154
|
||||
forward_10 e056
|
||||
forward_30 e057
|
||||
forward_5 e058
|
||||
free_breakfast eb44
|
||||
fullscreen e5d0
|
||||
fullscreen_exit e5d1
|
||||
functions e24a
|
||||
g_translate e927
|
||||
gamepad e30f
|
||||
games e021
|
||||
gavel e90e
|
||||
gesture e155
|
||||
get_app e884
|
||||
gif e908
|
||||
golf_course eb45
|
||||
gps_fixed e1b3
|
||||
gps_not_fixed e1b4
|
||||
gps_off e1b5
|
||||
grade e885
|
||||
gradient e3e9
|
||||
grain e3ea
|
||||
graphic_eq e1b8
|
||||
grid_off e3eb
|
||||
grid_on e3ec
|
||||
group e7ef
|
||||
group_add e7f0
|
||||
group_work e886
|
||||
hd e052
|
||||
hdr_off e3ed
|
||||
hdr_on e3ee
|
||||
hdr_strong e3f1
|
||||
hdr_weak e3f2
|
||||
headset e310
|
||||
headset_mic e311
|
||||
healing e3f3
|
||||
hearing e023
|
||||
help e887
|
||||
help_outline e8fd
|
||||
high_quality e024
|
||||
highlight e25f
|
||||
highlight_off e888
|
||||
history e889
|
||||
home e88a
|
||||
hot_tub eb46
|
||||
hotel e53a
|
||||
hourglass_empty e88b
|
||||
hourglass_full e88c
|
||||
http e902
|
||||
https e88d
|
||||
image e3f4
|
||||
image_aspect_ratio e3f5
|
||||
import_contacts e0e0
|
||||
import_export e0c3
|
||||
important_devices e912
|
||||
inbox e156
|
||||
indeterminate_check_box e909
|
||||
info e88e
|
||||
info_outline e88f
|
||||
input e890
|
||||
insert_chart e24b
|
||||
insert_comment e24c
|
||||
insert_drive_file e24d
|
||||
insert_emoticon e24e
|
||||
insert_invitation e24f
|
||||
insert_link e250
|
||||
insert_photo e251
|
||||
invert_colors e891
|
||||
invert_colors_off e0c4
|
||||
iso e3f6
|
||||
keyboard e312
|
||||
keyboard_arrow_down e313
|
||||
keyboard_arrow_left e314
|
||||
keyboard_arrow_right e315
|
||||
keyboard_arrow_up e316
|
||||
keyboard_backspace e317
|
||||
keyboard_capslock e318
|
||||
keyboard_hide e31a
|
||||
keyboard_return e31b
|
||||
keyboard_tab e31c
|
||||
keyboard_voice e31d
|
||||
kitchen eb47
|
||||
label e892
|
||||
label_outline e893
|
||||
landscape e3f7
|
||||
language e894
|
||||
laptop e31e
|
||||
laptop_chromebook e31f
|
||||
laptop_mac e320
|
||||
laptop_windows e321
|
||||
last_page e5dd
|
||||
launch e895
|
||||
layers e53b
|
||||
layers_clear e53c
|
||||
leak_add e3f8
|
||||
leak_remove e3f9
|
||||
lens e3fa
|
||||
library_add e02e
|
||||
library_books e02f
|
||||
library_music e030
|
||||
lightbulb_outline e90f
|
||||
line_style e919
|
||||
line_weight e91a
|
||||
linear_scale e260
|
||||
link e157
|
||||
linked_camera e438
|
||||
list e896
|
||||
live_help e0c6
|
||||
live_tv e639
|
||||
local_activity e53f
|
||||
local_airport e53d
|
||||
local_atm e53e
|
||||
local_bar e540
|
||||
local_cafe e541
|
||||
local_car_wash e542
|
||||
local_convenience_store e543
|
||||
local_dining e556
|
||||
local_drink e544
|
||||
local_florist e545
|
||||
local_gas_station e546
|
||||
local_grocery_store e547
|
||||
local_hospital e548
|
||||
local_hotel e549
|
||||
local_laundry_service e54a
|
||||
local_library e54b
|
||||
local_mall e54c
|
||||
local_movies e54d
|
||||
local_offer e54e
|
||||
local_parking e54f
|
||||
local_pharmacy e550
|
||||
local_phone e551
|
||||
local_pizza e552
|
||||
local_play e553
|
||||
local_post_office e554
|
||||
local_printshop e555
|
||||
local_see e557
|
||||
local_shipping e558
|
||||
local_taxi e559
|
||||
location_city e7f1
|
||||
location_disabled e1b6
|
||||
location_off e0c7
|
||||
location_on e0c8
|
||||
location_searching e1b7
|
||||
lock e897
|
||||
lock_open e898
|
||||
lock_outline e899
|
||||
looks e3fc
|
||||
looks_3 e3fb
|
||||
looks_4 e3fd
|
||||
looks_5 e3fe
|
||||
looks_6 e3ff
|
||||
looks_one e400
|
||||
looks_two e401
|
||||
loop e028
|
||||
loupe e402
|
||||
low_priority e16d
|
||||
loyalty e89a
|
||||
mail e158
|
||||
mail_outline e0e1
|
||||
map e55b
|
||||
markunread e159
|
||||
markunread_mailbox e89b
|
||||
memory e322
|
||||
menu e5d2
|
||||
merge_type e252
|
||||
message e0c9
|
||||
mic e029
|
||||
mic_none e02a
|
||||
mic_off e02b
|
||||
mms e618
|
||||
mode_comment e253
|
||||
mode_edit e254
|
||||
monetization_on e263
|
||||
money_off e25c
|
||||
monochrome_photos e403
|
||||
mood e7f2
|
||||
mood_bad e7f3
|
||||
more e619
|
||||
more_horiz e5d3
|
||||
more_vert e5d4
|
||||
motorcycle e91b
|
||||
mouse e323
|
||||
move_to_inbox e168
|
||||
movie e02c
|
||||
movie_creation e404
|
||||
movie_filter e43a
|
||||
multiline_chart e6df
|
||||
music_note e405
|
||||
music_video e063
|
||||
my_location e55c
|
||||
nature e406
|
||||
nature_people e407
|
||||
navigate_before e408
|
||||
navigate_next e409
|
||||
navigation e55d
|
||||
near_me e569
|
||||
network_cell e1b9
|
||||
network_check e640
|
||||
network_locked e61a
|
||||
network_wifi e1ba
|
||||
new_releases e031
|
||||
next_week e16a
|
||||
nfc e1bb
|
||||
no_encryption e641
|
||||
no_sim e0cc
|
||||
not_interested e033
|
||||
note e06f
|
||||
note_add e89c
|
||||
notifications e7f4
|
||||
notifications_active e7f7
|
||||
notifications_none e7f5
|
||||
notifications_off e7f6
|
||||
notifications_paused e7f8
|
||||
offline_pin e90a
|
||||
ondemand_video e63a
|
||||
opacity e91c
|
||||
open_in_browser e89d
|
||||
open_in_new e89e
|
||||
open_with e89f
|
||||
pages e7f9
|
||||
pageview e8a0
|
||||
palette e40a
|
||||
pan_tool e925
|
||||
panorama e40b
|
||||
panorama_fish_eye e40c
|
||||
panorama_horizontal e40d
|
||||
panorama_vertical e40e
|
||||
panorama_wide_angle e40f
|
||||
party_mode e7fa
|
||||
pause e034
|
||||
pause_circle_filled e035
|
||||
pause_circle_outline e036
|
||||
payment e8a1
|
||||
people e7fb
|
||||
people_outline e7fc
|
||||
perm_camera_mic e8a2
|
||||
perm_contact_calendar e8a3
|
||||
perm_data_setting e8a4
|
||||
perm_device_information e8a5
|
||||
perm_identity e8a6
|
||||
perm_media e8a7
|
||||
perm_phone_msg e8a8
|
||||
perm_scan_wifi e8a9
|
||||
person e7fd
|
||||
person_add e7fe
|
||||
person_outline e7ff
|
||||
person_pin e55a
|
||||
person_pin_circle e56a
|
||||
personal_video e63b
|
||||
pets e91d
|
||||
phone e0cd
|
||||
phone_android e324
|
||||
phone_bluetooth_speaker e61b
|
||||
phone_forwarded e61c
|
||||
phone_in_talk e61d
|
||||
phone_iphone e325
|
||||
phone_locked e61e
|
||||
phone_missed e61f
|
||||
phone_paused e620
|
||||
phonelink e326
|
||||
phonelink_erase e0db
|
||||
phonelink_lock e0dc
|
||||
phonelink_off e327
|
||||
phonelink_ring e0dd
|
||||
phonelink_setup e0de
|
||||
photo e410
|
||||
photo_album e411
|
||||
photo_camera e412
|
||||
photo_filter e43b
|
||||
photo_library e413
|
||||
photo_size_select_actual e432
|
||||
photo_size_select_large e433
|
||||
photo_size_select_small e434
|
||||
picture_as_pdf e415
|
||||
picture_in_picture e8aa
|
||||
picture_in_picture_alt e911
|
||||
pie_chart e6c4
|
||||
pie_chart_outlined e6c5
|
||||
pin_drop e55e
|
||||
place e55f
|
||||
play_arrow e037
|
||||
play_circle_filled e038
|
||||
play_circle_outline e039
|
||||
play_for_work e906
|
||||
playlist_add e03b
|
||||
playlist_add_check e065
|
||||
playlist_play e05f
|
||||
plus_one e800
|
||||
poll e801
|
||||
polymer e8ab
|
||||
pool eb48
|
||||
portable_wifi_off e0ce
|
||||
portrait e416
|
||||
power e63c
|
||||
power_input e336
|
||||
power_settings_new e8ac
|
||||
pregnant_woman e91e
|
||||
present_to_all e0df
|
||||
print e8ad
|
||||
priority_high e645
|
||||
public e80b
|
||||
publish e255
|
||||
query_builder e8ae
|
||||
question_answer e8af
|
||||
queue e03c
|
||||
queue_music e03d
|
||||
queue_play_next e066
|
||||
radio e03e
|
||||
radio_button_checked e837
|
||||
radio_button_unchecked e836
|
||||
rate_review e560
|
||||
receipt e8b0
|
||||
recent_actors e03f
|
||||
record_voice_over e91f
|
||||
redeem e8b1
|
||||
redo e15a
|
||||
refresh e5d5
|
||||
remove e15b
|
||||
remove_circle e15c
|
||||
remove_circle_outline e15d
|
||||
remove_from_queue e067
|
||||
remove_red_eye e417
|
||||
remove_shopping_cart e928
|
||||
reorder e8fe
|
||||
repeat e040
|
||||
repeat_one e041
|
||||
replay e042
|
||||
replay_10 e059
|
||||
replay_30 e05a
|
||||
replay_5 e05b
|
||||
reply e15e
|
||||
reply_all e15f
|
||||
report e160
|
||||
report_problem e8b2
|
||||
restaurant e56c
|
||||
restaurant_menu e561
|
||||
restore e8b3
|
||||
restore_page e929
|
||||
ring_volume e0d1
|
||||
room e8b4
|
||||
room_service eb49
|
||||
rotate_90_degrees_ccw e418
|
||||
rotate_left e419
|
||||
rotate_right e41a
|
||||
rounded_corner e920
|
||||
router e328
|
||||
rowing e921
|
||||
rss_feed e0e5
|
||||
rv_hookup e642
|
||||
satellite e562
|
||||
save e161
|
||||
scanner e329
|
||||
schedule e8b5
|
||||
school e80c
|
||||
screen_lock_landscape e1be
|
||||
screen_lock_portrait e1bf
|
||||
screen_lock_rotation e1c0
|
||||
screen_rotation e1c1
|
||||
screen_share e0e2
|
||||
sd_card e623
|
||||
sd_storage e1c2
|
||||
search e8b6
|
||||
security e32a
|
||||
select_all e162
|
||||
send e163
|
||||
sentiment_dissatisfied e811
|
||||
sentiment_neutral e812
|
||||
sentiment_satisfied e813
|
||||
sentiment_very_dissatisfied e814
|
||||
sentiment_very_satisfied e815
|
||||
settings e8b8
|
||||
settings_applications e8b9
|
||||
settings_backup_restore e8ba
|
||||
settings_bluetooth e8bb
|
||||
settings_brightness e8bd
|
||||
settings_cell e8bc
|
||||
settings_ethernet e8be
|
||||
settings_input_antenna e8bf
|
||||
settings_input_component e8c0
|
||||
settings_input_composite e8c1
|
||||
settings_input_hdmi e8c2
|
||||
settings_input_svideo e8c3
|
||||
settings_overscan e8c4
|
||||
settings_phone e8c5
|
||||
settings_power e8c6
|
||||
settings_remote e8c7
|
||||
settings_system_daydream e1c3
|
||||
settings_voice e8c8
|
||||
share e80d
|
||||
shop e8c9
|
||||
shop_two e8ca
|
||||
shopping_basket e8cb
|
||||
shopping_cart e8cc
|
||||
short_text e261
|
||||
show_chart e6e1
|
||||
shuffle e043
|
||||
signal_cellular_4_bar e1c8
|
||||
signal_cellular_connected_no_internet_4_bar e1cd
|
||||
signal_cellular_no_sim e1ce
|
||||
signal_cellular_null e1cf
|
||||
signal_cellular_off e1d0
|
||||
signal_wifi_4_bar e1d8
|
||||
signal_wifi_4_bar_lock e1d9
|
||||
signal_wifi_off e1da
|
||||
sim_card e32b
|
||||
sim_card_alert e624
|
||||
skip_next e044
|
||||
skip_previous e045
|
||||
slideshow e41b
|
||||
slow_motion_video e068
|
||||
smartphone e32c
|
||||
smoke_free eb4a
|
||||
smoking_rooms eb4b
|
||||
sms e625
|
||||
sms_failed e626
|
||||
snooze e046
|
||||
sort e164
|
||||
sort_by_alpha e053
|
||||
spa eb4c
|
||||
space_bar e256
|
||||
speaker e32d
|
||||
speaker_group e32e
|
||||
speaker_notes e8cd
|
||||
speaker_notes_off e92a
|
||||
speaker_phone e0d2
|
||||
spellcheck e8ce
|
||||
star e838
|
||||
star_border e83a
|
||||
star_half e839
|
||||
stars e8d0
|
||||
stay_current_landscape e0d3
|
||||
stay_current_portrait e0d4
|
||||
stay_primary_landscape e0d5
|
||||
stay_primary_portrait e0d6
|
||||
stop e047
|
||||
stop_screen_share e0e3
|
||||
storage e1db
|
||||
store e8d1
|
||||
store_mall_directory e563
|
||||
straighten e41c
|
||||
streetview e56e
|
||||
strikethrough_s e257
|
||||
style e41d
|
||||
subdirectory_arrow_left e5d9
|
||||
subdirectory_arrow_right e5da
|
||||
subject e8d2
|
||||
subscriptions e064
|
||||
subtitles e048
|
||||
subway e56f
|
||||
supervisor_account e8d3
|
||||
surround_sound e049
|
||||
swap_calls e0d7
|
||||
swap_horiz e8d4
|
||||
swap_vert e8d5
|
||||
swap_vertical_circle e8d6
|
||||
switch_camera e41e
|
||||
switch_video e41f
|
||||
sync e627
|
||||
sync_disabled e628
|
||||
sync_problem e629
|
||||
system_update e62a
|
||||
system_update_alt e8d7
|
||||
tab e8d8
|
||||
tab_unselected e8d9
|
||||
tablet e32f
|
||||
tablet_android e330
|
||||
tablet_mac e331
|
||||
tag_faces e420
|
||||
tap_and_play e62b
|
||||
terrain e564
|
||||
text_fields e262
|
||||
text_format e165
|
||||
textsms e0d8
|
||||
texture e421
|
||||
theaters e8da
|
||||
thumb_down e8db
|
||||
thumb_up e8dc
|
||||
thumbs_up_down e8dd
|
||||
time_to_leave e62c
|
||||
timelapse e422
|
||||
timeline e922
|
||||
timer e425
|
||||
timer_10 e423
|
||||
timer_3 e424
|
||||
timer_off e426
|
||||
title e264
|
||||
toc e8de
|
||||
today e8df
|
||||
toll e8e0
|
||||
tonality e427
|
||||
touch_app e913
|
||||
toys e332
|
||||
track_changes e8e1
|
||||
traffic e565
|
||||
train e570
|
||||
tram e571
|
||||
transfer_within_a_station e572
|
||||
transform e428
|
||||
translate e8e2
|
||||
trending_down e8e3
|
||||
trending_flat e8e4
|
||||
trending_up e8e5
|
||||
tune e429
|
||||
turned_in e8e6
|
||||
turned_in_not e8e7
|
||||
tv e333
|
||||
unarchive e169
|
||||
undo e166
|
||||
unfold_less e5d6
|
||||
unfold_more e5d7
|
||||
update e923
|
||||
usb e1e0
|
||||
verified_user e8e8
|
||||
vertical_align_bottom e258
|
||||
vertical_align_center e259
|
||||
vertical_align_top e25a
|
||||
vibration e62d
|
||||
video_call e070
|
||||
video_label e071
|
||||
video_library e04a
|
||||
videocam e04b
|
||||
videocam_off e04c
|
||||
videogame_asset e338
|
||||
view_agenda e8e9
|
||||
view_array e8ea
|
||||
view_carousel e8eb
|
||||
view_column e8ec
|
||||
view_comfy e42a
|
||||
view_compact e42b
|
||||
view_day e8ed
|
||||
view_headline e8ee
|
||||
view_list e8ef
|
||||
view_module e8f0
|
||||
view_quilt e8f1
|
||||
view_stream e8f2
|
||||
view_week e8f3
|
||||
vignette e435
|
||||
visibility e8f4
|
||||
visibility_off e8f5
|
||||
voice_chat e62e
|
||||
voicemail e0d9
|
||||
volume_down e04d
|
||||
volume_mute e04e
|
||||
volume_off e04f
|
||||
volume_up e050
|
||||
vpn_key e0da
|
||||
vpn_lock e62f
|
||||
wallpaper e1bc
|
||||
warning e002
|
||||
watch e334
|
||||
watch_later e924
|
||||
wb_auto e42c
|
||||
wb_cloudy e42d
|
||||
wb_incandescent e42e
|
||||
wb_iridescent e436
|
||||
wb_sunny e430
|
||||
wc e63d
|
||||
web e051
|
||||
web_asset e069
|
||||
weekend e16b
|
||||
whatshot e80e
|
||||
widgets e1bd
|
||||
wifi e63e
|
||||
wifi_lock e1e1
|
||||
wifi_tethering e1e2
|
||||
work e8f9
|
||||
wrap_text e25b
|
||||
youtube_searched_for e8fa
|
||||
zoom_in e8ff
|
||||
zoom_out e900
|
||||
zoom_out_map e56b
|
36
apprise_api/static/iconfont/material-icons.css
Normal file
36
apprise_api/static/iconfont/material-icons.css
Normal file
@ -0,0 +1,36 @@
|
||||
@font-face {
|
||||
font-family: 'Material Icons';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
src: url(MaterialIcons-Regular.eot); /* For IE6-8 */
|
||||
src: local('Material Icons'),
|
||||
local('MaterialIcons-Regular'),
|
||||
url(MaterialIcons-Regular.woff2) format('woff2'),
|
||||
url(MaterialIcons-Regular.woff) format('woff'),
|
||||
url(MaterialIcons-Regular.ttf) format('truetype');
|
||||
}
|
||||
|
||||
.material-icons {
|
||||
font-family: 'Material Icons';
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
font-size: 24px; /* Preferred icon size */
|
||||
display: inline-block;
|
||||
line-height: 1;
|
||||
text-transform: none;
|
||||
letter-spacing: normal;
|
||||
word-wrap: normal;
|
||||
white-space: nowrap;
|
||||
direction: ltr;
|
||||
|
||||
/* Support for all WebKit browsers. */
|
||||
-webkit-font-smoothing: antialiased;
|
||||
/* Support for Safari and Chrome. */
|
||||
text-rendering: optimizeLegibility;
|
||||
|
||||
/* Support for Firefox. */
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
|
||||
/* Support for IE. */
|
||||
font-feature-settings: 'liga';
|
||||
}
|
12374
apprise_api/static/js/materialize.js
vendored
Normal file
12374
apprise_api/static/js/materialize.js
vendored
Normal file
File diff suppressed because it is too large
Load Diff
6
apprise_api/static/js/materialize.min.js
vendored
Normal file
6
apprise_api/static/js/materialize.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
202
apprise_api/static/licenses/material-design-icons-3.0.1-LICENSE
Normal file
202
apprise_api/static/licenses/material-design-icons-3.0.1-LICENSE
Normal file
@ -0,0 +1,202 @@
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
21
apprise_api/static/licenses/materialize-1.0.0.LICENSE
Normal file
21
apprise_api/static/licenses/materialize-1.0.0.LICENSE
Normal file
@ -0,0 +1,21 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2014-2018 Materialize
|
||||
|
||||
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.
|
BIN
apprise_api/static/logo.png
Normal file
BIN
apprise_api/static/logo.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 3.6 KiB |
4
dev-requirements.txt
Normal file
4
dev-requirements.txt
Normal file
@ -0,0 +1,4 @@
|
||||
pytest
|
||||
flake8
|
||||
pytest-django
|
||||
pytest-cov
|
19
docker-compose.yml
Normal file
19
docker-compose.yml
Normal file
@ -0,0 +1,19 @@
|
||||
version: '3.3'
|
||||
|
||||
services:
|
||||
backend:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile-gunicorn
|
||||
volumes:
|
||||
- ./var:/var/apprise
|
||||
restart: always
|
||||
|
||||
frontend:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile-nginx
|
||||
ports:
|
||||
- "8000:80"
|
||||
links:
|
||||
- backend
|
49
manage.py
Executable file
49
manage.py
Executable file
@ -0,0 +1,49 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- 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 sys
|
||||
|
||||
# Update our path so it will see our apprise_api content
|
||||
sys.path.insert(
|
||||
0, os.path.join(os.path.dirname(os.path.abspath(__file__)), 'apprise_api'))
|
||||
|
||||
|
||||
def main():
|
||||
# Unless otherwise specified, default to a debug mode
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'core.settings.debug')
|
||||
try:
|
||||
from django.core.management import execute_from_command_line
|
||||
except ImportError as exc:
|
||||
raise ImportError(
|
||||
"Couldn't import Django. Are you sure it's installed and "
|
||||
"available on your PYTHONPATH environment variable? Did you "
|
||||
"forget to activate a virtual environment?"
|
||||
) from exc
|
||||
execute_from_command_line(sys.argv)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
2
requirements.txt
Normal file
2
requirements.txt
Normal file
@ -0,0 +1,2 @@
|
||||
django
|
||||
apprise
|
Loading…
Reference in New Issue
Block a user