├── .coveragerc ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── 1_bug_report.md │ ├── 2_enhancement_request.md │ └── 3_question.md ├── PULL_REQUEST_TEMPLATE.md ├── dependabot.yml └── workflows │ ├── build.yml │ └── tests.yml ├── .gitignore ├── CONTRIBUTIONS.md ├── Dockerfile ├── Dockerfile.py312 ├── LICENSE ├── README-es.md ├── README.md ├── Screenshot-1.png ├── Screenshot-2.png ├── Screenshot-3.png ├── Screenshot-4.png ├── apprise_api ├── api │ ├── __init__.py │ ├── apps.py │ ├── context_processors.py │ ├── forms.py │ ├── management │ │ ├── __init__.py │ │ └── commands │ │ │ ├── __init__.py │ │ │ └── storeprune.py │ ├── migrations │ │ └── __init__.py │ ├── models.py │ ├── payload_mapper.py │ ├── templates │ │ ├── base.html │ │ ├── config.html │ │ ├── config_list.html │ │ ├── details.html │ │ └── welcome.html │ ├── tests │ │ ├── __init__.py │ │ ├── test_add.py │ │ ├── test_attachment.py │ │ ├── test_cli.py │ │ ├── test_config_cache.py │ │ ├── test_del.py │ │ ├── test_details.py │ │ ├── test_get.py │ │ ├── test_healthecheck.py │ │ ├── test_json_urls.py │ │ ├── test_manager.py │ │ ├── test_notify.py │ │ ├── test_payload_mapper.py │ │ ├── test_stateful_notify.py │ │ ├── test_stateless_notify.py │ │ ├── test_urlfilter.py │ │ ├── test_utils.py │ │ ├── test_webhook.py │ │ └── test_welcome.py │ ├── urlfilter.py │ ├── urls.py │ ├── utils.py │ └── views.py ├── core │ ├── __init__.py │ ├── context_processors.py │ ├── middleware │ │ ├── __init__.py │ │ ├── config.py │ │ └── theme.py │ ├── settings │ │ ├── __init__.py │ │ ├── debug │ │ │ ├── __init__.py │ │ │ └── urls.py │ │ └── pytest │ │ │ ├── __init__.py │ │ │ └── runner.py │ ├── themes.py │ ├── urls.py │ └── wsgi.py ├── etc │ ├── nginx.conf │ └── supervisord.conf ├── gunicorn.conf.py ├── manage.py ├── static │ ├── css │ │ ├── base.css │ │ ├── highlight.min.css │ │ ├── materialize.min.css │ │ ├── theme-dark.min.css │ │ └── theme-light.min.css │ ├── favicon.ico │ ├── iconfont │ │ ├── MaterialIcons-Regular.eot │ │ ├── MaterialIcons-Regular.ijmap │ │ ├── MaterialIcons-Regular.svg │ │ ├── MaterialIcons-Regular.ttf │ │ ├── MaterialIcons-Regular.woff │ │ ├── MaterialIcons-Regular.woff2 │ │ ├── README.md │ │ ├── codepoints │ │ └── material-icons.css │ ├── js │ │ ├── highlight.pack.js │ │ ├── materialize.min.js │ │ └── sweetalert2.all.min.js │ ├── licenses │ │ ├── highlight-9.17.1.LICENSE │ │ ├── material-design-icons-3.0.1.LICENSE │ │ ├── materialize-1.0.0.LICENSE │ │ └── sweetalert2-11.17.2.LICENSE │ └── logo.png ├── supervisord-startup └── var │ └── plugin │ └── README.md ├── dev-requirements.txt ├── docker-compose.yml ├── manage.py ├── requirements.txt ├── setup.cfg ├── swagger.yaml └── tox.ini /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | data_file = .coverage-reports/.coverage 3 | parallel = False 4 | concurrency = multiprocessing 5 | include = apprise_api 6 | omit = 7 | *apps.py, 8 | */migrations/*, 9 | */core/settings/*, 10 | */*/tests/*, 11 | lib/*, 12 | lib64/*, 13 | *urls.py, 14 | */core/wsgi.py, 15 | gunicorn.conf.py, 16 | */manage.py 17 | 18 | disable_warnings = no-data-collected 19 | 20 | [report] 21 | show_missing = True 22 | skip_covered = True 23 | skip_empty = True 24 | fail_under = 75.0 25 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: caronc 2 | custom: ['https://www.paypal.me/lead2gold', ] 3 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/1_bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: 🐛 Bug Report 3 | about: Report any errors and problems 4 | title: '' 5 | labels: 'bug' 6 | assignees: '' 7 | 8 | --- 9 | 10 | :beetle: **Describe the bug** 11 | 12 | 13 | :bulb: **Screenshots and Logs** 14 | 15 | 16 | 17 | :computer: **Your System Details:** 18 | - OS: [e.g. RedHat v8.0] 19 | - Python Version: [e.g. Python v2.7] 20 | 21 | :crystal_ball: **Additional context** 22 | 23 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/2_enhancement_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: 💡 Enhancement Request 3 | about: Got a great idea? Let us know! 4 | title: '' 5 | labels: 'enhancement' 6 | assignees: '' 7 | 8 | --- 9 | 10 | :bulb: **The Idea** 11 | 12 | 13 | :hammer: **Breaking Feature** 14 | 16 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/3_question.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: ❓ Support Question 3 | about: Ask a question about Apprise 4 | title: '' 5 | labels: 'question' 6 | assignees: '' 7 | 8 | --- 9 | 10 | :question: **Question** 11 | 12 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## Description: 2 | **Related issue (if applicable):** refs # 3 | 4 | ## Checklist 5 | 6 | * [ ] The code change is tested and works locally. 7 | * [ ] There is no commented out code in this PR. 8 | * [ ] No lint errors (use `flake8`) 9 | * [ ] Tests added 10 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # https://docs.github.com/en/code-security/supply-chain-security/keeping-your-dependencies-updated-automatically/configuration-options-for-dependency-updates#package-ecosystem 2 | 3 | version: 2 4 | updates: 5 | 6 | # Enable updates for GitHub Actions 7 | - package-ecosystem: "github-actions" 8 | target-branch: "master" 9 | directory: "/" 10 | schedule: 11 | # Check for updates to GitHub Actions every month 12 | interval: "monthly" 13 | labels: 14 | - "dependencies" 15 | groups: 16 | actions: 17 | update-types: 18 | - "major" 19 | - "minor" 20 | - "patch" -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build Apprise API Image 2 | 3 | on: 4 | push: 5 | branches: master 6 | tags: 7 | - v* 8 | 9 | jobs: 10 | buildx: 11 | name: Build Docker Image 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Checkout 15 | uses: actions/checkout@v4 16 | 17 | - name: Check pushing to Docker Hub 18 | id: push-other-places 19 | # Only push to Dockerhub from the main repo 20 | # Otherwise forks would require a Docker Hub account and secrets setup 21 | run: | 22 | if [[ ${{ github.repository_owner }} == "caronc" ]] ; then 23 | echo "Enabling DockerHub image push" 24 | echo "enable=true" >> $GITHUB_OUTPUT 25 | else 26 | echo "Not pushing to DockerHub" 27 | echo "enable=false" >> $GITHUB_OUTPUT 28 | fi 29 | 30 | # Mostly for forks, set an output package name for ghcr.io using the repo name 31 | - name: Set ghcr repository name 32 | id: set-ghcr-repository 33 | run: | 34 | ghcr_name=$(echo "${{ github.repository_owner }}/apprise" | awk '{ print tolower($0) }') 35 | echo "Name is ${ghcr_name}" 36 | echo "ghcr-repository=${ghcr_name}" >> $GITHUB_OUTPUT 37 | 38 | - name: Docker meta 39 | id: docker_meta 40 | uses: docker/metadata-action@v5 41 | with: 42 | images: | 43 | ghcr.io/${{ steps.set-ghcr-repository.outputs.ghcr-repository }} 44 | name=docker.io/caronc/apprise,enable=${{ steps.push-other-places.outputs.enable }} 45 | tags: | 46 | type=semver,event=tag,pattern={{version}} 47 | type=semver,event=tag,pattern={{major}}.{{minor}} 48 | type=edge,branch=master 49 | 50 | - name: Set up QEMU 51 | uses: docker/setup-qemu-action@v3 52 | 53 | - name: Set up Docker Buildx 54 | uses: docker/setup-buildx-action@v3 55 | 56 | - name: Login to DockerHub 57 | uses: docker/login-action@v3 58 | # Don't attempt to login is not pushing to Docker Hub 59 | if: steps.push-other-places.outputs.enable == 'true' 60 | with: 61 | username: ${{ secrets.DOCKER_USERNAME }} 62 | password: ${{ secrets.DOCKER_PASSWORD }} 63 | 64 | - name: Login to GitHub Container Registry 65 | uses: docker/login-action@v3 66 | with: 67 | registry: ghcr.io 68 | username: ${{ github.actor }} 69 | password: ${{ secrets.GITHUB_TOKEN }} 70 | 71 | - name: Build and push 72 | uses: docker/build-push-action@v6 73 | with: 74 | context: . 75 | platforms: linux/amd64,linux/arm64 76 | push: ${{ github.event_name != 'pull_request' }} 77 | tags: ${{ steps.docker_meta.outputs.tags }} 78 | labels: ${{ steps.docker_meta.outputs.labels }} 79 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: 4 | 5 | # On which repository actions to trigger the build. 6 | push: 7 | branches: [ master ] 8 | pull_request: 9 | branches: [ master ] 10 | 11 | # Allow job to be triggered manually. 12 | workflow_dispatch: 13 | 14 | # Cancel in-progress jobs when pushing to the same branch. 15 | concurrency: 16 | cancel-in-progress: true 17 | group: ${{ github.workflow }}-${{ github.ref }} 18 | 19 | jobs: 20 | 21 | tests: 22 | 23 | runs-on: ${{ matrix.os }} 24 | strategy: 25 | 26 | # Run all jobs to completion (false), or cancel 27 | # all jobs once the first one fails (true). 28 | fail-fast: true 29 | 30 | # Define a minimal test matrix, it will be 31 | # expanded using subsequent `include` items. 32 | matrix: 33 | os: ["ubuntu-latest"] 34 | python-version: ["3.12"] 35 | bare: [false] 36 | 37 | defaults: 38 | run: 39 | shell: bash 40 | 41 | env: 42 | OS: ${{ matrix.os }} 43 | PYTHON: ${{ matrix.python-version }} 44 | BARE: ${{ matrix.bare }} 45 | 46 | name: Python ${{ matrix.python-version }} on ${{ matrix.os }} ${{ matrix.bare && '(bare)' || '' }} 47 | steps: 48 | 49 | - name: Acquire sources 50 | uses: actions/checkout@v4 51 | 52 | - name: Install prerequisites (Linux) 53 | if: runner.os == 'Linux' 54 | run: | 55 | sudo apt-get update 56 | 57 | - name: Install project dependencies (Baseline) 58 | run: | 59 | pip install -r requirements.txt -r dev-requirements.txt 60 | 61 | # For saving resources, code style checking is 62 | # only invoked within the `bare` environment. 63 | - name: Check code style 64 | if: matrix.bare == true 65 | run: | 66 | flake8 apprise_api --count --show-source --statistics 67 | 68 | - name: Run tests 69 | run: | 70 | coverage run -m pytest apprise_api 71 | 72 | - name: Process coverage data 73 | run: | 74 | coverage xml 75 | coverage report 76 | 77 | - name: Upload coverage data 78 | uses: codecov/codecov-action@v5 79 | with: 80 | files: ./coverage.xml 81 | fail_ci_if_error: false 82 | token: ${{ secrets.CODECOV_TOKEN }} 83 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # vi swap files 7 | .*.sw? 8 | 9 | # Distribution / packaging / virtualenv 10 | .Python 11 | .bash_history 12 | env/ 13 | build/ 14 | develop-eggs/ 15 | dist/ 16 | downloads/ 17 | eggs/ 18 | .eggs/ 19 | lib64 20 | lib 21 | var/ 22 | include/ 23 | bin/ 24 | parts/ 25 | sdist/ 26 | pyvenv.cfg 27 | *.egg-info/ 28 | .installed.cfg 29 | *.egg 30 | pip-selfcheck.json 31 | 32 | # PyInstaller 33 | # Usually these files are written by a python script from a template 34 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 35 | *.manifest 36 | *.spec 37 | 38 | # Allow RPM SPEC files despite pyInstaller ignore 39 | !packaging/redhat/*.spec 40 | 41 | # Installer logs 42 | pip-log.txt 43 | pip-delete-this-directory.txt 44 | 45 | # Unit test / coverage reports 46 | htmlcov/ 47 | .tox/ 48 | .coverage 49 | .coverage.* 50 | .cache 51 | nosetests.xml 52 | coverage.xml 53 | *,cover 54 | .hypothesis/ 55 | 56 | # Translations 57 | *.mo 58 | 59 | # Django stuff: 60 | *.log 61 | 62 | # PyBuilder 63 | target/ 64 | 65 | #Ipython Notebook 66 | .ipynb_checkpoints 67 | 68 | #PyCharm 69 | .idea 70 | 71 | # Dockerfile 72 | docker-compose.yml 73 | 74 | # Apprise Gateway Variable/Test Configuration 75 | apprise_gw/var/* 76 | -------------------------------------------------------------------------------- /CONTRIBUTIONS.md: -------------------------------------------------------------------------------- 1 | # Contributions to the Apprise API project 2 | 3 | ## Creator & Maintainer 4 | 5 | * Chris Caron 6 | 7 | ## Contributors 8 | 9 | The following users have contributed to this project and their deserved 10 | recognition has been identified here. If you have contributed and wish 11 | to be acknowledged for it, the syntax is as follows: 12 | 13 | ``` 14 | * [Your name or handle] <[email or website]> 15 | * [Month Year] - [Brief summary of your contribution] 16 | ``` 17 | 18 | The contributors have been listed in chronological order: 19 | * Scott Elblein 20 | * 23rd Sep 2023 - Provided Dark Theme 21 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.12-slim AS base 2 | 3 | # set version label 4 | ARG BUILD_DATE 5 | ARG VERSION 6 | LABEL build_version="Apprise API version:- ${VERSION} Build-date:- ${BUILD_DATE}" 7 | LABEL maintainer="Chris-Caron" 8 | 9 | # set environment variables 10 | ENV PYTHONDONTWRITEBYTECODE=1 11 | ENV PYTHONUNBUFFERED=1 12 | ENV APPRISE_CONFIG_DIR=/config 13 | ENV APPRISE_ATTACH_DIR=/attach 14 | ENV APPRISE_PLUGIN_PATHS=/plugin 15 | 16 | FROM base AS runtime 17 | 18 | # Install requirements and gunicorn 19 | COPY ./requirements.txt /etc/requirements.txt 20 | 21 | RUN set -eux && \ 22 | echo "Installing nginx" && \ 23 | apt-get update -qq && \ 24 | apt-get install -y -qq \ 25 | nginx && \ 26 | echo "Installing tools" && \ 27 | apt-get install -y -qq \ 28 | curl sed git && \ 29 | echo "Installing python requirements" && \ 30 | pip3 install --no-cache-dir -q -r /etc/requirements.txt gunicorn supervisor && \ 31 | pip freeze && \ 32 | echo "Cleaning up" && \ 33 | apt-get --yes autoremove --purge && \ 34 | apt-get clean --yes && \ 35 | rm --recursive --force --verbose /var/lib/apt/lists/* && \ 36 | rm --recursive --force --verbose /tmp/* && \ 37 | rm --recursive --force --verbose /var/tmp/* && \ 38 | rm --recursive --force --verbose /var/cache/apt/archives/* && \ 39 | truncate --size 0 /var/log/*log 40 | 41 | # Copy our static content in place 42 | COPY apprise_api/static /usr/share/nginx/html/s/ 43 | 44 | # set work directory 45 | WORKDIR /opt/apprise 46 | 47 | # Copy over Apprise API 48 | COPY apprise_api/ webapp 49 | 50 | # Configuration Permissions (to run nginx as a non-root user) 51 | RUN umask 0002 && \ 52 | touch /etc/nginx/server-override.conf && \ 53 | touch /etc/nginx/location-override.conf 54 | 55 | VOLUME /config 56 | VOLUME /attach 57 | VOLUME /plugin 58 | EXPOSE 8000 59 | CMD ["/opt/apprise/webapp/supervisord-startup"] 60 | -------------------------------------------------------------------------------- /Dockerfile.py312: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # BSD 2-Clause License 3 | # 4 | # Apprise-API - Push Notification Library. 5 | # Copyright (c) 2023, Chris Caron 6 | # 7 | # Redistribution and use in source and binary forms, with or without 8 | # modification, are permitted provided that the following conditions are met: 9 | # 10 | # 1. Redistributions of source code must retain the above copyright notice, 11 | # this list of conditions and the following disclaimer. 12 | # 13 | # 2. Redistributions in binary form must reproduce the above copyright notice, 14 | # this list of conditions and the following disclaimer in the documentation 15 | # and/or other materials provided with the distribution. 16 | # 17 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 18 | # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 19 | # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 20 | # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE 21 | # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 22 | # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 23 | # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 24 | # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 25 | # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 26 | # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 27 | # POSSIBILITY OF SUCH DAMAGE. 28 | 29 | # Base 30 | FROM python:3.12 31 | RUN apt-get update && \ 32 | apt-get install -y --no-install-recommends build-essential musl-dev bash && \ 33 | rm -rf /var/lib/apt/lists/* 34 | 35 | # Apprise Setup 36 | VOLUME ["/apprise-api"] 37 | WORKDIR /apprise-api 38 | COPY requirements.txt / 39 | COPY dev-requirements.txt / 40 | ENV PYTHONPATH /apprise-api 41 | ENV PYTHONPYCACHEPREFIX /apprise-api/__pycache__/py312 42 | 43 | RUN pip install --no-cache-dir -r /requirements.txt -r /dev-requirements.txt 44 | 45 | RUN addgroup --gid ${USER_GID:-1000} apprise 46 | RUN adduser --system --uid ${USER_UID:-1000} --ingroup apprise --home /apprise-api --no-create-home --disabled-password apprise 47 | 48 | USER apprise 49 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Chris Caron 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Screenshot-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/caronc/apprise-api/3fde6d00914b6a538f9003c5d940b909fdce42a7/Screenshot-1.png -------------------------------------------------------------------------------- /Screenshot-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/caronc/apprise-api/3fde6d00914b6a538f9003c5d940b909fdce42a7/Screenshot-2.png -------------------------------------------------------------------------------- /Screenshot-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/caronc/apprise-api/3fde6d00914b6a538f9003c5d940b909fdce42a7/Screenshot-3.png -------------------------------------------------------------------------------- /Screenshot-4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/caronc/apprise-api/3fde6d00914b6a538f9003c5d940b909fdce42a7/Screenshot-4.png -------------------------------------------------------------------------------- /apprise_api/api/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/caronc/apprise-api/3fde6d00914b6a538f9003c5d940b909fdce42a7/apprise_api/api/__init__.py -------------------------------------------------------------------------------- /apprise_api/api/apps.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Copyright (C) 2019 Chris Caron 4 | # All rights reserved. 5 | # 6 | # This code is licensed under the MIT License. 7 | # 8 | # Permission is hereby granted, free of charge, to any person obtaining a copy 9 | # of this software and associated documentation files(the "Software"), to deal 10 | # in the Software without restriction, including without limitation the rights 11 | # to use, copy, modify, merge, publish, distribute, sublicense, and / or sell 12 | # copies of the Software, and to permit persons to whom the Software is 13 | # furnished to do so, subject to the following conditions : 14 | # 15 | # The above copyright notice and this permission notice shall be included in 16 | # all copies or substantial portions of the Software. 17 | # 18 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE 21 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 24 | # THE SOFTWARE. 25 | from django.apps import AppConfig 26 | 27 | 28 | class ApiConfig(AppConfig): 29 | name = 'api' 30 | -------------------------------------------------------------------------------- /apprise_api/api/context_processors.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Copyright (C) 2020 Chris Caron 4 | # All rights reserved. 5 | # 6 | # This code is licensed under the MIT License. 7 | # 8 | # Permission is hereby granted, free of charge, to any person obtaining a copy 9 | # of this software and associated documentation files(the "Software"), to deal 10 | # in the Software without restriction, including without limitation the rights 11 | # to use, copy, modify, merge, publish, distribute, sublicense, and / or sell 12 | # copies of the Software, and to permit persons to whom the Software is 13 | # furnished to do so, subject to the following conditions : 14 | # 15 | # The above copyright notice and this permission notice shall be included in 16 | # all copies or substantial portions of the Software. 17 | # 18 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE 21 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 24 | # THE SOFTWARE. 25 | from .utils import gen_unique_config_id 26 | from .utils import ConfigCache 27 | from django.conf import settings 28 | import apprise 29 | 30 | 31 | def stateful_mode(request): 32 | """ 33 | Returns our loaded Stateful Mode 34 | """ 35 | return {'STATEFUL_MODE': ConfigCache.mode} 36 | 37 | 38 | def config_lock(request): 39 | """ 40 | Returns the state of our global configuration lock 41 | """ 42 | return {'CONFIG_LOCK': settings.APPRISE_CONFIG_LOCK} 43 | 44 | 45 | def admin_enabled(request): 46 | """ 47 | Returns whether we allow the config list to be displayed 48 | """ 49 | return {'APPRISE_ADMIN': settings.APPRISE_ADMIN} 50 | 51 | 52 | def apprise_version(request): 53 | """ 54 | Returns the current version of apprise loaded under the hood 55 | """ 56 | return {'APPRISE_VERSION': apprise.__version__} 57 | 58 | 59 | def default_config_id(request): 60 | """ 61 | Returns a unique config identifier 62 | """ 63 | return {'DEFAULT_CONFIG_ID': request.default_config_id} 64 | 65 | 66 | def unique_config_id(request): 67 | """ 68 | Returns a unique config identifier 69 | """ 70 | return {'UNIQUE_CONFIG_ID': gen_unique_config_id()} 71 | -------------------------------------------------------------------------------- /apprise_api/api/forms.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Copyright (C) 2019 Chris Caron 4 | # All rights reserved. 5 | # 6 | # This code is licensed under the MIT License. 7 | # 8 | # Permission is hereby granted, free of charge, to any person obtaining a copy 9 | # of this software and associated documentation files(the "Software"), to deal 10 | # in the Software without restriction, including without limitation the rights 11 | # to use, copy, modify, merge, publish, distribute, sublicense, and / or sell 12 | # copies of the Software, and to permit persons to whom the Software is 13 | # furnished to do so, subject to the following conditions : 14 | # 15 | # The above copyright notice and this permission notice shall be included in 16 | # all copies or substantial portions of the Software. 17 | # 18 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE 21 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 24 | # THE SOFTWARE. 25 | 26 | import apprise 27 | from django import forms 28 | from django.utils.translation import gettext_lazy as _ 29 | 30 | # Auto-Detect Keyword 31 | AUTO_DETECT_CONFIG_KEYWORD = 'auto' 32 | 33 | # Define our potential configuration types 34 | CONFIG_FORMATS = ( 35 | (AUTO_DETECT_CONFIG_KEYWORD, _('Auto-Detect')), 36 | (apprise.ConfigFormat.TEXT, _('TEXT')), 37 | (apprise.ConfigFormat.YAML, _('YAML')), 38 | ) 39 | 40 | NOTIFICATION_TYPES = ( 41 | (apprise.NotifyType.INFO, _('Info')), 42 | (apprise.NotifyType.SUCCESS, _('Success')), 43 | (apprise.NotifyType.WARNING, _('Warning')), 44 | (apprise.NotifyType.FAILURE, _('Failure')), 45 | ) 46 | 47 | # Define our potential input text categories 48 | INPUT_FORMATS = ( 49 | (apprise.NotifyFormat.TEXT, _('TEXT')), 50 | (apprise.NotifyFormat.MARKDOWN, _('MARKDOWN')), 51 | (apprise.NotifyFormat.HTML, _('HTML')), 52 | # As-is - do not interpret it 53 | (None, _('IGNORE')), 54 | ) 55 | 56 | URLS_MAX_LEN = 1024 57 | URLS_PLACEHOLDER = 'mailto://user:pass@domain.com, ' \ 58 | 'slack://tokena/tokenb/tokenc, ...' 59 | 60 | 61 | class AddByUrlForm(forms.Form): 62 | """ 63 | Form field for adding entries simply by passing in a string 64 | of one or more URLs that have been deliminted by either a 65 | comma and/or a space. 66 | 67 | This content can just be directly fed straight into Apprise 68 | """ 69 | urls = forms.CharField( 70 | label=_('URLs'), 71 | widget=forms.TextInput(attrs={'placeholder': URLS_PLACEHOLDER}), 72 | max_length=URLS_MAX_LEN, 73 | ) 74 | 75 | 76 | class AddByConfigForm(forms.Form): 77 | """ 78 | This is the reading in of a configuration file which contains 79 | potential asset information (if yaml file) and tag details. 80 | """ 81 | 82 | format = forms.ChoiceField( 83 | label=_('Format'), 84 | choices=CONFIG_FORMATS, 85 | initial=CONFIG_FORMATS[0][0], 86 | required=False, 87 | ) 88 | 89 | config = forms.CharField( 90 | label=_('Configuration'), 91 | widget=forms.Textarea(), 92 | max_length=4096, 93 | required=False, 94 | ) 95 | 96 | def clean_format(self): 97 | """ 98 | We just ensure there is a format always set and it defaults to auto 99 | """ 100 | data = self.cleaned_data['format'] 101 | if not data: 102 | # Set to auto 103 | data = CONFIG_FORMATS[0][0] 104 | return data 105 | 106 | 107 | class NotifyForm(forms.Form): 108 | """ 109 | This is the reading in of a configuration file which contains 110 | potential asset information (if yaml file) and tag details. 111 | """ 112 | 113 | format = forms.ChoiceField( 114 | label=_('Process As'), 115 | initial=INPUT_FORMATS[0][0], 116 | choices=INPUT_FORMATS, 117 | required=False, 118 | ) 119 | 120 | type = forms.ChoiceField( 121 | label=_('Type'), 122 | choices=NOTIFICATION_TYPES, 123 | initial=NOTIFICATION_TYPES[0][0], 124 | required=False, 125 | ) 126 | 127 | title = forms.CharField( 128 | label=_('Title'), 129 | widget=forms.TextInput(attrs={'placeholder': _('Optional Title')}), 130 | max_length=apprise.NotifyBase.title_maxlen, 131 | required=False, 132 | ) 133 | 134 | body = forms.CharField( 135 | label=_('Body'), 136 | widget=forms.Textarea( 137 | attrs={'placeholder': _('Define your message body here...')}), 138 | max_length=apprise.NotifyBase.body_maxlen, 139 | required=False, 140 | ) 141 | 142 | # Attachment Support 143 | attachment = forms.FileField( 144 | label=_('Attachment'), 145 | required=False, 146 | ) 147 | 148 | tag = forms.CharField( 149 | label=_('Tags'), 150 | widget=forms.TextInput( 151 | attrs={'placeholder': _('Optional_Tag1, Optional_Tag2, ...')}), 152 | required=False, 153 | ) 154 | 155 | # Allow support for tags keyword in addition to tag; the 'tag' field will 156 | # always take priority over this however adding `tags` gives the user more 157 | # flexibilty to use either/or keyword 158 | tags = forms.CharField( 159 | label=_('Tags'), 160 | widget=forms.HiddenInput(), 161 | required=False, 162 | ) 163 | 164 | def clean_type(self): 165 | """ 166 | We just ensure there is a type always set 167 | """ 168 | data = self.cleaned_data['type'] 169 | if not data: 170 | # Always set a type 171 | data = apprise.NotifyType.INFO 172 | return data 173 | 174 | def clean_format(self): 175 | """ 176 | We just ensure there is a format always set 177 | """ 178 | data = self.cleaned_data['format'] 179 | if not data: 180 | # Always set a type 181 | data = apprise.NotifyFormat.TEXT 182 | return data 183 | 184 | 185 | class NotifyByUrlForm(NotifyForm): 186 | """ 187 | Same as the NotifyForm but additionally processes a string of URLs to 188 | notify directly. 189 | """ 190 | urls = forms.CharField( 191 | label=_('URLs'), 192 | widget=forms.TextInput(attrs={'placeholder': URLS_PLACEHOLDER}), 193 | max_length=URLS_MAX_LEN, 194 | required=False, 195 | ) 196 | -------------------------------------------------------------------------------- /apprise_api/api/management/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/caronc/apprise-api/3fde6d00914b6a538f9003c5d940b909fdce42a7/apprise_api/api/management/__init__.py -------------------------------------------------------------------------------- /apprise_api/api/management/commands/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/caronc/apprise-api/3fde6d00914b6a538f9003c5d940b909fdce42a7/apprise_api/api/management/commands/__init__.py -------------------------------------------------------------------------------- /apprise_api/api/management/commands/storeprune.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Copyright (C) 2023 Chris Caron 4 | # All rights reserved. 5 | # 6 | # This code is licensed under the MIT License. 7 | # 8 | # Permission is hereby granted, free of charge, to any person obtaining a copy 9 | # of this software and associated documentation files(the "Software"), to deal 10 | # in the Software without restriction, including without limitation the rights 11 | # to use, copy, modify, merge, publish, distribute, sublicense, and / or sell 12 | # copies of the Software, and to permit persons to whom the Software is 13 | # furnished to do so, subject to the following conditions : 14 | # 15 | # The above copyright notice and this permission notice shall be included in 16 | # all copies or substantial portions of the Software. 17 | # 18 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE 21 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 24 | # THE SOFTWARE. 25 | 26 | from django.core.management.base import BaseCommand 27 | from django.conf import settings 28 | import apprise 29 | 30 | 31 | class Command(BaseCommand): 32 | help = f"Prune all persistent content older then {settings.APPRISE_STORAGE_PRUNE_DAYS} days()" 33 | 34 | def add_arguments(self, parser): 35 | parser.add_argument("-d", "--days", type=int, default=settings.APPRISE_STORAGE_PRUNE_DAYS) 36 | 37 | def handle(self, *args, **options): 38 | # Persistent Storage cleanup 39 | apprise.PersistentStore.disk_prune( 40 | path=settings.APPRISE_STORAGE_DIR, 41 | expires=options["days"] * 86400, action=True, 42 | ) 43 | self.stdout.write( 44 | self.style.SUCCESS('Successfully pruned persistent storeage (days: %d)' % options["days"]) 45 | ) 46 | -------------------------------------------------------------------------------- /apprise_api/api/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/caronc/apprise-api/3fde6d00914b6a538f9003c5d940b909fdce42a7/apprise_api/api/migrations/__init__.py -------------------------------------------------------------------------------- /apprise_api/api/models.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/caronc/apprise-api/3fde6d00914b6a538f9003c5d940b909fdce42a7/apprise_api/api/models.py -------------------------------------------------------------------------------- /apprise_api/api/payload_mapper.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Copyright (C) 2024 Chris Caron 4 | # All rights reserved. 5 | # 6 | # This code is licensed under the MIT License. 7 | # 8 | # Permission is hereby granted, free of charge, to any person obtaining a copy 9 | # of this software and associated documentation files(the "Software"), to deal 10 | # in the Software without restriction, including without limitation the rights 11 | # to use, copy, modify, merge, publish, distribute, sublicense, and / or sell 12 | # copies of the Software, and to permit persons to whom the Software is 13 | # furnished to do so, subject to the following conditions : 14 | # 15 | # The above copyright notice and this permission notice shall be included in 16 | # all copies or substantial portions of the Software. 17 | # 18 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE 21 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 24 | # THE SOFTWARE. 25 | from api.forms import NotifyForm 26 | 27 | # import the logging library 28 | import logging 29 | 30 | # Get an instance of a logger 31 | logger = logging.getLogger('django') 32 | 33 | 34 | def remap_fields(rules, payload, form=None): 35 | """ 36 | Remaps fields in the payload provided based on the rules provided 37 | 38 | The key value of the dictionary identifies the payload key type you 39 | wish to alter. If there is no value defined, then the entry is removed 40 | 41 | If there is a value provided, then it's key is swapped into the new key 42 | provided. 43 | 44 | The purpose of this function is to allow people to re-map the fields 45 | that are being posted to the Apprise API before hand. Mapping them 46 | can allow 3rd party programs that post 'subject' and 'content' to 47 | be remapped to say 'title' and 'body' respectively 48 | 49 | """ 50 | 51 | # Prepare our Form (identifies our expected keys) 52 | form = NotifyForm() if form is None else form 53 | 54 | # First generate our expected keys; only these can be mapped 55 | expected_keys = set(form.fields.keys()) 56 | for _key, value in rules.items(): 57 | 58 | key = _key.lower() 59 | if key in payload and not value: 60 | # Remove element 61 | del payload[key] 62 | continue 63 | 64 | vkey = value.lower() 65 | if vkey in expected_keys and key in payload: 66 | if key not in expected_keys or vkey not in payload: 67 | # replace 68 | payload[vkey] = payload[key] 69 | del payload[key] 70 | 71 | elif vkey in payload: 72 | # swap 73 | _tmp = payload[vkey] 74 | payload[vkey] = payload[key] 75 | payload[key] = _tmp 76 | 77 | elif key in expected_keys or key in payload: 78 | # assignment 79 | payload[key] = value 80 | 81 | return True 82 | -------------------------------------------------------------------------------- /apprise_api/api/templates/base.html: -------------------------------------------------------------------------------- 1 | {% load static %} 2 | {% load i18n %} 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | {% block title %}{% trans "Apprise API" %}{% endblock %} 28 | 29 | 30 | 31 |
32 | 33 | 45 | 46 |
47 | 48 |
49 | {% if STATEFUL_MODE != 'disabled' %} 50 | 62 | {% endif %} 63 | 76 | 83 | {% block menu %}{% endblock %} 84 |
85 | 86 |
87 | 109 | {% block body %}{% endblock %} 110 |
111 | 112 |
113 |
114 | 168 | 169 | 170 | 171 | -------------------------------------------------------------------------------- /apprise_api/api/templates/config_list.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% load i18n %} 3 | {% block body %} 4 |

