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