{% trans "Configuration List" %}

5 | {% if keys %} 6 | 12 | {% else %} 13 |

{% blocktrans %}There is no configuration defined.{% endblocktrans %}

14 | {% endif %} 15 | {% endblock %} 16 | -------------------------------------------------------------------------------- /apprise_api/api/templates/details.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% load i18n %} 3 | 4 | {% block body %} 5 |

{% trans 'Apprise Details' %}

6 |

7 | {% url 'details' as href %} 8 | {% blocktrans %}The following services are supported by this Apprise instance.{% endblocktrans %} 9 |

10 |

11 | {% if show_all %} 12 | {% blocktrans %}To see a simplified listing that only identifies the Apprise services enabled click here.{% endblocktrans %} 13 | {% else %} 14 | {% blocktrans %}To see a listing that identifies all of Apprise services available to this version (enabled or not) click here.{% endblocktrans %} 15 | {% endif %} 16 |

    17 |
  • 18 | chevron_right{% blocktrans %}Apprise Version:{% endblocktrans %} {{ details.version }} 19 |
  • 20 |
21 |
    22 | {% for entry in details.schemas %} 23 |
  • 24 |
    25 | {{ forloop.counter|stringformat:"03d"}} chevron_right 26 | {% if show_all %}{% if entry.enabled %}check_circle{%else%}remove_circle{%endif%}{% endif%} 27 |
    {{ entry.service_name }}
    28 | 34 |
    35 |
    36 |
    {{ entry.service_name }}
    37 | {% if show_all and not entry.enabled %} 38 | report{% blocktrans %}Note: This service is unavailable due to the service being disabled by the administrator or the required libraries needed to drive it is not installed or functioning correctly.{% endblocktrans %} 39 | {% endif %} 40 |
    41 | 42 |
      43 |
    • {% blocktrans %}Category{% endblocktrans %}: {{entry.category}}
    • 44 | {% if entry.protocols %} 45 |
    • {% blocktrans %}Insecure Schema(s){% endblocktrans %}: {{ entry.protocols|join:", " }}
    • 46 | {% endif %} 47 | {% if entry.secure_protocols %} 48 |
    • {% blocktrans %}Secure Schema(s){% endblocktrans %}: {{ entry.secure_protocols|join:", " }}
    • 49 | {% endif %} 50 |
    • 
       51 |                   # {% blocktrans %}Apprise URL Formatting{% endblocktrans %}
      52 | {% for url in entry.details.templates %} 53 | {{url}}
      54 | {% endfor %} 55 |
      56 |
    • 57 |
    • {% blocktrans %}For more details and additional Apprise configuration options available to this service:{% endblocktrans %} 58 | Click Here 59 |
    60 |
    61 |
  • 62 | {% endfor %} 63 |
64 |

65 | 66 |
67 |

{% trans 'API Endpoints' %}

68 |

69 | {% blocktrans %}Developers who wish to receive this result set in a JSON parseable string for their application can perform the following to achive this:{% endblocktrans %} 70 |

71 |
    72 |
  • 73 |
    74 | codeCurl Example 75 |
    76 |
    77 |
    
     78 |               #{% blocktrans %}Retrieve JSON Formatted Apprise Details{% endblocktrans %}
    79 | curl -H "Accept: application/json" \
    80 |     "{{ request.scheme }}://{{ request.META.HTTP_HOST }}{{ BASE_URL }}/details/{% if show_all %}?all=yes{% endif %}" 81 |
    82 |
    83 |
  • 84 |
  • 85 |
    86 | codePython Example 87 |
    88 |
    89 |
    
     90 |               import json
    91 | from urllib.request import Request
    92 |
    # The URL
    93 | req = Request(
    94 |     "{{ request.scheme }}://{{ request.META.HTTP_HOST }}{{ BASE_URL }}/details/{% if show_all %}?all=yes{% endif %}",
    95 |     json.dumps(payload).encode('utf-8'),
    96 |     {"Accept": "application/json"},
    97 |     method='GET',
    98 | ) 99 |
    100 |
    101 |
  • 102 |
  • 103 |
    104 | codePHP Example 105 |
    106 |
    107 |
    
    108 |               <?php
    109 |
    110 | // The URL
    111 | $url = '{{ request.scheme }}://{{ request.META.HTTP_HOST }}{{ BASE_URL }}/details/{% if show_all %}?all=yes{% endif %}';
    112 |
    113 | //Initiate cURL.
    114 | $ch = curl_init($url);
    115 |
    116 | //Set the content type to application/json
    117 | curl_setopt($ch, CURLOPT_HTTPHEADER, array('Accept: application/json'));
    118 |
    119 | //Execute the request
    120 | $result = curl_exec($ch); 121 |
    122 |
    123 |
  • 124 |
125 |

126 | {% blocktrans %}More details on the JSON format can be found here.{% endblocktrans %} 127 |

128 | {% endblock %} 129 | -------------------------------------------------------------------------------- /apprise_api/api/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/caronc/apprise-api/3fde6d00914b6a538f9003c5d940b909fdce42a7/apprise_api/api/tests/__init__.py -------------------------------------------------------------------------------- /apprise_api/api/tests/test_cli.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Copyright (C) 2023 Chris Caron 4 | # All rights reserved. 5 | # 6 | # This code is licensed under the MIT License. 7 | # 8 | # Permission is hereby granted, free of charge, to any person obtaining a copy 9 | # of this software and associated documentation files(the "Software"), to deal 10 | # in the Software without restriction, including without limitation the rights 11 | # to use, copy, modify, merge, publish, distribute, sublicense, and / or sell 12 | # copies of the Software, and to permit persons to whom the Software is 13 | # furnished to do so, subject to the following conditions : 14 | # 15 | # The above copyright notice and this permission notice shall be included in 16 | # all copies or substantial portions of the Software. 17 | # 18 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE 21 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 24 | # THE SOFTWARE. 25 | 26 | import io 27 | from django.test import SimpleTestCase 28 | from django.core import management 29 | 30 | 31 | class CommandTests(SimpleTestCase): 32 | 33 | def test_command_style(self): 34 | out = io.StringIO() 35 | management.call_command('storeprune', days=40, stdout=out) 36 | -------------------------------------------------------------------------------- /apprise_api/api/tests/test_config_cache.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Copyright (C) 2019 Chris Caron 4 | # All rights reserved. 5 | # 6 | # This code is licensed under the MIT License. 7 | # 8 | # Permission is hereby granted, free of charge, to any person obtaining a copy 9 | # of this software and associated documentation files(the "Software"), to deal 10 | # in the Software without restriction, including without limitation the rights 11 | # to use, copy, modify, merge, publish, distribute, sublicense, and / or sell 12 | # copies of the Software, and to permit persons to whom the Software is 13 | # furnished to do so, subject to the following conditions : 14 | # 15 | # The above copyright notice and this permission notice shall be included in 16 | # all copies or substantial portions of the Software. 17 | # 18 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE 21 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 24 | # THE SOFTWARE. 25 | import os 26 | 27 | from ..utils import AppriseConfigCache 28 | from ..utils import AppriseStoreMode 29 | from ..utils import SimpleFileExtension 30 | from apprise import ConfigFormat 31 | from unittest.mock import patch 32 | from unittest.mock import mock_open 33 | import errno 34 | 35 | 36 | def test_apprise_config_io_hash_mode(tmpdir): 37 | """ 38 | Test Apprise Config Disk Put/Get using HASH mode 39 | """ 40 | content = 'mailto://test:pass@gmail.com' 41 | key = 'test_apprise_config_io_hash' 42 | 43 | # Create our object to work with 44 | acc_obj = AppriseConfigCache(str(tmpdir), mode=AppriseStoreMode.HASH) 45 | 46 | # Verify that the content doesn't already exist 47 | assert acc_obj.get(key) == (None, '') 48 | 49 | # Write our content assigned to our key 50 | assert acc_obj.put(key, content, ConfigFormat.TEXT) 51 | 52 | # Test the handling of underlining disk/write exceptions 53 | with patch('gzip.open') as mock_gzopen: 54 | mock_gzopen.side_effect = OSError() 55 | # We'll fail to write our key now 56 | assert not acc_obj.put(key, content, ConfigFormat.TEXT) 57 | 58 | # Get path details 59 | conf_dir, _ = acc_obj.path(key) 60 | 61 | # List content of directory 62 | contents = os.listdir(conf_dir) 63 | 64 | # There should be just 1 new file in this directory 65 | assert len(contents) == 1 66 | assert contents[0].endswith('.{}'.format(ConfigFormat.TEXT)) 67 | 68 | # Verify that the content is retrievable 69 | assert acc_obj.get(key) == (content, ConfigFormat.TEXT) 70 | 71 | # Test the handling of underlining disk/read exceptions 72 | with patch('gzip.open') as mock_gzopen: 73 | mock_gzopen.side_effect = OSError() 74 | # We'll fail to read our key now 75 | assert acc_obj.get(key) == (None, None) 76 | 77 | # Tidy up our content 78 | assert acc_obj.clear(key) is True 79 | 80 | # But the second time is okay as it no longer exists 81 | assert acc_obj.clear(key) is None 82 | 83 | with patch('os.remove') as mock_remove: 84 | mock_remove.side_effect = OSError(errno.EPERM) 85 | # OSError 86 | assert acc_obj.clear(key) is False 87 | 88 | # If we try to put the same file, we'll fail since 89 | # one exists there already 90 | assert not acc_obj.put(key, content, ConfigFormat.TEXT) 91 | 92 | # Now test with YAML file 93 | content = """ 94 | version: 1 95 | 96 | urls: 97 | - windows:// 98 | """ 99 | 100 | # Write our content assigned to our key 101 | # This should gracefully clear the TEXT entry that was 102 | # previously in the spot 103 | assert acc_obj.put(key, content, ConfigFormat.YAML) 104 | 105 | # List content of directory 106 | contents = os.listdir(conf_dir) 107 | 108 | # There should STILL be just 1 new file in this directory 109 | assert len(contents) == 1 110 | assert contents[0].endswith('.{}'.format(ConfigFormat.YAML)) 111 | 112 | # Verify that the content is retrievable 113 | assert acc_obj.get(key) == (content, ConfigFormat.YAML) 114 | 115 | 116 | def test_apprise_config_list_simple_mode(tmpdir): 117 | """ 118 | Test Apprise Config Keys List using SIMPLE mode 119 | """ 120 | # Create our object to work with 121 | acc_obj = AppriseConfigCache(str(tmpdir), mode=AppriseStoreMode.SIMPLE) 122 | 123 | # Add a hidden file to the config directory (which should be ignored) 124 | hidden_file = os.path.join(str(tmpdir), '.hidden') 125 | with open(hidden_file, 'w') as f: 126 | f.write('hidden file') 127 | 128 | # Write 5 text configs and 5 yaml configs 129 | content_text = 'mailto://test:pass@gmail.com' 130 | content_yaml = """ 131 | version: 1 132 | urls: 133 | - windows:// 134 | """ 135 | text_key_tpl = 'test_apprise_config_list_simple_text_{}' 136 | yaml_key_tpl = 'test_apprise_config_list_simple_yaml_{}' 137 | text_keys = [text_key_tpl.format(i) for i in range(5)] 138 | yaml_keys = [yaml_key_tpl.format(i) for i in range(5)] 139 | key = None 140 | for key in text_keys: 141 | assert acc_obj.put(key, content_text, ConfigFormat.TEXT) 142 | for key in yaml_keys: 143 | assert acc_obj.put(key, content_yaml, ConfigFormat.YAML) 144 | 145 | # Ensure the 10 configuration files (plus the hidden file) are the only 146 | # contents of the directory 147 | conf_dir, _ = acc_obj.path(key) 148 | contents = os.listdir(conf_dir) 149 | assert len(contents) == 11 150 | 151 | keys = acc_obj.keys() 152 | assert len(keys) == 10 153 | assert sorted(keys) == sorted(text_keys + yaml_keys) 154 | 155 | 156 | def test_apprise_config_list_hash_mode(tmpdir): 157 | """ 158 | Test Apprise Config Keys List using HASH mode 159 | """ 160 | # Create our object to work with 161 | acc_obj = AppriseConfigCache(str(tmpdir), mode=AppriseStoreMode.HASH) 162 | 163 | # Add a hidden file to the config directory (which should be ignored) 164 | hidden_file = os.path.join(str(tmpdir), '.hidden') 165 | with open(hidden_file, 'w') as f: 166 | f.write('hidden file') 167 | 168 | # Write 5 text configs and 5 yaml configs 169 | content_text = 'mailto://test:pass@gmail.com' 170 | content_yaml = """ 171 | version: 1 172 | urls: 173 | - windows:// 174 | """ 175 | text_key_tpl = 'test_apprise_config_list_simple_text_{}' 176 | yaml_key_tpl = 'test_apprise_config_list_simple_yaml_{}' 177 | text_keys = [text_key_tpl.format(i) for i in range(5)] 178 | yaml_keys = [yaml_key_tpl.format(i) for i in range(5)] 179 | key = None 180 | for key in text_keys: 181 | assert acc_obj.put(key, content_text, ConfigFormat.TEXT) 182 | for key in yaml_keys: 183 | assert acc_obj.put(key, content_yaml, ConfigFormat.YAML) 184 | 185 | # Ensure the 10 configuration files (plus the hidden file) are the only 186 | # contents of the directory 187 | conf_dir, _ = acc_obj.path(key) 188 | contents = os.listdir(conf_dir) 189 | assert len(contents) == 1 190 | 191 | # does not search on hash mode 192 | keys = acc_obj.keys() 193 | assert len(keys) == 0 194 | 195 | 196 | def test_apprise_config_io_simple_mode(tmpdir): 197 | """ 198 | Test Apprise Config Disk Put/Get using SIMPLE mode 199 | """ 200 | content = 'mailto://test:pass@gmail.com' 201 | key = 'test_apprise_config_io_simple' 202 | 203 | # Create our object to work with 204 | acc_obj = AppriseConfigCache(str(tmpdir), mode=AppriseStoreMode.SIMPLE) 205 | 206 | # Verify that the content doesn't already exist 207 | assert acc_obj.get(key) == (None, '') 208 | 209 | # Write our content assigned to our key 210 | assert acc_obj.put(key, content, ConfigFormat.TEXT) 211 | 212 | m = mock_open() 213 | m.side_effect = OSError() 214 | with patch('builtins.open', m): 215 | # We'll fail to write our key now 216 | assert not acc_obj.put(key, content, ConfigFormat.TEXT) 217 | 218 | # Get path details 219 | conf_dir, _ = acc_obj.path(key) 220 | 221 | # List content of directory 222 | contents = os.listdir(conf_dir) 223 | 224 | # There should be just 1 new file in this directory 225 | assert len(contents) == 1 226 | assert contents[0].endswith('.{}'.format(SimpleFileExtension.TEXT)) 227 | 228 | # Verify that the content is retrievable 229 | assert acc_obj.get(key) == (content, ConfigFormat.TEXT) 230 | 231 | # Test the handling of underlining disk/read exceptions 232 | with patch('builtins.open', m) as mock__open: 233 | mock__open.side_effect = OSError() 234 | # We'll fail to read our key now 235 | assert acc_obj.get(key) == (None, None) 236 | 237 | # Tidy up our content 238 | assert acc_obj.clear(key) is True 239 | 240 | # But the second time is okay as it no longer exists 241 | assert acc_obj.clear(key) is None 242 | 243 | with patch('os.remove') as mock_remove: 244 | mock_remove.side_effect = OSError(errno.EPERM) 245 | # OSError 246 | assert acc_obj.clear(key) is False 247 | 248 | # If we try to put the same file, we'll fail since 249 | # one exists there already 250 | assert not acc_obj.put(key, content, ConfigFormat.TEXT) 251 | 252 | # Now test with YAML file 253 | content = """ 254 | version: 1 255 | 256 | urls: 257 | - windows:// 258 | """ 259 | 260 | # Write our content assigned to our key 261 | # This should gracefully clear the TEXT entry that was 262 | # previously in the spot 263 | assert acc_obj.put(key, content, ConfigFormat.YAML) 264 | 265 | # List content of directory 266 | contents = os.listdir(conf_dir) 267 | 268 | # There should STILL be just 1 new file in this directory 269 | assert len(contents) == 1 270 | assert contents[0].endswith('.{}'.format(SimpleFileExtension.YAML)) 271 | 272 | # Verify that the content is retrievable 273 | assert acc_obj.get(key) == (content, ConfigFormat.YAML) 274 | 275 | 276 | def test_apprise_config_io_disabled_mode(tmpdir): 277 | """ 278 | Test Apprise Config Disk Put/Get using DISABLED mode 279 | """ 280 | content = 'mailto://test:pass@gmail.com' 281 | key = 'test_apprise_config_io_disabled' 282 | 283 | # Create our object to work with using an invalid mode 284 | acc_obj = AppriseConfigCache(str(tmpdir), mode="invalid") 285 | 286 | # We always fall back to disabled if we can't interpret the mode 287 | assert acc_obj.mode is AppriseStoreMode.DISABLED 288 | 289 | # Create our object to work with 290 | acc_obj = AppriseConfigCache(str(tmpdir), mode=AppriseStoreMode.DISABLED) 291 | 292 | # Verify that the content doesn't already exist 293 | assert acc_obj.get(key) == (None, '') 294 | 295 | # Write our content assigned to our key 296 | # This isn't allowed 297 | assert acc_obj.put(key, content, ConfigFormat.TEXT) is False 298 | 299 | # Get path details 300 | conf_dir, _ = acc_obj.path(key) 301 | 302 | # List content of directory 303 | contents = os.listdir(conf_dir) 304 | 305 | # There should never be an entry 306 | assert len(contents) == 0 307 | 308 | # Content never exists 309 | assert acc_obj.clear(key) is None 310 | -------------------------------------------------------------------------------- /apprise_api/api/tests/test_del.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Copyright (C) 2019 Chris Caron 4 | # All rights reserved. 5 | # 6 | # This code is licensed under the MIT License. 7 | # 8 | # Permission is hereby granted, free of charge, to any person obtaining a copy 9 | # of this software and associated documentation files(the "Software"), to deal 10 | # in the Software without restriction, including without limitation the rights 11 | # to use, copy, modify, merge, publish, distribute, sublicense, and / or sell 12 | # copies of the Software, and to permit persons to whom the Software is 13 | # furnished to do so, subject to the following conditions : 14 | # 15 | # The above copyright notice and this permission notice shall be included in 16 | # all copies or substantial portions of the Software. 17 | # 18 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE 21 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 24 | # THE SOFTWARE. 25 | from django.test import SimpleTestCase 26 | from django.test.utils import override_settings 27 | from unittest.mock import patch 28 | import hashlib 29 | 30 | 31 | class DelTests(SimpleTestCase): 32 | 33 | def test_del_get_invalid_key_status_code(self): 34 | """ 35 | Test GET requests to invalid key 36 | """ 37 | response = self.client.get('/del/**invalid-key**') 38 | assert response.status_code == 404 39 | 40 | def test_key_lengths(self): 41 | """ 42 | Test our key lengths 43 | """ 44 | 45 | # our key to use 46 | h = hashlib.sha512() 47 | h.update(b'string') 48 | key = h.hexdigest() 49 | 50 | # Our limit 51 | assert len(key) == 128 52 | 53 | # Add our URL 54 | response = self.client.post( 55 | '/add/{}'.format(key), {'urls': 'mailto://user:pass@yahoo.ca'}) 56 | assert response.status_code == 200 57 | 58 | # remove a key that is too long 59 | response = self.client.post('/del/{}'.format(key + 'x')) 60 | assert response.status_code == 404 61 | 62 | # remove the key 63 | response = self.client.post('/del/{}'.format(key)) 64 | assert response.status_code == 200 65 | 66 | # Test again; key is gone 67 | response = self.client.post('/del/{}'.format(key)) 68 | assert response.status_code == 204 69 | 70 | @override_settings(APPRISE_CONFIG_LOCK=True) 71 | def test_del_with_lock(self): 72 | """ 73 | Test deleting a configuration by URLs with lock set won't work 74 | """ 75 | # our key to use 76 | key = 'test_delete_with_lock' 77 | 78 | # We simply do not have permission to do so 79 | response = self.client.post('/del/{}'.format(key)) 80 | assert response.status_code == 403 81 | 82 | def test_del_post(self): 83 | """ 84 | Test DEL POST 85 | """ 86 | # our key to use 87 | key = 'test_delete' 88 | 89 | # Invalid Key 90 | response = self.client.post('/del/**invalid-key**') 91 | assert response.status_code == 404 92 | 93 | # A key that just simply isn't present 94 | response = self.client.post('/del/{}'.format(key)) 95 | assert response.status_code == 204 96 | 97 | # Add our key 98 | response = self.client.post( 99 | '/add/{}'.format(key), {'urls': 'mailto://user:pass@yahoo.ca'}) 100 | assert response.status_code == 200 101 | 102 | # Test removing key when the OS just can't do it: 103 | with patch('os.remove', side_effect=OSError): 104 | # We can now remove the key 105 | response = self.client.post('/del/{}'.format(key)) 106 | assert response.status_code == 500 107 | 108 | # We can now remove the key 109 | response = self.client.post('/del/{}'.format(key)) 110 | assert response.status_code == 200 111 | 112 | # Key has already been removed 113 | response = self.client.post('/del/{}'.format(key)) 114 | assert response.status_code == 204 115 | -------------------------------------------------------------------------------- /apprise_api/api/tests/test_details.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Copyright (C) 2023 Chris Caron 4 | # All rights reserved. 5 | # 6 | # This code is licensed under the MIT License. 7 | # 8 | # Permission is hereby granted, free of charge, to any person obtaining a copy 9 | # of this software and associated documentation files(the "Software"), to deal 10 | # in the Software without restriction, including without limitation the rights 11 | # to use, copy, modify, merge, publish, distribute, sublicense, and / or sell 12 | # copies of the Software, and to permit persons to whom the Software is 13 | # furnished to do so, subject to the following conditions : 14 | # 15 | # The above copyright notice and this permission notice shall be included in 16 | # all copies or substantial portions of the Software. 17 | # 18 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE 21 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 24 | # THE SOFTWARE. 25 | from django.test import SimpleTestCase 26 | 27 | 28 | class DetailTests(SimpleTestCase): 29 | 30 | def test_post_not_supported(self): 31 | """ 32 | Test POST requests 33 | """ 34 | response = self.client.post('/details') 35 | # 405 as posting is not allowed 36 | assert response.status_code == 405 37 | 38 | def test_details_simple(self): 39 | """ 40 | Test retrieving details 41 | """ 42 | 43 | # Nothing to return 44 | response = self.client.get('/details') 45 | self.assertEqual(response.status_code, 200) 46 | assert response['Content-Type'].startswith('text/html') 47 | 48 | # JSON Response 49 | response = self.client.get( 50 | '/details', content_type='application/json', 51 | **{'HTTP_CONTENT_TYPE': 'application/json'}) 52 | self.assertEqual(response.status_code, 200) 53 | assert response['Content-Type'].startswith('application/json') 54 | 55 | # JSON Response 56 | response = self.client.get( 57 | '/details', content_type='application/json', 58 | **{'HTTP_ACCEPT': 'application/json'}) 59 | self.assertEqual(response.status_code, 200) 60 | assert response['Content-Type'].startswith('application/json') 61 | 62 | response = self.client.get('/details?all=yes') 63 | self.assertEqual(response.status_code, 200) 64 | assert response['Content-Type'].startswith('text/html') 65 | 66 | # JSON Response 67 | response = self.client.get( 68 | '/details?all=yes', content_type='application/json', 69 | **{'HTTP_CONTENT_TYPE': 'application/json'}) 70 | self.assertEqual(response.status_code, 200) 71 | assert response['Content-Type'].startswith('application/json') 72 | 73 | # JSON Response 74 | response = self.client.get( 75 | '/details?all=yes', content_type='application/json', 76 | **{'HTTP_ACCEPT': 'application/json'}) 77 | self.assertEqual(response.status_code, 200) 78 | assert response['Content-Type'].startswith('application/json') 79 | -------------------------------------------------------------------------------- /apprise_api/api/tests/test_get.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Copyright (C) 2019 Chris Caron 4 | # All rights reserved. 5 | # 6 | # This code is licensed under the MIT License. 7 | # 8 | # Permission is hereby granted, free of charge, to any person obtaining a copy 9 | # of this software and associated documentation files(the "Software"), to deal 10 | # in the Software without restriction, including without limitation the rights 11 | # to use, copy, modify, merge, publish, distribute, sublicense, and / or sell 12 | # copies of the Software, and to permit persons to whom the Software is 13 | # furnished to do so, subject to the following conditions : 14 | # 15 | # The above copyright notice and this permission notice shall be included in 16 | # all copies or substantial portions of the Software. 17 | # 18 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE 21 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 24 | # THE SOFTWARE. 25 | from django.test import SimpleTestCase 26 | from unittest.mock import patch 27 | 28 | 29 | class GetTests(SimpleTestCase): 30 | 31 | def test_get_invalid_key_status_code(self): 32 | """ 33 | Test GET requests to invalid key 34 | """ 35 | response = self.client.get('/get/**invalid-key**') 36 | assert response.status_code == 404 37 | 38 | def test_get_config(self): 39 | """ 40 | Test retrieving configuration 41 | """ 42 | 43 | # our key to use 44 | key = 'test_get_config_' 45 | 46 | # GET returns 405 (not allowed) 47 | response = self.client.get('/get/{}'.format(key)) 48 | assert response.status_code == 405 49 | 50 | # No content saved to the location yet 51 | response = self.client.post('/get/{}'.format(key)) 52 | self.assertEqual(response.status_code, 204) 53 | 54 | # Add some content 55 | response = self.client.post( 56 | '/add/{}'.format(key), 57 | {'urls': 'mailto://user:pass@yahoo.ca'}) 58 | assert response.status_code == 200 59 | 60 | # Handle case when we try to retrieve our content but we have no idea 61 | # what the format is in. Essentialy there had to have been disk 62 | # corruption here or someone meddling with the backend. 63 | with patch('gzip.open', side_effect=OSError): 64 | response = self.client.post('/get/{}'.format(key)) 65 | assert response.status_code == 500 66 | 67 | # Now we should be able to see our content 68 | response = self.client.post('/get/{}'.format(key)) 69 | assert response.status_code == 200 70 | 71 | # Add a YAML file 72 | response = self.client.post( 73 | '/add/{}'.format(key), { 74 | 'format': 'yaml', 75 | 'config': """ 76 | urls: 77 | - dbus://"""}) 78 | assert response.status_code == 200 79 | 80 | # Now retrieve our YAML configuration 81 | response = self.client.post('/get/{}'.format(key)) 82 | assert response.status_code == 200 83 | 84 | # Verify that the correct Content-Type is set in the header of the 85 | # response 86 | assert 'Content-Type' in response 87 | assert response['Content-Type'].startswith('text/yaml') 88 | -------------------------------------------------------------------------------- /apprise_api/api/tests/test_healthecheck.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Copyright (C) 2024 Chris Caron 4 | # All rights reserved. 5 | # 6 | # This code is licensed under the MIT License. 7 | # 8 | # Permission is hereby granted, free of charge, to any person obtaining a copy 9 | # of this software and associated documentation files(the "Software"), to deal 10 | # in the Software without restriction, including without limitation the rights 11 | # to use, copy, modify, merge, publish, distribute, sublicense, and / or sell 12 | # copies of the Software, and to permit persons to whom the Software is 13 | # furnished to do so, subject to the following conditions : 14 | # 15 | # The above copyright notice and this permission notice shall be included in 16 | # all copies or substantial portions of the Software. 17 | # 18 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE 21 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 24 | # THE SOFTWARE. 25 | import mock 26 | from django.test import SimpleTestCase 27 | from json import loads 28 | from django.test.utils import override_settings 29 | from ..utils import healthcheck 30 | 31 | 32 | class HealthCheckTests(SimpleTestCase): 33 | 34 | def test_post_not_supported(self): 35 | """ 36 | Test POST requests 37 | """ 38 | response = self.client.post('/status') 39 | # 405 as posting is not allowed 40 | assert response.status_code == 405 41 | 42 | def test_healthcheck_simple(self): 43 | """ 44 | Test retrieving basic successful health-checks 45 | """ 46 | 47 | # First Status Check 48 | response = self.client.get('/status') 49 | self.assertEqual(response.status_code, 200) 50 | self.assertEqual(response.content, b'OK') 51 | assert response['Content-Type'].startswith('text/plain') 52 | 53 | # Second Status Check (Lazy Mode kicks in) 54 | response = self.client.get('/status') 55 | self.assertEqual(response.status_code, 200) 56 | self.assertEqual(response.content, b'OK') 57 | assert response['Content-Type'].startswith('text/plain') 58 | 59 | # JSON Response 60 | response = self.client.get( 61 | '/status', content_type='application/json', 62 | **{'HTTP_CONTENT_TYPE': 'application/json'}) 63 | self.assertEqual(response.status_code, 200) 64 | content = loads(response.content) 65 | assert content == { 66 | 'config_lock': False, 67 | 'attach_lock': False, 68 | 'status': { 69 | 'persistent_storage': True, 70 | 'can_write_config': True, 71 | 'can_write_attach': True, 72 | 'details': ['OK'] 73 | } 74 | } 75 | assert response['Content-Type'].startswith('application/json') 76 | 77 | with override_settings(APPRISE_CONFIG_LOCK=True): 78 | # Status Check (Form based) 79 | response = self.client.get('/status') 80 | self.assertEqual(response.status_code, 200) 81 | self.assertEqual(response.content, b'OK') 82 | assert response['Content-Type'].startswith('text/plain') 83 | 84 | # JSON Response 85 | response = self.client.get( 86 | '/status', content_type='application/json', 87 | **{'HTTP_CONTENT_TYPE': 'application/json'}) 88 | self.assertEqual(response.status_code, 200) 89 | content = loads(response.content) 90 | assert content == { 91 | 'config_lock': True, 92 | 'attach_lock': False, 93 | 'status': { 94 | 'persistent_storage': True, 95 | 'can_write_config': False, 96 | 'can_write_attach': True, 97 | 'details': ['OK'] 98 | } 99 | } 100 | 101 | with override_settings(APPRISE_STATEFUL_MODE='disabled'): 102 | # Status Check (Form based) 103 | response = self.client.get('/status') 104 | self.assertEqual(response.status_code, 200) 105 | self.assertEqual(response.content, b'OK') 106 | assert response['Content-Type'].startswith('text/plain') 107 | 108 | # JSON Response 109 | response = self.client.get( 110 | '/status', content_type='application/json', 111 | **{'HTTP_CONTENT_TYPE': 'application/json'}) 112 | self.assertEqual(response.status_code, 200) 113 | content = loads(response.content) 114 | assert content == { 115 | 'config_lock': False, 116 | 'attach_lock': False, 117 | 'status': { 118 | 'persistent_storage': True, 119 | 'can_write_config': False, 120 | 'can_write_attach': True, 121 | 'details': ['OK'] 122 | } 123 | } 124 | 125 | with override_settings(APPRISE_ATTACH_SIZE=0): 126 | # Status Check (Form based) 127 | response = self.client.get('/status') 128 | self.assertEqual(response.status_code, 200) 129 | self.assertEqual(response.content, b'OK') 130 | assert response['Content-Type'].startswith('text/plain') 131 | 132 | # JSON Response 133 | response = self.client.get( 134 | '/status', content_type='application/json', 135 | **{'HTTP_CONTENT_TYPE': 'application/json'}) 136 | self.assertEqual(response.status_code, 200) 137 | content = loads(response.content) 138 | assert content == { 139 | 'config_lock': False, 140 | 'attach_lock': True, 141 | 'status': { 142 | 'persistent_storage': True, 143 | 'can_write_config': True, 144 | 'can_write_attach': False, 145 | 'details': ['OK'] 146 | } 147 | } 148 | 149 | with override_settings(APPRISE_MAX_ATTACHMENTS=0): 150 | # Status Check (Form based) 151 | response = self.client.get('/status') 152 | self.assertEqual(response.status_code, 200) 153 | self.assertEqual(response.content, b'OK') 154 | assert response['Content-Type'].startswith('text/plain') 155 | 156 | # JSON Response 157 | response = self.client.get( 158 | '/status', content_type='application/json', 159 | **{'HTTP_CONTENT_TYPE': 'application/json'}) 160 | self.assertEqual(response.status_code, 200) 161 | content = loads(response.content) 162 | assert content == { 163 | 'config_lock': False, 164 | 'attach_lock': False, 165 | 'status': { 166 | 'persistent_storage': True, 167 | 'can_write_config': True, 168 | 'can_write_attach': True, 169 | 'details': ['OK'] 170 | } 171 | } 172 | 173 | def test_healthcheck_library(self): 174 | """ 175 | Test underlining healthcheck library 176 | """ 177 | 178 | result = healthcheck(lazy=True) 179 | assert result == { 180 | 'persistent_storage': True, 181 | 'can_write_config': True, 182 | 'can_write_attach': True, 183 | 'details': ['OK'] 184 | } 185 | 186 | # A Double lazy check 187 | result = healthcheck(lazy=True) 188 | assert result == { 189 | 'persistent_storage': True, 190 | 'can_write_config': True, 191 | 'can_write_attach': True, 192 | 'details': ['OK'] 193 | } 194 | 195 | # Force a lazy check where we can't acquire the modify time 196 | with mock.patch('os.path.getmtime') as mock_getmtime: 197 | mock_getmtime.side_effect = FileNotFoundError() 198 | result = healthcheck(lazy=True) 199 | # We still succeed; we just don't leverage our lazy check 200 | # which prevents addition (unnessisary) writes 201 | assert result == { 202 | 'persistent_storage': True, 203 | 'can_write_config': True, 204 | 'can_write_attach': True, 205 | 'details': ['OK'], 206 | } 207 | 208 | # Force a lazy check where we can't acquire the modify time 209 | with mock.patch('os.path.getmtime') as mock_getmtime: 210 | mock_getmtime.side_effect = OSError() 211 | result = healthcheck(lazy=True) 212 | # We still succeed; we just don't leverage our lazy check 213 | # which prevents addition (unnessisary) writes 214 | assert result == { 215 | 'persistent_storage': True, 216 | 'can_write_config': False, 217 | 'can_write_attach': False, 218 | 'details': [ 219 | 'CONFIG_PERMISSION_ISSUE', 220 | 'ATTACH_PERMISSION_ISSUE', 221 | ]} 222 | 223 | # Force a non-lazy check 224 | with mock.patch('os.makedirs') as mock_makedirs: 225 | mock_makedirs.side_effect = OSError() 226 | result = healthcheck(lazy=False) 227 | assert result == { 228 | 'persistent_storage': False, 229 | 'can_write_config': False, 230 | 'can_write_attach': False, 231 | 'details': [ 232 | 'CONFIG_PERMISSION_ISSUE', 233 | 'ATTACH_PERMISSION_ISSUE', 234 | 'STORE_PERMISSION_ISSUE', 235 | ]} 236 | 237 | with mock.patch('os.path.getmtime') as mock_getmtime: 238 | with mock.patch('os.fdopen', side_effect=OSError()): 239 | mock_getmtime.side_effect = OSError() 240 | mock_makedirs.side_effect = None 241 | result = healthcheck(lazy=False) 242 | assert result == { 243 | 'persistent_storage': True, 244 | 'can_write_config': False, 245 | 'can_write_attach': False, 246 | 'details': [ 247 | 'CONFIG_PERMISSION_ISSUE', 248 | 'ATTACH_PERMISSION_ISSUE', 249 | ]} 250 | 251 | with mock.patch('apprise.PersistentStore.flush', return_value=False): 252 | result = healthcheck(lazy=False) 253 | assert result == { 254 | 'persistent_storage': False, 255 | 'can_write_config': True, 256 | 'can_write_attach': True, 257 | 'details': [ 258 | 'STORE_PERMISSION_ISSUE', 259 | ]} 260 | 261 | # Test a case where we simply do not define a persistent store path 262 | # health checks will always disable persistent storage 263 | with override_settings(APPRISE_STORAGE_DIR=""): 264 | with mock.patch('apprise.PersistentStore.flush', return_value=False): 265 | result = healthcheck(lazy=False) 266 | assert result == { 267 | 'persistent_storage': False, 268 | 'can_write_config': True, 269 | 'can_write_attach': True, 270 | 'details': ['OK']} 271 | 272 | mock_makedirs.side_effect = (OSError(), OSError(), None, None, None, None) 273 | result = healthcheck(lazy=False) 274 | assert result == { 275 | 'persistent_storage': True, 276 | 'can_write_config': False, 277 | 'can_write_attach': False, 278 | 'details': [ 279 | 'CONFIG_PERMISSION_ISSUE', 280 | 'ATTACH_PERMISSION_ISSUE', 281 | ]} 282 | 283 | mock_makedirs.side_effect = (OSError(), None, None, None, None) 284 | result = healthcheck(lazy=False) 285 | assert result == { 286 | 'persistent_storage': True, 287 | 'can_write_config': False, 288 | 'can_write_attach': True, 289 | 'details': [ 290 | 'CONFIG_PERMISSION_ISSUE', 291 | ]} 292 | -------------------------------------------------------------------------------- /apprise_api/api/tests/test_json_urls.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Copyright (C) 2019 Chris Caron 4 | # All rights reserved. 5 | # 6 | # This code is licensed under the MIT License. 7 | # 8 | # Permission is hereby granted, free of charge, to any person obtaining a copy 9 | # of this software and associated documentation files(the "Software"), to deal 10 | # in the Software without restriction, including without limitation the rights 11 | # to use, copy, modify, merge, publish, distribute, sublicense, and / or sell 12 | # copies of the Software, and to permit persons to whom the Software is 13 | # furnished to do so, subject to the following conditions : 14 | # 15 | # The above copyright notice and this permission notice shall be included in 16 | # all copies or substantial portions of the Software. 17 | # 18 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE 21 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 24 | # THE SOFTWARE. 25 | from django.test import SimpleTestCase 26 | from django.test.utils import override_settings 27 | from unittest.mock import patch 28 | 29 | 30 | class JsonUrlsTests(SimpleTestCase): 31 | 32 | def test_get_invalid_key_status_code(self): 33 | """ 34 | Test GET requests to invalid key 35 | """ 36 | response = self.client.get('/get/**invalid-key**') 37 | assert response.status_code == 404 38 | 39 | def test_post_not_supported(self): 40 | """ 41 | Test POST requests with key 42 | """ 43 | response = self.client.post('/json/urls/test') 44 | # 405 as posting is not allowed 45 | assert response.status_code == 405 46 | 47 | def test_json_urls_config(self): 48 | """ 49 | Test retrieving configuration 50 | """ 51 | 52 | # our key to use 53 | key = 'test_json_urls_config' 54 | 55 | # Nothing to return 56 | response = self.client.get('/json/urls/{}'.format(key)) 57 | self.assertEqual(response.status_code, 204) 58 | 59 | # Add some content 60 | response = self.client.post( 61 | '/add/{}'.format(key), 62 | {'urls': 'mailto://user:pass@yahoo.ca'}) 63 | assert response.status_code == 200 64 | 65 | # Handle case when we try to retrieve our content but we have no idea 66 | # what the format is in. Essentialy there had to have been disk 67 | # corruption here or someone meddling with the backend. 68 | with patch('gzip.open', side_effect=OSError): 69 | response = self.client.get('/json/urls/{}'.format(key)) 70 | assert response.status_code == 500 71 | assert response['Content-Type'].startswith('application/json') 72 | assert 'tags' in response.json() 73 | assert 'urls' in response.json() 74 | 75 | # has error directive 76 | assert 'error' in response.json() 77 | 78 | # entries exist by are empty 79 | assert len(response.json()['tags']) == 0 80 | assert len(response.json()['urls']) == 0 81 | 82 | # Now we should be able to see our content 83 | response = self.client.get('/json/urls/{}'.format(key)) 84 | assert response.status_code == 200 85 | assert response['Content-Type'].startswith('application/json') 86 | assert 'tags' in response.json() 87 | assert 'urls' in response.json() 88 | 89 | # No errors occurred, therefore no error entry 90 | assert 'error' not in response.json() 91 | 92 | # No tags (but can be assumed "all") is always present 93 | assert len(response.json()['tags']) == 0 94 | 95 | # Same request as above but we set the privacy flag 96 | response = self.client.get('/json/urls/{}?privacy=1'.format(key)) 97 | assert response.status_code == 200 98 | assert response['Content-Type'].startswith('application/json') 99 | assert 'tags' in response.json() 100 | assert 'urls' in response.json() 101 | 102 | # No errors occurred, therefore no error entry 103 | assert 'error' not in response.json() 104 | 105 | # No tags (but can be assumed "all") is always present 106 | assert len(response.json()['tags']) == 0 107 | 108 | # One URL loaded 109 | assert len(response.json()['urls']) == 1 110 | assert 'url' in response.json()['urls'][0] 111 | assert 'tags' in response.json()['urls'][0] 112 | assert len(response.json()['urls'][0]['tags']) == 0 113 | 114 | # We can see that th URLs are not the same when the privacy flag is set 115 | without_privacy = \ 116 | self.client.get('/json/urls/{}?privacy=1'.format(key)) 117 | with_privacy = self.client.get('/json/urls/{}'.format(key)) 118 | assert with_privacy.json()['urls'][0] != \ 119 | without_privacy.json()['urls'][0] 120 | 121 | with override_settings(APPRISE_CONFIG_LOCK=True): 122 | # When our configuration lock is set, our result set enforces the 123 | # privacy flag even if it was otherwise set: 124 | with_privacy = \ 125 | self.client.get('/json/urls/{}?privacy=1'.format(key)) 126 | 127 | # But now they're the same under this new condition 128 | assert with_privacy.json()['urls'][0] == \ 129 | without_privacy.json()['urls'][0] 130 | 131 | # Add a YAML file 132 | response = self.client.post( 133 | '/add/{}'.format(key), { 134 | 'format': 'yaml', 135 | 'config': """ 136 | urls: 137 | - dbus://: 138 | - tag: tag1, tag2"""}) 139 | assert response.status_code == 200 140 | 141 | # Now retrieve our JSON resonse 142 | response = self.client.get('/json/urls/{}'.format(key)) 143 | assert response.status_code == 200 144 | 145 | # No errors occured, therefore no error entry 146 | assert 'error' not in response.json() 147 | 148 | # No tags (but can be assumed "all") is always present 149 | assert len(response.json()['tags']) == 2 150 | 151 | # One URL loaded 152 | assert len(response.json()['urls']) == 1 153 | assert 'url' in response.json()['urls'][0] 154 | assert 'tags' in response.json()['urls'][0] 155 | assert len(response.json()['urls'][0]['tags']) == 2 156 | 157 | # Verify that the correct Content-Type is set in the header of the 158 | # response 159 | assert 'Content-Type' in response 160 | assert response['Content-Type'].startswith('application/json') 161 | -------------------------------------------------------------------------------- /apprise_api/api/tests/test_manager.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Copyright (C) 2019 Chris Caron 4 | # All rights reserved. 5 | # 6 | # This code is licensed under the MIT License. 7 | # 8 | # Permission is hereby granted, free of charge, to any person obtaining a copy 9 | # of this software and associated documentation files(the "Software"), to deal 10 | # in the Software without restriction, including without limitation the rights 11 | # to use, copy, modify, merge, publish, distribute, sublicense, and / or sell 12 | # copies of the Software, and to permit persons to whom the Software is 13 | # furnished to do so, subject to the following conditions : 14 | # 15 | # The above copyright notice and this permission notice shall be included in 16 | # all copies or substantial portions of the Software. 17 | # 18 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE 21 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 24 | # THE SOFTWARE. 25 | from django.test import SimpleTestCase 26 | from django.test import override_settings 27 | 28 | 29 | class ManagerPageTests(SimpleTestCase): 30 | """ 31 | Manager Webpage testing 32 | """ 33 | 34 | def test_manage_status_code(self): 35 | """ 36 | General testing of management page 37 | """ 38 | # No permission to get keys 39 | response = self.client.get('/cfg/') 40 | assert response.status_code == 403 41 | 42 | with override_settings(APPRISE_ADMIN=True, APPRISE_STATEFUL_MODE='hash'): 43 | response = self.client.get('/cfg/') 44 | assert response.status_code == 403 45 | 46 | with override_settings(APPRISE_ADMIN=False, APPRISE_STATEFUL_MODE='simple'): 47 | response = self.client.get('/cfg/') 48 | assert response.status_code == 403 49 | 50 | with override_settings(APPRISE_ADMIN=False, APPRISE_STATEFUL_MODE='disabled'): 51 | response = self.client.get('/cfg/') 52 | assert response.status_code == 403 53 | 54 | with override_settings(APPRISE_ADMIN=True, APPRISE_STATEFUL_MODE='disabled'): 55 | response = self.client.get('/cfg/') 56 | assert response.status_code == 403 57 | 58 | # But only when the setting is enabled 59 | with override_settings(APPRISE_ADMIN=True, APPRISE_STATEFUL_MODE='simple'): 60 | response = self.client.get('/cfg/') 61 | assert response.status_code == 200 62 | 63 | # An invalid key was specified 64 | response = self.client.get('/cfg/**invalid-key**') 65 | assert response.status_code == 404 66 | 67 | # An invalid key was specified 68 | response = self.client.get('/cfg/valid-key') 69 | assert response.status_code == 200 70 | -------------------------------------------------------------------------------- /apprise_api/api/tests/test_payload_mapper.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Copyright (C) 2024 Chris Caron 4 | # All rights reserved. 5 | # 6 | # This code is licensed under the MIT License. 7 | # 8 | # Permission is hereby granted, free of charge, to any person obtaining a copy 9 | # of this software and associated documentation files(the "Software"), to deal 10 | # in the Software without restriction, including without limitation the rights 11 | # to use, copy, modify, merge, publish, distribute, sublicense, and / or sell 12 | # copies of the Software, and to permit persons to whom the Software is 13 | # furnished to do so, subject to the following conditions : 14 | # 15 | # The above copyright notice and this permission notice shall be included in 16 | # all copies or substantial portions of the Software. 17 | # 18 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE 21 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 24 | # THE SOFTWARE. 25 | from django.test import SimpleTestCase 26 | from ..payload_mapper import remap_fields 27 | 28 | 29 | class NotifyPayloadMapper(SimpleTestCase): 30 | """ 31 | Test Payload Mapper 32 | """ 33 | 34 | def test_remap_fields(self): 35 | """ 36 | Test payload re-mapper 37 | """ 38 | 39 | # 40 | # No rules defined 41 | # 42 | rules = {} 43 | payload = { 44 | 'format': 'markdown', 45 | 'title': 'title', 46 | 'body': '# body', 47 | } 48 | payload_orig = payload.copy() 49 | 50 | # Map our fields 51 | remap_fields(rules, payload) 52 | 53 | # no change is made 54 | assert payload == payload_orig 55 | 56 | # 57 | # rules defined - test 1 58 | # 59 | rules = { 60 | # map 'as' to 'format' 61 | 'as': 'format', 62 | # map 'subject' to 'title' 63 | 'subject': 'title', 64 | # map 'content' to 'body' 65 | 'content': 'body', 66 | # 'missing' is an invalid entry so this will be skipped 67 | 'unknown': 'missing', 68 | 69 | # Empty field 70 | 'attachment': '', 71 | 72 | # Garbage is an field that can be removed since it doesn't 73 | # conflict with the form 74 | 'garbage': '', 75 | 76 | # Tag 77 | 'tag': 'test', 78 | } 79 | payload = { 80 | 'as': 'markdown', 81 | 'subject': 'title', 82 | 'content': '# body', 83 | 'tag': '', 84 | 'unknown': 'hmm', 85 | 'attachment': '', 86 | 'garbage': '', 87 | } 88 | 89 | # Map our fields 90 | remap_fields(rules, payload) 91 | 92 | # Our field mappings have taken place 93 | assert payload == { 94 | 'tag': 'test', 95 | 'unknown': 'missing', 96 | 'format': 'markdown', 97 | 'title': 'title', 98 | 'body': '# body', 99 | } 100 | 101 | # 102 | # rules defined - test 2 103 | # 104 | rules = { 105 | # 106 | # map 'content' to 'body' 107 | 'content': 'body', 108 | # a double mapping to body will trigger an error 109 | 'message': 'body', 110 | # Swapping fields 111 | 'body': 'another set of data', 112 | } 113 | payload = { 114 | 'as': 'markdown', 115 | 'subject': 'title', 116 | 'content': '# content body', 117 | 'message': '# message body', 118 | 'body': 'another set of data', 119 | } 120 | 121 | # Map our fields 122 | remap_fields(rules, payload) 123 | 124 | # Our information gets swapped 125 | assert payload == { 126 | 'as': 'markdown', 127 | 'subject': 'title', 128 | 'body': 'another set of data', 129 | } 130 | 131 | # 132 | # swapping fields - test 3 133 | # 134 | rules = { 135 | # 136 | # map 'content' to 'body' 137 | 'title': 'body', 138 | } 139 | payload = { 140 | 'format': 'markdown', 141 | 'title': 'body', 142 | 'body': '# title', 143 | } 144 | 145 | # Map our fields 146 | remap_fields(rules, payload) 147 | 148 | # Our information gets swapped 149 | assert payload == { 150 | 'format': 'markdown', 151 | 'title': '# title', 152 | 'body': 'body', 153 | } 154 | 155 | # 156 | # swapping fields - test 4 157 | # 158 | rules = { 159 | # 160 | # map 'content' to 'body' 161 | 'title': 'body', 162 | } 163 | payload = { 164 | 'format': 'markdown', 165 | 'title': 'body', 166 | } 167 | 168 | # Map our fields 169 | remap_fields(rules, payload) 170 | 171 | # Our information gets swapped 172 | assert payload == { 173 | 'format': 'markdown', 174 | 'body': 'body', 175 | } 176 | 177 | # 178 | # swapping fields - test 5 179 | # 180 | rules = { 181 | # 182 | # map 'content' to 'body' 183 | 'content': 'body', 184 | } 185 | payload = { 186 | 'format': 'markdown', 187 | 'content': 'the message', 188 | 'body': 'to-be-replaced', 189 | } 190 | 191 | # Map our fields 192 | remap_fields(rules, payload) 193 | 194 | # Our information gets swapped 195 | assert payload == { 196 | 'format': 'markdown', 197 | 'body': 'the message', 198 | } 199 | 200 | # 201 | # mapping of fields don't align - test 6 202 | # 203 | rules = { 204 | 'payload': 'body', 205 | 'fmt': 'format', 206 | 'extra': 'tag', 207 | } 208 | payload = { 209 | 'format': 'markdown', 210 | 'type': 'info', 211 | 'title': '', 212 | 'body': '## test notifiction', 213 | 'attachment': None, 214 | 'tag': 'general', 215 | 'tags': '', 216 | } 217 | 218 | # Make a copy of our original payload 219 | payload_orig = payload.copy() 220 | 221 | # Map our fields 222 | remap_fields(rules, payload) 223 | 224 | # There are no rules applied since nothing aligned 225 | assert payload == payload_orig 226 | -------------------------------------------------------------------------------- /apprise_api/api/tests/test_utils.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Copyright (C) 2024 Chris Caron 4 | # All rights reserved. 5 | # 6 | # This code is licensed under the MIT License. 7 | # 8 | # Permission is hereby granted, free of charge, to any person obtaining a copy 9 | # of this software and associated documentation files(the "Software"), to deal 10 | # in the Software without restriction, including without limitation the rights 11 | # to use, copy, modify, merge, publish, distribute, sublicense, and / or sell 12 | # copies of the Software, and to permit persons to whom the Software is 13 | # furnished to do so, subject to the following conditions : 14 | # 15 | # The above copyright notice and this permission notice shall be included in 16 | # all copies or substantial portions of the Software. 17 | # 18 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE 21 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 24 | # THE SOFTWARE. 25 | import os 26 | import mock 27 | import tempfile 28 | from django.test import SimpleTestCase 29 | from .. import utils 30 | 31 | 32 | class UtilsTests(SimpleTestCase): 33 | 34 | def test_touchdir(self): 35 | """ 36 | Test touchdir() 37 | """ 38 | 39 | with tempfile.TemporaryDirectory() as tmpdir: 40 | with mock.patch('os.makedirs', side_effect=OSError()): 41 | assert utils.touchdir(os.path.join(tmpdir, 'tmp-file')) is False 42 | 43 | with mock.patch('os.makedirs', side_effect=FileExistsError()): 44 | # Dir doesn't exist 45 | assert utils.touchdir(os.path.join(tmpdir, 'tmp-file')) is False 46 | 47 | assert utils.touchdir(os.path.join(tmpdir, 'tmp-file')) is True 48 | 49 | # Date is updated 50 | assert utils.touchdir(os.path.join(tmpdir, 'tmp-file')) is True 51 | 52 | with mock.patch('os.utime', side_effect=OSError()): 53 | # Fails to update file 54 | assert utils.touchdir(os.path.join(tmpdir, 'tmp-file')) is False 55 | 56 | def test_touch(self): 57 | """ 58 | Test touch() 59 | """ 60 | 61 | with tempfile.TemporaryDirectory() as tmpdir: 62 | with mock.patch('os.fdopen', side_effect=OSError()): 63 | assert utils.touch(os.path.join(tmpdir, 'tmp-file')) is False 64 | -------------------------------------------------------------------------------- /apprise_api/api/tests/test_webhook.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Copyright (C) 2019 Chris Caron 4 | # All rights reserved. 5 | # 6 | # This code is licensed under the MIT License. 7 | # 8 | # Permission is hereby granted, free of charge, to any person obtaining a copy 9 | # of this software and associated documentation files(the "Software"), to deal 10 | # in the Software without restriction, including without limitation the rights 11 | # to use, copy, modify, merge, publish, distribute, sublicense, and / or sell 12 | # copies of the Software, and to permit persons to whom the Software is 13 | # furnished to do so, subject to the following conditions : 14 | # 15 | # The above copyright notice and this permission notice shall be included in 16 | # all copies or substantial portions of the Software. 17 | # 18 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE 21 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 24 | # THE SOFTWARE. 25 | from django.test import SimpleTestCase 26 | from unittest import mock 27 | from json import loads 28 | import requests 29 | from ..utils import send_webhook 30 | from django.test.utils import override_settings 31 | 32 | 33 | class WebhookTests(SimpleTestCase): 34 | 35 | @mock.patch('requests.post') 36 | def test_webhook_testing(self, mock_post): 37 | """ 38 | Test webhook handling 39 | """ 40 | 41 | # Response object 42 | response = mock.Mock() 43 | response.status_code = requests.codes.ok 44 | mock_post.return_value = response 45 | 46 | with override_settings( 47 | APPRISE_WEBHOOK_URL='https://' 48 | 'user:pass@localhost/webhook'): 49 | send_webhook({}) 50 | assert mock_post.call_count == 1 51 | 52 | details = mock_post.call_args_list[0] 53 | assert details[0][0] == 'https://localhost/webhook' 54 | assert loads(details[1]['data']) == {} 55 | assert 'User-Agent' in details[1]['headers'] 56 | assert 'Content-Type' in details[1]['headers'] 57 | assert details[1]['headers']['User-Agent'] == 'Apprise-API' 58 | assert details[1]['headers']['Content-Type'] == 'application/json' 59 | assert details[1]['auth'] == ('user', 'pass') 60 | assert details[1]['verify'] is True 61 | assert details[1]['params'] == {} 62 | assert details[1]['timeout'] == (4.0, 4.0) 63 | 64 | mock_post.reset_mock() 65 | 66 | with override_settings( 67 | APPRISE_WEBHOOK_URL='http://' 68 | 'user@localhost/webhook/here' 69 | '?verify=False&key=value&cto=2.0&rto=1.0'): 70 | send_webhook({}) 71 | assert mock_post.call_count == 1 72 | 73 | details = mock_post.call_args_list[0] 74 | assert details[0][0] == 'http://localhost/webhook/here' 75 | assert loads(details[1]['data']) == {} 76 | assert 'User-Agent' in details[1]['headers'] 77 | assert 'Content-Type' in details[1]['headers'] 78 | assert details[1]['headers']['User-Agent'] == 'Apprise-API' 79 | assert details[1]['headers']['Content-Type'] == 'application/json' 80 | assert details[1]['auth'] == ('user', None) 81 | assert details[1]['verify'] is False 82 | assert details[1]['params'] == {'key': 'value'} 83 | assert details[1]['timeout'] == (2.0, 1.0) 84 | 85 | mock_post.reset_mock() 86 | 87 | with override_settings(APPRISE_WEBHOOK_URL='invalid'): 88 | # Invalid webhook defined 89 | send_webhook({}) 90 | assert mock_post.call_count == 0 91 | 92 | mock_post.reset_mock() 93 | 94 | with override_settings(APPRISE_WEBHOOK_URL=None): 95 | # Invalid webhook defined 96 | send_webhook({}) 97 | assert mock_post.call_count == 0 98 | 99 | mock_post.reset_mock() 100 | 101 | with override_settings(APPRISE_WEBHOOK_URL='http://$#@'): 102 | # Invalid hostname defined 103 | send_webhook({}) 104 | assert mock_post.call_count == 0 105 | 106 | mock_post.reset_mock() 107 | 108 | with override_settings( 109 | APPRISE_WEBHOOK_URL='invalid://hostname'): 110 | # Invalid webhook defined 111 | send_webhook({}) 112 | assert mock_post.call_count == 0 113 | 114 | mock_post.reset_mock() 115 | 116 | # A valid URL with a bad server response: 117 | response.status_code = requests.codes.internal_server_error 118 | mock_post.return_value = response 119 | with override_settings( 120 | APPRISE_WEBHOOK_URL='http://localhost'): 121 | 122 | send_webhook({}) 123 | assert mock_post.call_count == 1 124 | 125 | mock_post.reset_mock() 126 | 127 | # A valid URL with a bad server response: 128 | mock_post.return_value = None 129 | mock_post.side_effect = requests.RequestException("error") 130 | with override_settings( 131 | APPRISE_WEBHOOK_URL='http://localhost'): 132 | 133 | send_webhook({}) 134 | assert mock_post.call_count == 1 135 | -------------------------------------------------------------------------------- /apprise_api/api/tests/test_welcome.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Copyright (C) 2019 Chris Caron 4 | # All rights reserved. 5 | # 6 | # This code is licensed under the MIT License. 7 | # 8 | # Permission is hereby granted, free of charge, to any person obtaining a copy 9 | # of this software and associated documentation files(the "Software"), to deal 10 | # in the Software without restriction, including without limitation the rights 11 | # to use, copy, modify, merge, publish, distribute, sublicense, and / or sell 12 | # copies of the Software, and to permit persons to whom the Software is 13 | # furnished to do so, subject to the following conditions : 14 | # 15 | # The above copyright notice and this permission notice shall be included in 16 | # all copies or substantial portions of the Software. 17 | # 18 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE 21 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 24 | # THE SOFTWARE. 25 | from django.test import SimpleTestCase 26 | 27 | 28 | class WelcomePageTests(SimpleTestCase): 29 | 30 | def test_welcome_page_status_code(self): 31 | response = self.client.get('/') 32 | assert response.status_code == 200 33 | -------------------------------------------------------------------------------- /apprise_api/api/urlfilter.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Copyright (C) 2023 Chris Caron 4 | # All rights reserved. 5 | # 6 | # This code is licensed under the MIT License. 7 | # 8 | # Permission is hereby granted, free of charge, to any person obtaining a copy 9 | # of this software and associated documentation files(the "Software"), to deal 10 | # in the Software without restriction, including without limitation the rights 11 | # to use, copy, modify, merge, publish, distribute, sublicense, and / or sell 12 | # copies of the Software, and to permit persons to whom the Software is 13 | # furnished to do so, subject to the following conditions : 14 | # 15 | # The above copyright notice and this permission notice shall be included in 16 | # all copies or substantial portions of the Software. 17 | # 18 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE 21 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 24 | # THE SOFTWARE. 25 | import re 26 | from apprise.utils.parse import parse_url 27 | 28 | 29 | class AppriseURLFilter: 30 | """ 31 | A URL filtering class that uses pre-parsed and pre-compiled allow/deny lists. 32 | 33 | Deny rules are always processed before allow rules. If a URL matches any deny rule, 34 | it is immediately rejected. If no deny rule matches, then the URL is allowed only 35 | if it matches an allow rule; otherwise, it is rejected. 36 | 37 | Each entry in the allow/deny lists can be provided as: 38 | - A full URL (with http:// or https://) 39 | - A URL without a scheme (e.g. "localhost/resources") 40 | - A plain hostname or IP 41 | 42 | Wildcards: 43 | - '*' will match any sequence of characters. 44 | - '?' will match a single alphanumeric/dash/underscore character. 45 | 46 | A trailing '*' is implied if not already present so that rules operate as a prefix match. 47 | """ 48 | 49 | def __init__(self, allow_list: str, deny_list: str): 50 | # Pre-compile our rules. 51 | # Each rule is stored as a tuple (compiled_regex, is_url_based) 52 | # where `is_url_based` indicates if the token included "http://" or "https://" 53 | self.allow_rules = self._parse_list(allow_list) 54 | self.deny_rules = self._parse_list(deny_list) 55 | 56 | def _parse_list(self, list_str: str): 57 | """ 58 | Split the list (tokens separated by whitespace or commas) and compile each token. 59 | Tokens are classified as follows: 60 | - URL‐based tokens: if they start with “http://” or “https://” (explicit) 61 | or if they contain a “/” (implicit; no scheme given). 62 | - Host‐based tokens: those that do not contain a “/”. 63 | Returns a list of tuples (compiled_regex, is_url_based). 64 | """ 65 | tokens = re.split(r'[\s,]+', list_str.strip().lower()) 66 | rules = [] 67 | for token in tokens: 68 | if not token: 69 | continue 70 | 71 | if token.startswith("http://") or token.startswith("https://"): 72 | # Explicit URL token. 73 | compiled = self._compile_url_token(token) 74 | is_url_based = True 75 | 76 | elif "/" in token: 77 | # Implicit URL token: prepend a scheme pattern. 78 | compiled = self._compile_implicit_token(token) 79 | is_url_based = True 80 | 81 | else: 82 | # Host-based token. 83 | compiled = self._compile_host_token(token) 84 | is_url_based = False 85 | 86 | rules.append((compiled, is_url_based)) 87 | return rules 88 | 89 | def _compile_url_token(self, token: str): 90 | """ 91 | Compiles a URL‐based token (explicit token that starts with a scheme) into a regex. 92 | An implied trailing wildcard is added to the path: 93 | - If no path is given (or just “/”) then “(/.*)?” is appended. 94 | - If a nonempty path is given that does not end with “/” or “*”, then “($|/.*)” is appended. 95 | - If the path ends with “/”, then the trailing slash is removed and “(/.*)?” is appended, 96 | so that “/resources” and “/resources/” are treated equivalently. 97 | Also, if no port is specified in the host part, the regex ensures that no port is present. 98 | """ 99 | # Determine the scheme. 100 | scheme_regex = "" 101 | if token.startswith("http://"): 102 | scheme_regex = r'http' 103 | # drop http:// 104 | token = token[7:] 105 | 106 | elif token.startswith("https://"): 107 | scheme_regex = r'https' 108 | # drop https:// 109 | token = token[8:] 110 | 111 | else: # https? 112 | # Used for implicit tokens; our _compile_implicit_token ensures this. 113 | scheme_regex = r'https?' 114 | # strip https?:// 115 | token = token[9:] 116 | 117 | # Split token into host (and optional port) and path. 118 | if "/" in token: 119 | netloc, path = token.split("/", 1) 120 | path = "/" + path 121 | else: 122 | netloc = token 123 | path = "" 124 | 125 | # Process netloc and port. 126 | if ":" in netloc: 127 | host, port = netloc.split(":", 1) 128 | port_specified = True 129 | 130 | else: 131 | host = netloc 132 | port_specified = False 133 | 134 | regex = "^" + scheme_regex + "://" 135 | regex += self._wildcard_to_regex(host, is_host=True) 136 | if port_specified: 137 | regex += ":" + re.escape(port) 138 | 139 | else: 140 | # Ensure no port is present. 141 | regex += r"(?!:)" 142 | 143 | # Process the path. 144 | if path in ("", "/"): 145 | regex += r"(/.*)?" 146 | 147 | else: 148 | if path.endswith("*"): 149 | # Remove the trailing "*" and append .* 150 | regex += self._wildcard_to_regex(path[:-1]) + "([^/]+/?)" 151 | 152 | elif path.endswith("/"): 153 | # Remove the trailing "/" and allow an optional slash with extra path. 154 | norm = self._wildcard_to_regex(path.rstrip("/")) 155 | regex += norm + r"(/.*)?" 156 | 157 | else: 158 | # For a nonempty path that does not end with "/" or "*", 159 | # match either an exact match or a prefix (with a following slash). 160 | norm = self._wildcard_to_regex(path) 161 | regex += norm + r"($|/.*)" 162 | 163 | regex += "$" 164 | return re.compile(regex, re.IGNORECASE) 165 | 166 | def _compile_implicit_token(self, token: str): 167 | """ 168 | For an implicit token (one that does not start with a scheme but contains a “/”), 169 | prepend “https?://” so that it matches both http and https, then compile it. 170 | """ 171 | new_token = "https?://" + token 172 | return self._compile_url_token(new_token) 173 | 174 | def _compile_host_token(self, token: str): 175 | """ 176 | Compiles a host‐based token (one with no “/”) into a regex. 177 | Note: When matching host‐based tokens, we require that the URL’s scheme is exactly “http”. 178 | """ 179 | regex = "^" + self._wildcard_to_regex(token) + "$" 180 | return re.compile(regex, re.IGNORECASE) 181 | 182 | def _wildcard_to_regex(self, pattern: str, is_host: bool = True) -> str: 183 | """ 184 | Converts a pattern containing wildcards into a regex. 185 | - '*' becomes '.*' if host or [^/]+/? if path 186 | - '?' becomes '[A-Za-z0-9_-]' 187 | - Other characters are escaped. 188 | Special handling: if the pattern starts with "https?://", that prefix is preserved 189 | (so it can match either http:// or https://). 190 | """ 191 | regex = "" 192 | for char in pattern: 193 | if char == '*': 194 | regex += r"[^/]+/?" if not is_host else r'.*' 195 | 196 | elif char == '?': 197 | regex += r'[^/]' if not is_host else r"[A-Za-z0-9_-]" 198 | 199 | else: 200 | regex += re.escape(char) 201 | 202 | return regex 203 | 204 | def is_allowed(self, url: str) -> bool: 205 | """ 206 | Checks a given URL against the deny list first, then the allow list. 207 | For URL-based rules (explicit or implicit), the full URL is tested. 208 | For host-based rules, the URL’s netloc (which includes the port) is tested. 209 | """ 210 | parsed = parse_url(url, strict_port=True, simple=True) 211 | if not parsed: 212 | return False 213 | 214 | # includes port if present 215 | netloc = '%s:%d' % (parsed['host'], parsed.get('port')) if parsed.get('port') else parsed['host'] 216 | 217 | # Check deny rules first. 218 | for pattern, is_url_based in self.deny_rules: 219 | if is_url_based: 220 | if pattern.match(url): 221 | return False 222 | 223 | elif pattern.match(netloc): 224 | return False 225 | 226 | # Then check allow rules. 227 | for pattern, is_url_based in self.allow_rules: 228 | if is_url_based: 229 | if pattern.match(url): 230 | return True 231 | elif pattern.match(netloc): 232 | return True 233 | 234 | return False 235 | -------------------------------------------------------------------------------- /apprise_api/api/urls.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Copyright (C) 2019 Chris Caron 4 | # All rights reserved. 5 | # 6 | # This code is licensed under the MIT License. 7 | # 8 | # Permission is hereby granted, free of charge, to any person obtaining a copy 9 | # of this software and associated documentation files(the "Software"), to deal 10 | # in the Software without restriction, including without limitation the rights 11 | # to use, copy, modify, merge, publish, distribute, sublicense, and / or sell 12 | # copies of the Software, and to permit persons to whom the Software is 13 | # furnished to do so, subject to the following conditions : 14 | # 15 | # The above copyright notice and this permission notice shall be included in 16 | # all copies or substantial portions of the Software. 17 | # 18 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE 21 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 24 | # THE SOFTWARE. 25 | from django.urls import re_path 26 | from . import views 27 | 28 | urlpatterns = [ 29 | re_path( 30 | r'^$', 31 | views.WelcomeView.as_view(), name='welcome'), 32 | re_path( 33 | r'^status/?$', 34 | views.HealthCheckView.as_view(), name='health'), 35 | re_path( 36 | r'^details/?$', 37 | views.DetailsView.as_view(), name='details'), 38 | re_path( 39 | r'^cfg/(?P[\w_-]{1,128})/?$', 40 | views.ConfigView.as_view(), name='config'), 41 | re_path( 42 | r'^cfg/?$', 43 | views.ConfigListView.as_view(), name='config_list'), 44 | re_path( 45 | r'^add/(?P[\w_-]{1,128})/?$', 46 | views.AddView.as_view(), name='add'), 47 | re_path( 48 | r'^del/(?P[\w_-]{1,128})/?$', 49 | views.DelView.as_view(), name='del'), 50 | re_path( 51 | r'^get/(?P[\w_-]{1,128})/?$', 52 | views.GetView.as_view(), name='get'), 53 | re_path( 54 | r'^notify/(?P[\w_-]{1,128})/?$', 55 | views.NotifyView.as_view(), name='notify'), 56 | re_path( 57 | r'^notify/?$', 58 | views.StatelessNotifyView.as_view(), name='s_notify'), 59 | re_path( 60 | r'^json/urls/(?P[\w_-]{1,128})/?$', 61 | views.JsonUrlView.as_view(), name='json_urls'), 62 | ] 63 | -------------------------------------------------------------------------------- /apprise_api/core/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/caronc/apprise-api/3fde6d00914b6a538f9003c5d940b909fdce42a7/apprise_api/core/__init__.py -------------------------------------------------------------------------------- /apprise_api/core/context_processors.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Copyright (C) 2020 Chris Caron 4 | # All rights reserved. 5 | # 6 | # This code is licensed under the MIT License. 7 | # 8 | # Permission is hereby granted, free of charge, to any person obtaining a copy 9 | # of this software and associated documentation files(the "Software"), to deal 10 | # in the Software without restriction, including without limitation the rights 11 | # to use, copy, modify, merge, publish, distribute, sublicense, and / or sell 12 | # copies of the Software, and to permit persons to whom the Software is 13 | # furnished to do so, subject to the following conditions : 14 | # 15 | # The above copyright notice and this permission notice shall be included in 16 | # all copies or substantial portions of the Software. 17 | # 18 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE 21 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 24 | # THE SOFTWARE. 25 | from django.conf import settings 26 | 27 | 28 | def base_url(request): 29 | """ 30 | Returns our defined BASE_URL object 31 | """ 32 | return { 33 | 'BASE_URL': settings.BASE_URL, 34 | 'CONFIG_DIR': settings.APPRISE_CONFIG_DIR, 35 | 'ATTACH_DIR': settings.APPRISE_ATTACH_DIR, 36 | } 37 | -------------------------------------------------------------------------------- /apprise_api/core/middleware/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/caronc/apprise-api/3fde6d00914b6a538f9003c5d940b909fdce42a7/apprise_api/core/middleware/__init__.py -------------------------------------------------------------------------------- /apprise_api/core/middleware/config.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Copyright (C) 2023 Chris Caron 4 | # All rights reserved. 5 | # 6 | # This code is licensed under the MIT License. 7 | # 8 | # Permission is hereby granted, free of charge, to any person obtaining a copy 9 | # of this software and associated documentation files(the "Software"), to deal 10 | # in the Software without restriction, including without limitation the rights 11 | # to use, copy, modify, merge, publish, distribute, sublicense, and / or sell 12 | # copies of the Software, and to permit persons to whom the Software is 13 | # furnished to do so, subject to the following conditions : 14 | # 15 | # The above copyright notice and this permission notice shall be included in 16 | # all copies or substantial portions of the Software. 17 | # 18 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE 21 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 24 | # THE SOFTWARE. 25 | # 26 | import re 27 | from django.conf import settings 28 | import datetime 29 | 30 | 31 | class DetectConfigMiddleware: 32 | """ 33 | Using the `key=` variable, allow one pre-configure the default 34 | configuration to use. 35 | 36 | """ 37 | 38 | _is_cfg_path = re.compile(r'/cfg/(?P[\w_-]{1,128})') 39 | 40 | def __init__(self, get_response): 41 | """ 42 | Prepare our initialization 43 | """ 44 | self.get_response = get_response 45 | 46 | def __call__(self, request): 47 | """ 48 | Define our middleware hook 49 | """ 50 | 51 | result = self._is_cfg_path.match(request.path) 52 | if not result: 53 | # Our current config 54 | config = \ 55 | request.COOKIES.get('key', settings.APPRISE_DEFAULT_CONFIG_ID) 56 | 57 | # Extract our key (fall back to our default if not set) 58 | config = request.GET.get("key", config).strip() 59 | 60 | else: 61 | config = result.group('key') 62 | 63 | if not config: 64 | # Fallback to default config 65 | config = settings.APPRISE_DEFAULT_CONFIG_ID 66 | 67 | # Set our theme to a cookie 68 | request.default_config_id = config 69 | 70 | # Get our response object 71 | response = self.get_response(request) 72 | 73 | # Set our cookie 74 | max_age = 365 * 24 * 60 * 60 # 1 year 75 | expires = datetime.datetime.utcnow() + \ 76 | datetime.timedelta(seconds=max_age) 77 | 78 | # Set our cookie 79 | response.set_cookie('key', config, expires=expires) 80 | 81 | # return our response 82 | return response 83 | -------------------------------------------------------------------------------- /apprise_api/core/middleware/theme.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Copyright (C) 2023 Chris Caron 4 | # All rights reserved. 5 | # 6 | # This code is licensed under the MIT License. 7 | # 8 | # Permission is hereby granted, free of charge, to any person obtaining a copy 9 | # of this software and associated documentation files(the "Software"), to deal 10 | # in the Software without restriction, including without limitation the rights 11 | # to use, copy, modify, merge, publish, distribute, sublicense, and / or sell 12 | # copies of the Software, and to permit persons to whom the Software is 13 | # furnished to do so, subject to the following conditions : 14 | # 15 | # The above copyright notice and this permission notice shall be included in 16 | # all copies or substantial portions of the Software. 17 | # 18 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE 21 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 24 | # THE SOFTWARE. 25 | # 26 | from django.conf import settings 27 | from core.themes import SiteTheme, SITE_THEMES 28 | import datetime 29 | 30 | 31 | class AutoThemeMiddleware: 32 | """ 33 | Using the `theme=` variable, allow one to fix the language to either 34 | 'dark' or 'light' 35 | 36 | """ 37 | 38 | def __init__(self, get_response): 39 | """ 40 | Prepare our initialization 41 | """ 42 | self.get_response = get_response 43 | 44 | def __call__(self, request): 45 | """ 46 | Define our middleware hook 47 | """ 48 | 49 | # Our current theme 50 | current_theme = \ 51 | request.COOKIES.get('t', request.COOKIES.get( 52 | 'theme', settings.APPRISE_DEFAULT_THEME)) 53 | 54 | # Extract our theme (fall back to our default if not set) 55 | theme = request.GET.get("theme", current_theme).strip().lower() 56 | theme = next((entry for entry in SITE_THEMES 57 | if entry.startswith(theme)), None) \ 58 | if theme else None 59 | 60 | if theme not in SITE_THEMES: 61 | # Fallback to default theme 62 | theme = SiteTheme.LIGHT 63 | 64 | # Set our theme to a cookie 65 | request.theme = theme 66 | 67 | # Set our next theme 68 | request.next_theme = SiteTheme.LIGHT \ 69 | if theme == SiteTheme.DARK \ 70 | else SiteTheme.DARK 71 | 72 | # Get our response object 73 | response = self.get_response(request) 74 | 75 | # Set our cookie 76 | max_age = 365 * 24 * 60 * 60 # 1 year 77 | expires = datetime.datetime.utcnow() + \ 78 | datetime.timedelta(seconds=max_age) 79 | 80 | # Set our cookie 81 | response.set_cookie('theme', theme, expires=expires) 82 | 83 | # return our response 84 | return response 85 | -------------------------------------------------------------------------------- /apprise_api/core/settings/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Copyright (C) 2023 Chris Caron 4 | # All rights reserved. 5 | # 6 | # This code is licensed under the MIT License. 7 | # 8 | # Permission is hereby granted, free of charge, to any person obtaining a copy 9 | # of this software and associated documentation files(the "Software"), to deal 10 | # in the Software without restriction, including without limitation the rights 11 | # to use, copy, modify, merge, publish, distribute, sublicense, and / or sell 12 | # copies of the Software, and to permit persons to whom the Software is 13 | # furnished to do so, subject to the following conditions : 14 | # 15 | # The above copyright notice and this permission notice shall be included in 16 | # all copies or substantial portions of the Software. 17 | # 18 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE 21 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 24 | # THE SOFTWARE. 25 | import os 26 | from core.themes import SiteTheme 27 | 28 | # Disable Timezones 29 | USE_TZ = False 30 | 31 | # Base Directory (relative to settings) 32 | BASE_DIR = os.path.dirname( 33 | os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) 34 | 35 | # SECURITY WARNING: keep the secret key used in production secret! 36 | SECRET_KEY = os.environ.get( 37 | 'SECRET_KEY', '+reua88v8rs4j!bcfdtinb-f0edxazf!$x_q1g7jtgckxd7gi=') 38 | 39 | # SECURITY WARNING: don't run with debug turned on in production! 40 | # If you want to run this app in DEBUG mode, run the following: 41 | # 42 | # ./manage.py runserver --settings=core.settings.debug 43 | # 44 | # Or alternatively run: 45 | # 46 | # export DJANGO_SETTINGS_MODULE=core.settings.debug 47 | # ./manage.py runserver 48 | # 49 | # Support 'yes', '1', 'true', 'enable', 'active', and + 50 | DEBUG = os.environ.get("DEBUG", 'No')[0].lower() in ( 51 | 'a', 'y', '1', 't', 'e', '+') 52 | 53 | # allow all hosts by default otherwise read from the 54 | # ALLOWED_HOSTS environment variable 55 | ALLOWED_HOSTS = os.environ.get('ALLOWED_HOSTS', '*').split(' ') 56 | 57 | # Application definition 58 | INSTALLED_APPS = [ 59 | 'django.contrib.contenttypes', 60 | 'django.contrib.staticfiles', 61 | 62 | # Apprise API 63 | 'api', 64 | 65 | # Prometheus 66 | 'django_prometheus', 67 | ] 68 | 69 | MIDDLEWARE = [ 70 | 'django_prometheus.middleware.PrometheusBeforeMiddleware', 71 | 'django.middleware.common.CommonMiddleware', 72 | 'core.middleware.theme.AutoThemeMiddleware', 73 | 'core.middleware.config.DetectConfigMiddleware', 74 | 'django_prometheus.middleware.PrometheusAfterMiddleware', 75 | ] 76 | 77 | ROOT_URLCONF = 'core.urls' 78 | 79 | TEMPLATES = [ 80 | { 81 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 82 | 'DIRS': [], 83 | 'APP_DIRS': True, 84 | 'OPTIONS': { 85 | 'context_processors': [ 86 | 'django.template.context_processors.request', 87 | 'core.context_processors.base_url', 88 | 'api.context_processors.default_config_id', 89 | 'api.context_processors.unique_config_id', 90 | 'api.context_processors.stateful_mode', 91 | 'api.context_processors.config_lock', 92 | 'api.context_processors.admin_enabled', 93 | 'api.context_processors.apprise_version', 94 | ], 95 | }, 96 | }, 97 | ] 98 | 99 | LOGGING = { 100 | 'version': 1, 101 | 'disable_existing_loggers': True, 102 | 'formatters': { 103 | 'standard': { 104 | 'format': '%(asctime)s [%(levelname)s] %(name)s: %(message)s' 105 | }, 106 | }, 107 | 'handlers': { 108 | 'console': { 109 | 'class': 'logging.StreamHandler', 110 | 'formatter': 'standard' 111 | }, 112 | }, 113 | 'loggers': { 114 | 'django': { 115 | 'handlers': ['console'], 116 | 'level': os.environ.get( 117 | 'LOG_LEVEL', 'debug' if DEBUG else 'info').upper(), 118 | }, 119 | 'apprise': { 120 | 'handlers': ['console'], 121 | 'level': os.environ.get( 122 | 'LOG_LEVEL', 'debug' if DEBUG else 'info').upper(), 123 | }, 124 | } 125 | } 126 | 127 | 128 | WSGI_APPLICATION = 'core.wsgi.application' 129 | 130 | # Define our base URL 131 | # The default value is to be a single slash 132 | BASE_URL = os.environ.get('BASE_URL', '') 133 | 134 | # Define our default configuration ID to use 135 | APPRISE_DEFAULT_CONFIG_ID = \ 136 | os.environ.get('APPRISE_DEFAULT_CONFIG_ID', 'apprise') 137 | 138 | # Define our Prometheus Namespace 139 | PROMETHEUS_METRIC_NAMESPACE = "apprise" 140 | 141 | # Static files relative path (CSS, JavaScript, Images) 142 | STATIC_URL = BASE_URL + '/s/' 143 | 144 | # Default theme can be either 'light' or 'dark' 145 | APPRISE_DEFAULT_THEME = \ 146 | os.environ.get('APPRISE_DEFAULT_THEME', SiteTheme.LIGHT) 147 | 148 | # Webhook that is posted to upon executed results 149 | # Set it to something like https://myserver.com/path/ 150 | # Requets are done as a POST 151 | APPRISE_WEBHOOK_URL = os.environ.get('APPRISE_WEBHOOK_URL', '') 152 | 153 | # The location to store Apprise configuration files 154 | APPRISE_CONFIG_DIR = os.environ.get( 155 | 'APPRISE_CONFIG_DIR', os.path.join(BASE_DIR, 'var', 'config')) 156 | 157 | # The location to store Apprise Persistent Storage files 158 | APPRISE_STORAGE_DIR = os.environ.get( 159 | 'APPRISE_STORAGE_DIR', os.path.join(APPRISE_CONFIG_DIR, 'store')) 160 | 161 | # Default number of days to prune persistent storage 162 | APPRISE_STORAGE_PRUNE_DAYS = \ 163 | int(os.environ.get('APPRISE_STORAGE_PRUNE_DAYS', 30)) 164 | 165 | # The default URL ID Length 166 | APPRISE_STORAGE_UID_LENGTH = \ 167 | int(os.environ.get('APPRISE_STORAGE_UID_LENGTH', 8)) 168 | 169 | # The default storage mode; options are: 170 | # - memory : Disables persistent storage (this is also automatically set 171 | # if the APPRISE_STORAGE_DIR is empty reguardless of what is 172 | # defined below. 173 | # - auto : Writes to storage after each notifications execution (default) 174 | # - flush : Writes to storage constantly (as much as possible). This 175 | # produces more i/o but can allow multiple calls to the same 176 | # notification to be in sync more 177 | APPRISE_STORAGE_MODE = os.environ.get('APPRISE_STORAGE_MODE', 'auto').lower() 178 | 179 | # The location to place file attachments 180 | APPRISE_ATTACH_DIR = os.environ.get( 181 | 'APPRISE_ATTACH_DIR', os.path.join(BASE_DIR, 'var', 'attach')) 182 | 183 | # The maximum file attachment size allowed by the API (defined in MB) 184 | APPRISE_ATTACH_SIZE = int(os.environ.get('APPRISE_ATTACH_SIZE', 200)) * 1048576 185 | 186 | # A provided list that identify all of the URLs/Hosts/IPs that Apprise can 187 | # retrieve remote attachments from. 188 | # 189 | # Processing Order: 190 | # - The DENY list is always processed before the ALLOW list. 191 | # * If a match is found, processing stops, and the URL attachment is ignored 192 | # - The ALLOW list is ONLY processed if there was no match found on the DENY list 193 | # - If there is no match found on the ALLOW List, then the Attachment URL is ignored 194 | # * Not matching anything on the ALLOW list is effectively treated as a DENY 195 | # 196 | # Lists are both processed from top down (stopping on first match) 197 | 198 | # Use the following rules when constructing your ALLOW/DENY entries: 199 | # - Entries are separated with either a comma (,) and/or a space 200 | # - Entries can start with http:// or https:// (enforcing URL security HTTPS as part of check) 201 | # - IPs or hostnames provided is the preferred approach if it doesn't matter if the entry is 202 | # https:// or http:// 203 | # - * wildcards are allowed. * means nothing or anything else that follows. 204 | # - ? placeholder wildcards are allowed (identifying the explicit placeholder 205 | # of an alpha/numeric/dash/underscore character) 206 | # 207 | # Notes of interest: 208 | # - If the list is empty, then attachments can not be retrieved via URL at all. 209 | # - If the URL to be attached is not found or matched against an entry in this list then 210 | # the URL based attachment is ignored and is not retrieved at all. 211 | # - Set the list to * (a single astrix) to match all URLs and accepting all provided 212 | # matches 213 | APPRISE_ATTACH_DENY_URLS = \ 214 | os.environ.get('APPRISE_ATTACH_REJECT_URL', '127.0.* localhost*').lower() 215 | 216 | # The Allow list which is processed after the Deny list above 217 | APPRISE_ATTACH_ALLOW_URLS = \ 218 | os.environ.get('APPRISE_ATTACH_ALLOW_URL', '*').lower() 219 | 220 | 221 | # The maximum size in bytes that a request body may be before raising an error 222 | # (defined in MB) 223 | DATA_UPLOAD_MAX_MEMORY_SIZE = abs(int(os.environ.get( 224 | 'APPRISE_UPLOAD_MAX_MEMORY_SIZE', 3))) * 1048576 225 | 226 | # When set Apprise API Locks itself down so that future (configuration) 227 | # changes can not be made or accessed. It disables access to: 228 | # - the configuration screen: /cfg/{token} 229 | # - this in turn makes it so the Apprise CLI tool can not use it's 230 | # --config= (-c) options against this server. 231 | # - All notifications (both persistent and non persistent) continue to work 232 | # as they did before. This includes both /notify/{token}/ and /notify/ 233 | # - Certain API calls no longer work such as: 234 | # - /del/{token}/ 235 | # - /add/{token}/ 236 | # - the /json/urls/{token} API location will continue to work but will always 237 | # enforce it's privacy mode. 238 | # 239 | # The idea here is that someone has set up the configuration they way they want 240 | # and do not want this information exposed any more then it needs to be. 241 | # it's a lock down mode if you will. 242 | APPRISE_CONFIG_LOCK = \ 243 | os.environ.get("APPRISE_CONFIG_LOCK", 'no')[0].lower() in ( 244 | 'a', 'y', '1', 't', 'e', '+') 245 | 246 | # Stateless posts to /notify/ will resort to this set of URLs if none 247 | # were otherwise posted with the URL request. 248 | APPRISE_STATELESS_URLS = os.environ.get('APPRISE_STATELESS_URLS', '') 249 | 250 | # Allow stateless URLS to generate and/or work with persistent storage 251 | # By default this is set to no 252 | APPRISE_STATELESS_STORAGE = \ 253 | os.environ.get("APPRISE_STATELESS_STORAGE", 'no')[0].lower() in ( 254 | 'a', 'y', '1', 't', 'e', '+') 255 | 256 | # Defines the stateful mode; possible values are: 257 | # - hash (default): content is hashed and zipped 258 | # - simple: content is just written straight to disk 'as-is' 259 | # - disabled: disable all stateful functionality 260 | APPRISE_STATEFUL_MODE = os.environ.get('APPRISE_STATEFUL_MODE', 'hash') 261 | 262 | # Our Apprise Deny List 263 | # - By default we disable all non-remote calling services 264 | # - You do not need to identify every schema supported by the service you 265 | # wish to disable (only one). For example, if you were to specify 266 | # xml, that would include the xmls entry as well (or vs versa) 267 | APPRISE_DENY_SERVICES = os.environ.get('APPRISE_DENY_SERVICES', ','.join(( 268 | 'windows', 'dbus', 'gnome', 'macosx', 'syslog'))) 269 | 270 | # Our Apprise Exclusive Allow List 271 | # - anything not identified here is denied/disabled) 272 | # - this list trumps the APPRISE_DENY_SERVICES identified above 273 | APPRISE_ALLOW_SERVICES = os.environ.get('APPRISE_ALLOW_SERVICES', '') 274 | 275 | # Define the number of recursive calls your system will allow users to make 276 | # The idea here is to prevent people from defining apprise:// URL's triggering 277 | # a call to the same server again, and again and again. By default we allow 278 | # 1 level of recursion 279 | APPRISE_RECURSION_MAX = int(os.environ.get('APPRISE_RECURSION_MAX', 1)) 280 | 281 | # Provided optional plugin paths to scan for custom schema definitions 282 | APPRISE_PLUGIN_PATHS = os.environ.get( 283 | 'APPRISE_PLUGIN_PATHS', os.path.join(BASE_DIR, 'var', 'plugin')).split(',') 284 | 285 | # Define the number of attachments that can exist as part of a payload 286 | # Setting this to zero disables the limit 287 | APPRISE_MAX_ATTACHMENTS = int(os.environ.get('APPRISE_MAX_ATTACHMENTS', 6)) 288 | 289 | # Allow Admin mode: 290 | # - showing a list of configuration keys (when STATEFUL_MODE is set to simple) 291 | APPRISE_ADMIN = \ 292 | os.environ.get("APPRISE_ADMIN", 'no')[0].lower() in ( 293 | 'a', 'y', '1', 't', 'e', '+') 294 | -------------------------------------------------------------------------------- /apprise_api/core/settings/debug/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Copyright (C) 2019 Chris Caron 4 | # All rights reserved. 5 | # 6 | # This code is licensed under the MIT License. 7 | # 8 | # Permission is hereby granted, free of charge, to any person obtaining a copy 9 | # of this software and associated documentation files(the "Software"), to deal 10 | # in the Software without restriction, including without limitation the rights 11 | # to use, copy, modify, merge, publish, distribute, sublicense, and / or sell 12 | # copies of the Software, and to permit persons to whom the Software is 13 | # furnished to do so, subject to the following conditions : 14 | # 15 | # The above copyright notice and this permission notice shall be included in 16 | # all copies or substantial portions of the Software. 17 | # 18 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE 21 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 24 | # THE SOFTWARE. 25 | 26 | # To create a valid debug settings.py we need to intentionally pollute our 27 | # file with all of the content found in the master configuration. 28 | import os 29 | from .. import * # noqa F403 30 | 31 | # Debug is always on when running in debug mode 32 | DEBUG = True 33 | 34 | # Allowed hosts is not required in debug mode 35 | ALLOWED_HOSTS = [] 36 | 37 | # Over-ride the default URLConf for debugging 38 | ROOT_URLCONF = 'core.settings.debug.urls' 39 | 40 | # Our static paths directory for serving 41 | STATICFILES_DIRS = ( 42 | os.path.join(BASE_DIR, 'static'), # noqa F405 43 | ) 44 | -------------------------------------------------------------------------------- /apprise_api/core/settings/debug/urls.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Copyright (C) 2019 Chris Caron 4 | # All rights reserved. 5 | # 6 | # This code is licensed under the MIT License. 7 | # 8 | # Permission is hereby granted, free of charge, to any person obtaining a copy 9 | # of this software and associated documentation files(the "Software"), to deal 10 | # in the Software without restriction, including without limitation the rights 11 | # to use, copy, modify, merge, publish, distribute, sublicense, and / or sell 12 | # copies of the Software, and to permit persons to whom the Software is 13 | # furnished to do so, subject to the following conditions : 14 | # 15 | # The above copyright notice and this permission notice shall be included in 16 | # all copies or substantial portions of the Software. 17 | # 18 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE 21 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 24 | # THE SOFTWARE. 25 | from django.conf import settings 26 | from django.conf.urls.static import static 27 | from ...urls import * # noqa F403 28 | 29 | # Extend our patterns 30 | urlpatterns += static(settings.STATIC_URL) # noqa F405 31 | -------------------------------------------------------------------------------- /apprise_api/core/settings/pytest/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Copyright (C) 2019 Chris Caron 4 | # All rights reserved. 5 | # 6 | # This code is licensed under the MIT License. 7 | # 8 | # Permission is hereby granted, free of charge, to any person obtaining a copy 9 | # of this software and associated documentation files(the "Software"), to deal 10 | # in the Software without restriction, including without limitation the rights 11 | # to use, copy, modify, merge, publish, distribute, sublicense, and / or sell 12 | # copies of the Software, and to permit persons to whom the Software is 13 | # furnished to do so, subject to the following conditions : 14 | # 15 | # The above copyright notice and this permission notice shall be included in 16 | # all copies or substantial portions of the Software. 17 | # 18 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE 21 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 24 | # THE SOFTWARE. 25 | 26 | # To create a valid debug settings.py we need to intentionally pollute our 27 | # file with all of the content found in the master configuration. 28 | from tempfile import TemporaryDirectory 29 | from .. import * # noqa F403 30 | 31 | # Debug is always on when running in debug mode 32 | DEBUG = True 33 | 34 | # Allowed hosts is not required in debug mode 35 | ALLOWED_HOSTS = [] 36 | 37 | # A temporary directory to work in for unit testing 38 | APPRISE_CONFIG_DIR = TemporaryDirectory().name 39 | 40 | # Setup our runner 41 | TEST_RUNNER = 'core.settings.pytest.runner.PytestTestRunner' 42 | -------------------------------------------------------------------------------- /apprise_api/core/settings/pytest/runner.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Copyright (C) 2019 Chris Caron 4 | # All rights reserved. 5 | # 6 | # This code is licensed under the MIT License. 7 | # 8 | # Permission is hereby granted, free of charge, to any person obtaining a copy 9 | # of this software and associated documentation files(the "Software"), to deal 10 | # in the Software without restriction, including without limitation the rights 11 | # to use, copy, modify, merge, publish, distribute, sublicense, and / or sell 12 | # copies of the Software, and to permit persons to whom the Software is 13 | # furnished to do so, subject to the following conditions : 14 | # 15 | # The above copyright notice and this permission notice shall be included in 16 | # all copies or substantial portions of the Software. 17 | # 18 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE 21 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 24 | # THE SOFTWARE. 25 | 26 | # To create a valid debug settings.py we need to intentionally pollute our 27 | # file with all of the content found in the master configuration. 28 | 29 | 30 | class PytestTestRunner(object): 31 | """Runs pytest to discover and run tests.""" 32 | 33 | def __init__(self, verbosity=1, failfast=False, keepdb=False, **kwargs): 34 | self.verbosity = verbosity 35 | self.failfast = failfast 36 | self.keepdb = keepdb 37 | 38 | def run_tests(self, test_labels): 39 | """Run pytest and return the exitcode. 40 | 41 | It translates some of Django's test command option to pytest's. 42 | """ 43 | import pytest 44 | 45 | argv = [] 46 | if self.verbosity == 0: 47 | argv.append('--quiet') 48 | if self.verbosity == 2: 49 | argv.append('--verbose') 50 | if self.verbosity == 3: 51 | argv.append('-vv') 52 | if self.failfast: 53 | argv.append('--exitfirst') 54 | if self.keepdb: 55 | argv.append('--reuse-db') 56 | 57 | argv.extend(test_labels) 58 | return pytest.main(argv) 59 | -------------------------------------------------------------------------------- /apprise_api/core/themes.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Copyright (C) 2019 Chris Caron 4 | # All rights reserved. 5 | # 6 | # This code is licensed under the MIT License. 7 | # 8 | # Permission is hereby granted, free of charge, to any person obtaining a copy 9 | # of this software and associated documentation files(the "Software"), to deal 10 | # in the Software without restriction, including without limitation the rights 11 | # to use, copy, modify, merge, publish, distribute, sublicense, and / or sell 12 | # copies of the Software, and to permit persons to whom the Software is 13 | # furnished to do so, subject to the following conditions : 14 | # 15 | # The above copyright notice and this permission notice shall be included in 16 | # all copies or substantial portions of the Software. 17 | # 18 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE 21 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 24 | # THE SOFTWARE. 25 | 26 | 27 | class SiteTheme: 28 | """ 29 | Defines our site themes 30 | """ 31 | LIGHT = 'light' 32 | DARK = 'dark' 33 | 34 | 35 | SITE_THEMES = ( 36 | SiteTheme.LIGHT, 37 | SiteTheme.DARK, 38 | ) 39 | -------------------------------------------------------------------------------- /apprise_api/core/urls.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Copyright (C) 2019 Chris Caron 4 | # All rights reserved. 5 | # 6 | # This code is licensed under the MIT License. 7 | # 8 | # Permission is hereby granted, free of charge, to any person obtaining a copy 9 | # of this software and associated documentation files(the "Software"), to deal 10 | # in the Software without restriction, including without limitation the rights 11 | # to use, copy, modify, merge, publish, distribute, sublicense, and / or sell 12 | # copies of the Software, and to permit persons to whom the Software is 13 | # furnished to do so, subject to the following conditions : 14 | # 15 | # The above copyright notice and this permission notice shall be included in 16 | # all copies or substantial portions of the Software. 17 | # 18 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE 21 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 24 | # THE SOFTWARE. 25 | from django.urls import path 26 | from django.conf.urls import include 27 | 28 | from api import urls as api_urls 29 | 30 | urlpatterns = [ 31 | path('', include(api_urls)), 32 | path('', include('django_prometheus.urls')), 33 | ] 34 | -------------------------------------------------------------------------------- /apprise_api/core/wsgi.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Copyright (C) 2019 Chris Caron 4 | # All rights reserved. 5 | # 6 | # This code is licensed under the MIT License. 7 | # 8 | # Permission is hereby granted, free of charge, to any person obtaining a copy 9 | # of this software and associated documentation files(the "Software"), to deal 10 | # in the Software without restriction, including without limitation the rights 11 | # to use, copy, modify, merge, publish, distribute, sublicense, and / or sell 12 | # copies of the Software, and to permit persons to whom the Software is 13 | # furnished to do so, subject to the following conditions : 14 | # 15 | # The above copyright notice and this permission notice shall be included in 16 | # all copies or substantial portions of the Software. 17 | # 18 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE 21 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 24 | # THE SOFTWARE. 25 | import os 26 | from django.core.wsgi import get_wsgi_application 27 | 28 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'core.settings') 29 | application = get_wsgi_application() 30 | -------------------------------------------------------------------------------- /apprise_api/etc/nginx.conf: -------------------------------------------------------------------------------- 1 | daemon off; 2 | worker_processes auto; 3 | pid /run/apprise/nginx.pid; 4 | include /etc/nginx/modules-enabled/*.conf; 5 | 6 | events { 7 | worker_connections 768; 8 | } 9 | 10 | http { 11 | ## 12 | # Basic Settings 13 | ## 14 | sendfile on; 15 | tcp_nopush on; 16 | types_hash_max_size 2048; 17 | include /etc/nginx/mime.types; 18 | default_type application/octet-stream; 19 | # Do not display Nginx Version 20 | server_tokens off; 21 | 22 | ## 23 | # Upload Restriction 24 | ## 25 | client_max_body_size 500M; 26 | 27 | ## 28 | # Logging Settings 29 | ## 30 | access_log /dev/stdout; 31 | error_log /dev/stdout info; 32 | 33 | ## 34 | # Gzip Settings 35 | ## 36 | gzip on; 37 | 38 | ## 39 | # Host Configuration 40 | ## 41 | client_body_buffer_size 256k; 42 | client_body_in_file_only off; 43 | 44 | server { 45 | listen 8000; # IPv4 Support 46 | listen [::]:8000; # IPv6 Support 47 | 48 | # Allow users to map to this file and provide their own custom 49 | # overrides such as 50 | include /etc/nginx/server-override.conf; 51 | 52 | # Main Website 53 | location / { 54 | include /etc/nginx/uwsgi_params; 55 | proxy_set_header Host $http_host; 56 | proxy_set_header X-Real-IP $remote_addr; 57 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 58 | proxy_pass http://localhost:8080; 59 | # Give ample time for notifications to fire 60 | proxy_read_timeout 120s; 61 | include /etc/nginx/location-override.conf; 62 | } 63 | 64 | # Static Content 65 | location /s/ { 66 | root /usr/share/nginx/html; 67 | index index.html; 68 | include /etc/nginx/location-override.conf; 69 | } 70 | 71 | # 404 error handling 72 | error_page 404 /404.html; 73 | 74 | # redirect server error pages to the static page /50x.html 75 | error_page 500 502 503 504 /50x.html; 76 | location = /50x.html { 77 | root /usr/share/nginx/html; 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /apprise_api/etc/supervisord.conf: -------------------------------------------------------------------------------- 1 | [supervisord] 2 | nodaemon=true 3 | pidfile=/run/apprise/supervisord.pid 4 | logfile=/dev/null 5 | logfile_maxbytes=0 6 | user=www-data 7 | group=www-data 8 | 9 | [program:nginx] 10 | command=/usr/sbin/nginx -c /opt/apprise/webapp/etc/nginx.conf -p /opt/apprise 11 | directory=/opt/apprise 12 | stdout_logfile=/dev/stdout 13 | stdout_logfile_maxbytes=0 14 | stderr_logfile=/dev/stderr 15 | stderr_logfile_maxbytes=0 16 | 17 | [program:gunicorn] 18 | command=gunicorn -c /opt/apprise/webapp/gunicorn.conf.py -b :8080 --worker-tmp-dir /dev/shm core.wsgi 19 | directory=/opt/apprise/webapp 20 | stdout_logfile=/dev/stdout 21 | stdout_logfile_maxbytes=0 22 | stderr_logfile=/dev/stderr 23 | stderr_logfile_maxbytes=0 24 | -------------------------------------------------------------------------------- /apprise_api/gunicorn.conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Copyright (C) 2023 Chris Caron 4 | # All rights reserved. 5 | # 6 | # This code is licensed under the MIT License. 7 | # 8 | # Permission is hereby granted, free of charge, to any person obtaining a copy 9 | # of this software and associated documentation files(the "Software"), to deal 10 | # in the Software without restriction, including without limitation the rights 11 | # to use, copy, modify, merge, publish, distribute, sublicense, and / or sell 12 | # copies of the Software, and to permit persons to whom the Software is 13 | # furnished to do so, subject to the following conditions : 14 | # 15 | # The above copyright notice and this permission notice shall be included in 16 | # all copies or substantial portions of the Software. 17 | # 18 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE 21 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 24 | # THE SOFTWARE. 25 | import os 26 | import multiprocessing 27 | 28 | # This file is launched with the call: 29 | # gunicorn --config core.wsgi:application 30 | 31 | raw_env = [ 32 | 'LANG={}'.format(os.environ.get('LANG', 'en_US.UTF-8')), 33 | 'DJANGO_SETTINGS_MODULE=core.settings', 34 | ] 35 | 36 | # This is the path as prepared in the docker compose 37 | pythonpath = '/opt/apprise/webapp' 38 | 39 | # bind to port 8000 40 | bind = [ 41 | '0.0.0.0:8000', # IPv4 Support 42 | '[::]:8000', # IPv6 Support 43 | ] 44 | 45 | # Workers are relative to the number of CPU's provided by hosting server 46 | workers = int(os.environ.get( 47 | 'APPRISE_WORKER_COUNT', multiprocessing.cpu_count() * 2 + 1)) 48 | 49 | # Increase worker timeout value to give upstream services time to 50 | # respond. 51 | timeout = int(os.environ.get('APPRISE_WORKER_TIMEOUT', 300)) 52 | 53 | # Our worker type to use; over-ride the default `sync` 54 | worker_class = 'gevent' 55 | 56 | # Get workers memory consumption under control by leveraging gunicorn 57 | # worker recycling timeout 58 | max_requests = 1000 59 | max_requests_jitter = 50 60 | 61 | # Logging 62 | # '-' means log to stdout. 63 | errorlog = '-' 64 | accesslog = '-' 65 | loglevel = 'warn' 66 | -------------------------------------------------------------------------------- /apprise_api/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # 4 | # Copyright (C) 2019 Chris Caron 5 | # All rights reserved. 6 | # 7 | # This code is licensed under the MIT License. 8 | # 9 | # Permission is hereby granted, free of charge, to any person obtaining a copy 10 | # of this software and associated documentation files(the "Software"), to deal 11 | # in the Software without restriction, including without limitation the rights 12 | # to use, copy, modify, merge, publish, distribute, sublicense, and / or sell 13 | # copies of the Software, and to permit persons to whom the Software is 14 | # furnished to do so, subject to the following conditions : 15 | # 16 | # The above copyright notice and this permission notice shall be included in 17 | # all copies or substantial portions of the Software. 18 | # 19 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE 22 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 25 | # THE SOFTWARE. 26 | import os 27 | import sys 28 | 29 | 30 | def main(): 31 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'core.settings.debug') 32 | try: 33 | from django.core.management import execute_from_command_line 34 | except ImportError as exc: 35 | raise ImportError( 36 | "Couldn't import Django. Are you sure it's installed and " 37 | "available on your PYTHONPATH environment variable? Did you " 38 | "forget to activate a virtual environment?" 39 | ) from exc 40 | execute_from_command_line(sys.argv) 41 | 42 | 43 | if __name__ == '__main__': 44 | main() 45 | -------------------------------------------------------------------------------- /apprise_api/static/css/base.css: -------------------------------------------------------------------------------- 1 | .nav h1 { 2 | margin: 0.4rem; 3 | font-size: 2.1rem; 4 | font-weight: bold; 5 | text-transform: uppercase; 6 | float: left; 7 | } 8 | 9 | /* Apprise Version */ 10 | .nav ul { 11 | float: right; 12 | font-style: normal; 13 | font-size: 0.7rem; 14 | } 15 | .theme { 16 | text-align: right; 17 | display: block; 18 | float:right; 19 | } 20 | 21 | input { 22 | display: block; 23 | } 24 | 25 | .tabs .tab.disabled a,.tabs .tab.disabled a:hover{font-weight: inherit} 26 | 27 | code { 28 | font-family: monospace; 29 | white-space: normal; 30 | padding: 0.2rem; 31 | } 32 | 33 | h1, h2, h3, h4, h5 { 34 | margin-top: 0; 35 | } 36 | 37 | td, th { 38 | vertical-align: top; 39 | padding-top: 0; 40 | } 41 | .api-details ol { 42 | margin-top: 0; 43 | margin-bottom: 0; 44 | } 45 | 46 | ul.detail-buttons strong { 47 | font-weight: 800; 48 | } 49 | 50 | h4 em { 51 | font-size: 2.0rem; 52 | display: inline-block; 53 | margin: 0; 54 | padding: 0; 55 | word-break: break-all; 56 | line-height: 1.0em; 57 | } 58 | 59 | em { 60 | color: #004d40; 61 | font-weight:bold; 62 | } 63 | 64 | .no-config .info { 65 | color: #004d40; 66 | font-size: 3.3rem; 67 | 68 | } 69 | 70 | textarea { 71 | height: 16rem; 72 | font-family: monospace; 73 | } 74 | 75 | .collapsible-body { 76 | padding: 1rem 2rem; 77 | } 78 | 79 | #overview strong { 80 | color: #004d40; 81 | display: inline-block; 82 | background-color: #eee; 83 | } 84 | 85 | .tabs .tab a{ 86 | border-radius: 25px 25px 0 0; 87 | color:#2bbbad; 88 | } 89 | .collection a.collection-item:not(.active):hover, 90 | .tabs .tab a:focus, .tabs .tab a:focus.active { 91 | background-color: #eee; 92 | } 93 | .tabs .tab a:hover,.tabs .tab a.active { 94 | background-color:transparent; 95 | color:#004d40; 96 | font-weight: bold; 97 | background-color: #eee; 98 | } 99 | .tabs .tab.disabled a,.tabs .tab.disabled a:hover { 100 | color:rgba(102,147,153,0.7); 101 | } 102 | .tabs .indicator { 103 | background-color:#004d40; 104 | } 105 | .tabs .tab-locked a { 106 | /* Handle locked out tabs */ 107 | color:rgba(212, 161, 157, 0.7); 108 | } 109 | .tabs .tab-locked a:hover,.tabs .tab-locked a.active { 110 | /* Handle locked out tabs */ 111 | color: #6b0900; 112 | } 113 | 114 | .material-icons{ 115 | display: inline-flex; 116 | vertical-align: middle; 117 | } 118 | 119 | #url-list .card-panel { 120 | padding: 0.5rem; 121 | margin: 0.1rem 0; 122 | border-radius: 12px; 123 | width: 50%; 124 | min-width: 35rem; 125 | min-height: 4em; 126 | float: left; 127 | position: relative; 128 | } 129 | 130 | .chip { 131 | margin: 0.3rem; 132 | background-color: inherit; 133 | border: 1px solid #464646; 134 | color: #464646; 135 | cursor: pointer; 136 | } 137 | 138 | 139 | #url-list code { 140 | overflow-x: hidden; 141 | overflow-y: hidden; 142 | white-space: wrap; 143 | text-wrap: wrap; 144 | overflow-wrap: break-word; 145 | border-radius: 5px; 146 | margin-top: 0.8rem; 147 | display: block; 148 | } 149 | 150 | #url-list .card-panel .url-id { 151 | width: auto; 152 | margin: 0.3rem; 153 | background-color: inherit; 154 | color: #aaa; 155 | padding: 0.2em; 156 | font-size: 0.7em; 157 | border: 0px; 158 | /* Top Justified */ 159 | position: absolute; 160 | top: -0.5em; 161 | right: 0.3em; 162 | } 163 | 164 | /* Notification Details */ 165 | ul.logs { 166 | font-family: monospace, monospace; 167 | height: 60%; 168 | overflow: auto; 169 | } 170 | 171 | ul.logs li { 172 | display: flex; 173 | text-align: left; 174 | width: 50em; 175 | } 176 | 177 | ul.logs li div.log_time { 178 | font-weight: normal; 179 | flex: 0 15em; 180 | } 181 | 182 | ul.logs li div.log_level { 183 | font-weight: bold; 184 | align: right; 185 | flex: 0 5em; 186 | } 187 | 188 | ul.logs li div.log_msg { 189 | flex: 1; 190 | } 191 | 192 | .url-enabled { 193 | color:#004d40; 194 | } 195 | .url-disabled { 196 | color: #8B0000; 197 | } 198 | h6 { 199 | font-weight: bold; 200 | } 201 | #overview pre { 202 | margin-left: 2.0rem 203 | } 204 | 205 | code.config-id { 206 | font-size: 0.7em; 207 | } 208 | 209 | /* file button styled */ 210 | .btn-file { 211 | position: relative; 212 | overflow: hidden; 213 | text-transform: uppercase; 214 | } 215 | .btn-file input[type=file] { 216 | position: absolute; 217 | top: 0; 218 | right: 0; 219 | min-width: 100%; 220 | min-height: 100%; 221 | font-size: 100px; 222 | text-align: right; 223 | filter: alpha(opacity=0); 224 | opacity: 0; 225 | outline: none; 226 | background: white; 227 | cursor: inherit; 228 | display: block; 229 | } 230 | 231 | .file-selected { 232 | line-height: 2.0em; 233 | font-size: 1.2rem; 234 | border-radius: 5px; 235 | padding: 0 1em; 236 | overflow: hidden; 237 | } 238 | 239 | .chip.selected { 240 | font-weight: 600; 241 | } 242 | 243 | #health-check { 244 | background-color: #f883791f; 245 | border-radius: 25px; 246 | padding: 2em; 247 | margin-bottom: 2em; 248 | } 249 | #health-check h4 { 250 | font-size: 30px; 251 | } 252 | #health-check h4 .material-icons { 253 | margin-top: -0.2em; 254 | } 255 | 256 | #health-check li .material-icons { 257 | font-size: 30px; 258 | margin-top: -0.2em; 259 | } 260 | 261 | #health-check ul { 262 | list-style-type: disc; 263 | padding-left: 2em; 264 | } 265 | 266 | #health-check ul strong { 267 | font-weight: 600; 268 | font-size: 1.2rem; 269 | display: block; 270 | } 271 | 272 | -------------------------------------------------------------------------------- /apprise_api/static/css/highlight.min.css: -------------------------------------------------------------------------------- 1 | /* GitHub highlight.js style (c) Ivan Sagalaev */ 2 | .nav.nav-color{background:#8fbcbb!important}.hljs{display:block;overflow-x:auto;padding:.5em;color:#333;background:#f8f8f8}.hljs-comment,.hljs-quote{color:#998;font-style:italic}.hljs-keyword,.hljs-selector-tag,.hljs-subst{color:#333;font-weight:700}.hljs-literal,.hljs-number,.hljs-tag .hljs-attr,.hljs-template-variable,.hljs-variable{color:teal}.hljs-doctag,.hljs-string{color:#d14}.hljs-section,.hljs-selector-id,.hljs-title{color:#900;font-weight:700}.hljs-subst{font-weight:400}.hljs-class .hljs-title,.hljs-type{color:#458;font-weight:700}.hljs-attribute,.hljs-name,.hljs-tag{color:navy;font-weight:400}.hljs-link,.hljs-regexp{color:#009926}.hljs-bullet,.hljs-symbol{color:#990073}.hljs-built_in,.hljs-builtin-name{color:#0086b3}.hljs-meta{color:#999;font-weight:700}.hljs-deletion{background:#fdd}.hljs-addition{background:#dfd}.hljs-emphasis{font-style:italic}.hljs-strong{font-weight:700}a i {color: #333} 3 | -------------------------------------------------------------------------------- /apprise_api/static/css/theme-light.min.css: -------------------------------------------------------------------------------- 1 | .tabs .tab a {background-color: #f3f3f3;} 2 | .tabs.tabs-transparent .tab a, 3 | .tabs.tabs-transparent .tab.disabled a, 4 | .tabs.tabs-transparent .tab.disabled a:hover, 5 | .tab.disabled i.material-icons{ 6 | color:#a7a7a7 7 | } 8 | .tabs .tab.disabled a, 9 | .tabs .tab.disabled a:hover { 10 | background-color: #f3f3f3; 11 | color:#a7a7a7 12 | } 13 | .file-selected { 14 | color: #039be5; 15 | background-color: #f3f3f3; 16 | } 17 | 18 | #url-list .card-panel.selected { 19 | background-color: #fff8dc; 20 | } 21 | 22 | .chip { 23 | background-color: #fff!important; 24 | } 25 | 26 | .chip.selected { 27 | color: #fff; 28 | background-color: #258528!important; 29 | } 30 | 31 | 32 | ul.logs li.log_INFO { 33 | color: black; 34 | } 35 | 36 | ul.logs li.log_DEBUG { 37 | color: #606060; 38 | } 39 | 40 | ul.logs li.log_WARNING { 41 | color: orange; 42 | } 43 | 44 | ul.logs li.log_ERROR { 45 | color: #8B0000; 46 | } 47 | -------------------------------------------------------------------------------- /apprise_api/static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/caronc/apprise-api/3fde6d00914b6a538f9003c5d940b909fdce42a7/apprise_api/static/favicon.ico -------------------------------------------------------------------------------- /apprise_api/static/iconfont/MaterialIcons-Regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/caronc/apprise-api/3fde6d00914b6a538f9003c5d940b909fdce42a7/apprise_api/static/iconfont/MaterialIcons-Regular.eot -------------------------------------------------------------------------------- /apprise_api/static/iconfont/MaterialIcons-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/caronc/apprise-api/3fde6d00914b6a538f9003c5d940b909fdce42a7/apprise_api/static/iconfont/MaterialIcons-Regular.ttf -------------------------------------------------------------------------------- /apprise_api/static/iconfont/MaterialIcons-Regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/caronc/apprise-api/3fde6d00914b6a538f9003c5d940b909fdce42a7/apprise_api/static/iconfont/MaterialIcons-Regular.woff -------------------------------------------------------------------------------- /apprise_api/static/iconfont/MaterialIcons-Regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/caronc/apprise-api/3fde6d00914b6a538f9003c5d940b909fdce42a7/apprise_api/static/iconfont/MaterialIcons-Regular.woff2 -------------------------------------------------------------------------------- /apprise_api/static/iconfont/README.md: -------------------------------------------------------------------------------- 1 | The recommended way to use the Material Icons font is by linking to the web font hosted on Google Fonts: 2 | 3 | ```html 4 | 6 | ``` 7 | 8 | Read more in our full usage guide: 9 | http://google.github.io/material-design-icons/#icon-font-for-the-web 10 | -------------------------------------------------------------------------------- /apprise_api/static/iconfont/material-icons.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: 'Material Icons'; 3 | font-style: normal; 4 | font-weight: 400; 5 | src: url(MaterialIcons-Regular.eot); /* For IE6-8 */ 6 | src: local('Material Icons'), 7 | local('MaterialIcons-Regular'), 8 | url(MaterialIcons-Regular.woff2) format('woff2'), 9 | url(MaterialIcons-Regular.woff) format('woff'), 10 | url(MaterialIcons-Regular.ttf) format('truetype'); 11 | } 12 | 13 | .material-icons { 14 | font-family: 'Material Icons'; 15 | font-weight: normal; 16 | font-style: normal; 17 | font-size: 24px; /* Preferred icon size */ 18 | display: inline-block; 19 | line-height: 1; 20 | text-transform: none; 21 | letter-spacing: normal; 22 | word-wrap: normal; 23 | white-space: nowrap; 24 | direction: ltr; 25 | 26 | /* Support for all WebKit browsers. */ 27 | -webkit-font-smoothing: antialiased; 28 | /* Support for Safari and Chrome. */ 29 | text-rendering: optimizeLegibility; 30 | 31 | /* Support for Firefox. */ 32 | -moz-osx-font-smoothing: grayscale; 33 | 34 | /* Support for IE. */ 35 | font-feature-settings: 'liga'; 36 | } 37 | -------------------------------------------------------------------------------- /apprise_api/static/licenses/highlight-9.17.1.LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2006, Ivan Sagalaev. 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | * Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | * Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | * Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /apprise_api/static/licenses/material-design-icons-3.0.1.LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. -------------------------------------------------------------------------------- /apprise_api/static/licenses/materialize-1.0.0.LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014-2018 Materialize 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /apprise_api/static/licenses/sweetalert2-11.17.2.LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Tristan Edwards & Limon Monte 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /apprise_api/static/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/caronc/apprise-api/3fde6d00914b6a538f9003c5d940b909fdce42a7/apprise_api/static/logo.png -------------------------------------------------------------------------------- /apprise_api/supervisord-startup: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Copyright (C) 2024 Chris Caron 3 | # All rights reserved. 4 | # 5 | # This code is licensed under the MIT License. 6 | # 7 | # Permission is hereby granted, free of charge, to any person obtaining a copy 8 | # of this software and associated documentation files(the "Software"), to deal 9 | # in the Software without restriction, including without limitation the rights 10 | # to use, copy, modify, merge, publish, distribute, sublicense, and / or sell 11 | # copies of the Software, and to permit persons to whom the Software is 12 | # furnished to do so, subject to the following conditions : 13 | # 14 | # The above copyright notice and this permission notice shall be included in 15 | # all copies or substantial portions of the Software. 16 | # 17 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE 20 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | # THE SOFTWARE. 24 | 25 | if [ $(id -u) -eq 0 ]; then 26 | # 27 | # Root User 28 | # 29 | echo "Apprise API Super User Startup" 30 | 31 | # Default values 32 | PUID=${PUID:=1000} 33 | PGID=${PGID:=1000} 34 | 35 | # lookup our identifier 36 | GROUP=$(getent group $PGID 2>/dev/null | cut -d: -f1) 37 | [ -z "$GROUP" ] && groupadd --force -g $PGID apprise &>/dev/null && \ 38 | GROUP=apprise 39 | 40 | USER=$(id -un $PUID 2>/dev/null) 41 | [ $? -ne 0 ] && useradd -M -N \ 42 | -o -u $PUID -G $GROUP -c "Apprise API User" -d /opt/apprise apprise && \ 43 | USER=apprise 44 | 45 | if [ -z "$USER" ]; then 46 | echo "The specified User ID (PUID) of $PUID is invalid; Aborting operation." 47 | exit 1 48 | 49 | elif [ -z "$GROUP" ]; then 50 | echo "The specified Group ID (PGID) of $PGID is invalid; Aborting operation." 51 | exit 1 52 | fi 53 | 54 | # Ensure our group has been correctly assigned 55 | usermod -a -G $GROUP $USER &>/dev/null 56 | chmod o+w /dev/stdout /dev/stderr 57 | 58 | else 59 | # 60 | # Non-Root User 61 | # 62 | echo "Apprise API Non-Super User Startup" 63 | USER=$(id -un 2>/dev/null) 64 | GROUP=$(id -gn 2>/dev/null) 65 | fi 66 | 67 | [ ! -d /attach ] && mkdir -p /attach 68 | chown -R $USER:$GROUP /attach 69 | [ ! -d /config/store ] && mkdir -p /config/store 70 | chown $USER:$GROUP /config 71 | chown -R $USER:$GROUP /config/store 72 | [ ! -d /plugin ] && mkdir -p /plugin 73 | [ ! -d /run/apprise ] && mkdir -p /run/apprise 74 | 75 | # Some Directories require enforced permissions to play it safe 76 | chown $USER:$GROUP -R /run/apprise /var/lib/nginx /opt/apprise 77 | sed -i -e "s/^\(user[ \t]*=[ \t]*\).*$/\1$USER/g" \ 78 | /opt/apprise/webapp/etc/supervisord.conf 79 | sed -i -e "s/^\(group[ \t]*=[ \t]*\).*$/\1$GROUP/g" \ 80 | /opt/apprise/webapp/etc/supervisord.conf 81 | 82 | if [ "${IPV4_ONLY+x}" ] && [ "${IPV6_ONLY+x}" ]; then 83 | echo -n "Warning: both IPV4_ONLY and IPV6_ONLY flags set; ambigious; no changes made." 84 | 85 | # Handle IPV4_ONLY flag 86 | elif [ "${IPV4_ONLY+x}" ]; then 87 | echo -n "Enforcing Exclusive IPv4 environment... " 88 | sed -ibak -e '/IPv6 Support/d' /opt/apprise/webapp/etc/nginx.conf /opt/apprise/webapp/gunicorn.conf.py && \ 89 | echo "Done." || echo "Failed!" 90 | 91 | # Handle IPV6_ONLY flag 92 | elif [ "${IPV6_ONLY+x}" ]; then 93 | echo -n "Enforcing Exclusive IPv6 environment... " 94 | sed -ibak -e '/IPv4 Support/d' /opt/apprise/webapp/etc/nginx.conf /opt/apprise/webapp/gunicorn.conf.py && \ 95 | echo "Done." || echo "Failed!" 96 | fi 97 | 98 | # Working directory 99 | cd /opt/apprise 100 | 101 | # Launch our SupervisorD 102 | /usr/local/bin/supervisord -c /opt/apprise/webapp/etc/supervisord.conf 103 | 104 | # Always return our SupervisorD return code 105 | exit $? 106 | -------------------------------------------------------------------------------- /apprise_api/var/plugin/README.md: -------------------------------------------------------------------------------- 1 | # Custom Plugin Directory 2 | 3 | Apprise supports the ability to define your own `schema://` definitions and load them. To read more about how you can create your own customizations, check out [this link here](https://github.com/caronc/apprise/wiki/decorator_notify). 4 | -------------------------------------------------------------------------------- /dev-requirements.txt: -------------------------------------------------------------------------------- 1 | flake8 2 | mock 3 | pytest-django 4 | pytest 5 | pytest-cov 6 | tox 7 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | services: 4 | apprise: 5 | build: . 6 | container_name: apprise 7 | environment: 8 | - APPRISE_STATEFUL_MODE=simple 9 | ports: 10 | - 8000:8000 11 | volumes: 12 | - ./apprise_api:/opt/apprise/webapp:ro 13 | # if uncommenting the below, you will need to type the following 14 | # Note: if you opt for bind mount config file consider setting env var APPRISE_STATEFUL_MODE to simple with appropriate file format 15 | # otherwise the django instance won't have permissions to write 16 | # to the directory correctly: 17 | # $> chown -R 33:33 ./config 18 | # $> chmod -R 775 ./config 19 | # - ./config:/config:rw 20 | # Note: The attachment directory can be exposed outside of the container if required 21 | # $> chown -R 33:33 ./attach 22 | # $> chmod -R 775 ./attach 23 | # - ./attach:/attach:rw 24 | 25 | ## Un-comment the below and then access a testing environment with: 26 | ## docker-compose run test.py310 build 27 | ## docker-compose run --service-ports --rm test.py310 bash 28 | ## 29 | ## From here you 30 | ## > Check for any lint errors 31 | ## flake8 apprise_api 32 | ## 33 | ## > Run unit tests 34 | ## pytest apprise_api 35 | ## 36 | ## > Host service (visit http://localhost on host pc to access): 37 | ## ./manage.py runserver 0.0.0.0:8000 38 | # test.py312: 39 | # ports: 40 | # - 8000:8000 41 | # build: 42 | # context: . 43 | # dockerfile: Dockerfile.py312 44 | # volumes: 45 | # - ./:/apprise-api 46 | -------------------------------------------------------------------------------- /manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # 4 | # Copyright (C) 2019 Chris Caron 5 | # All rights reserved. 6 | # 7 | # This code is licensed under the MIT License. 8 | # 9 | # Permission is hereby granted, free of charge, to any person obtaining a copy 10 | # of this software and associated documentation files(the "Software"), to deal 11 | # in the Software without restriction, including without limitation the rights 12 | # to use, copy, modify, merge, publish, distribute, sublicense, and / or sell 13 | # copies of the Software, and to permit persons to whom the Software is 14 | # furnished to do so, subject to the following conditions : 15 | # 16 | # The above copyright notice and this permission notice shall be included in 17 | # all copies or substantial portions of the Software. 18 | # 19 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE 22 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 25 | # THE SOFTWARE. 26 | import os 27 | import sys 28 | 29 | # Update our path so it will see our apprise_api content 30 | sys.path.insert( 31 | 0, os.path.join(os.path.dirname(os.path.abspath(__file__)), 'apprise_api')) 32 | 33 | 34 | def main(): 35 | # Unless otherwise specified, default to a debug mode 36 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'core.settings.debug') 37 | try: 38 | from django.core.management import execute_from_command_line 39 | except ImportError as exc: 40 | raise ImportError( 41 | "Couldn't import Django. Are you sure it's installed and " 42 | "available on your PYTHONPATH environment variable? Did you " 43 | "forget to activate a virtual environment?" 44 | ) from exc 45 | execute_from_command_line(sys.argv) 46 | 47 | 48 | if __name__ == '__main__': 49 | main() 50 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | ## 2 | ## Apprise Backend Installation 3 | ## 4 | ## You should only have 1 of the 3 items uncommented below. 5 | 6 | ## 1. Uncomment the below line to pull the main branch of Apprise: 7 | # apprise @ git+https://github.com/caronc/apprise 8 | 9 | ## 2. Uncomment the below line instead if you wish to focus on a tag: 10 | # apprise @ git+https://github.com/caronc/apprise@custom-tag-or-version 11 | 12 | ## 3. The below grabs our stable version (generally the best choice): 13 | apprise == 1.9.3 14 | 15 | ## Apprise API Minimum Requirements 16 | django 17 | gevent 18 | gunicorn 19 | 20 | ## for webhook support 21 | requests 22 | 23 | ## 3rd Party Service support 24 | paho-mqtt < 2.0.0 25 | gntp 26 | cryptography 27 | 28 | # prometheus metrics 29 | django-prometheus 30 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | # ensure LICENSE is included in wheel metadata 3 | license_file = LICENSE 4 | 5 | [flake8] 6 | # We exclude packages we don't maintain 7 | exclude = .eggs,.tox 8 | ignore = E741,E722,W503,W504,W605 9 | statistics = true 10 | builtins = _ 11 | max-line-length = 160 12 | 13 | [aliases] 14 | test=pytest 15 | 16 | [tool:pytest] 17 | DJANGO_SETTINGS_MODULE = core.settings.pytest 18 | addopts = --ignore=lib --ignore=lib64 --nomigrations --cov=apprise_api --cov-report=term-missing 19 | filterwarnings = 20 | once::Warning 21 | -------------------------------------------------------------------------------- /swagger.yaml: -------------------------------------------------------------------------------- 1 | openapi: '3.0.3' 2 | info: 3 | title: Apprise API 4 | description: https://github.com/caronc/apprise-api 5 | version: 0.7.0 6 | paths: 7 | /notify: 8 | post: 9 | operationId: Stateless_SendNotification 10 | summary: Sends one or more notifications to the URLs identified as part of the payload, or those identified in the environment variable APPRISE_STATELESS_URLS. 11 | requestBody: 12 | required: true 13 | content: 14 | application/json: 15 | schema: 16 | $ref: '#/components/schemas/StatelessNotificationRequest' 17 | responses: 18 | 200: 19 | description: OK 20 | tags: 21 | - Stateless 22 | /add/{key}: 23 | post: 24 | operationId: Persistent_AddConfiguration 25 | summary: Saves Apprise Configuration (or set of URLs) to the persistent store. 26 | parameters: 27 | - in: path 28 | name: key 29 | required: true 30 | schema: 31 | type: string 32 | description: Configuration key 33 | requestBody: 34 | content: 35 | application/json: 36 | schema: 37 | $ref: '#/components/schemas/AddConfigurationRequest' 38 | responses: 39 | 200: 40 | description: OK 41 | tags: 42 | - Persistent 43 | /del/{key}: 44 | post: 45 | operationId: Persistent_RemoveConfiguration 46 | summary: Removes Apprise Configuration from the persistent store. 47 | parameters: 48 | - $ref: '#/components/parameters/key' 49 | responses: 50 | 200: 51 | description: OK 52 | tags: 53 | - Persistent 54 | /get/{key}: 55 | post: 56 | operationId: Persistent_GetConfiguration 57 | summary: Returns the Apprise Configuration from the persistent store. 58 | parameters: 59 | - $ref: '#/components/parameters/key' 60 | responses: 61 | 200: 62 | description: OK 63 | content: 64 | text/plain: 65 | schema: 66 | type: string 67 | tags: 68 | - Persistent 69 | /notify/{key}: 70 | post: 71 | operationId: Persistent_SendNotification 72 | summary: Sends notification(s) to all of the end points you've previously configured associated with a {KEY}. 73 | parameters: 74 | - $ref: '#/components/parameters/key' 75 | requestBody: 76 | content: 77 | application/json: 78 | schema: 79 | $ref: '#/components/schemas/PersistentNotificationRequest' 80 | responses: 81 | 200: 82 | description: OK 83 | tags: 84 | - Persistent 85 | /json/urls/{key}: 86 | get: 87 | operationId: Persistent_GetUrls 88 | summary: Returns a JSON response object that contains all of the URLS and Tags associated with the key specified. 89 | parameters: 90 | - $ref: '#/components/parameters/key' 91 | - in: query 92 | name: privacy 93 | schema: 94 | type: integer 95 | enum: [0, 1] 96 | # This should be changed to use 'oneOf' when upgrading to OpenApi 3.1 97 | x-enumNames: ["ShowSecrets", "HideSecrets"] 98 | required: false 99 | - in: query 100 | name: tag 101 | schema: 102 | type: string 103 | default: all 104 | required: false 105 | responses: 106 | 200: 107 | description: OK 108 | content: 109 | application/json: 110 | schema: 111 | $ref: '#/components/schemas/JsonUrlsResponse' 112 | tags: 113 | - Persistent 114 | 115 | components: 116 | parameters: 117 | key: 118 | in: path 119 | name: key 120 | required: true 121 | schema: 122 | type: string 123 | minLength: 1 124 | maxLength: 64 125 | description: Configuration key 126 | schemas: 127 | NotificationType: 128 | type: string 129 | enum: [info, warning, failure] 130 | default: info 131 | NotificationFormat: 132 | type: string 133 | enum: [text, markdown, html] 134 | default: text 135 | StatelessNotificationRequest: 136 | properties: 137 | urls: 138 | type: array 139 | items: 140 | type: string 141 | body: 142 | type: string 143 | title: 144 | type: string 145 | type: 146 | $ref: '#/components/schemas/NotificationType' 147 | format: 148 | $ref: '#/components/schemas/NotificationFormat' 149 | tag: 150 | type: string 151 | required: 152 | - body 153 | AddConfigurationRequest: 154 | properties: 155 | urls: 156 | type: array 157 | items: 158 | type: string 159 | default: null 160 | config: 161 | type: string 162 | format: 163 | type: string 164 | enum: [text, yaml] 165 | PersistentNotificationRequest: 166 | properties: 167 | body: 168 | type: string 169 | title: 170 | type: string 171 | type: 172 | $ref: '#/components/schemas/NotificationType' 173 | format: 174 | $ref: '#/components/schemas/NotificationFormat' 175 | tag: 176 | type: string 177 | default: all 178 | required: 179 | - body 180 | JsonUrlsResponse: 181 | properties: 182 | tags: 183 | type: array 184 | items: 185 | type: string 186 | urls: 187 | type: array 188 | items: 189 | type: object 190 | $ref: '#/components/schemas/url' 191 | url: 192 | properties: 193 | url: 194 | type: string 195 | tags: 196 | type: array 197 | items: 198 | type: string 199 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = py312,coverage-report 3 | skipsdist = true 4 | 5 | [testenv] 6 | # Prevent random setuptools/pip breakages like 7 | # https://github.com/pypa/setuptools/issues/1042 from breaking our builds. 8 | setenv = 9 | VIRTUALENV_NO_DOWNLOAD=1 10 | deps= 11 | -r{toxinidir}/requirements.txt 12 | -r{toxinidir}/dev-requirements.txt 13 | commands = 14 | coverage run --parallel -m pytest {posargs} apprise_api 15 | flake8 apprise_api --count --show-source --statistics 16 | 17 | [testenv:py312] 18 | deps= 19 | -r{toxinidir}/requirements.txt 20 | -r{toxinidir}/dev-requirements.txt 21 | commands = 22 | coverage run --parallel -m pytest {posargs} apprise_api 23 | flake8 apprise_api --count --show-source --statistics 24 | 25 | [testenv:coverage-report] 26 | deps = coverage 27 | skip_install = true 28 | commands= 29 | coverage combine apprise_api 30 | coverage report apprise_api 31 | --------------------------------------------------------------------------------