From 4418d8caf080ca6336a482237dfc21ea614f7895 Mon Sep 17 00:00:00 2001 From: Chris Caron Date: Mon, 15 May 2023 18:27:42 -0400 Subject: [PATCH] Custom plugin directory scanning support added (#111) --- README.md | 34 +++++++++++++------ apprise_api/api/tests/test_notify.py | 8 ++++- apprise_api/api/tests/test_stateful_notify.py | 6 ++++ .../api/tests/test_stateless_notify.py | 17 +++++++++- apprise_api/api/views.py | 28 ++++++++------- apprise_api/var/plugin/README.md | 3 ++ 6 files changed, 70 insertions(+), 26 deletions(-) create mode 100644 apprise_api/var/plugin/README.md diff --git a/README.md b/README.md index 68d5f00..3ef89d1 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,7 @@ Apprise API was designed to easily fit into existing (and new) eco-systems that [![Docker Pulls](https://img.shields.io/docker/pulls/caronc/apprise.svg?style=flat-square)](https://hub.docker.com/r/caronc/apprise) ## Screenshots + There is a small built-in *Configuration Manager* that can be optionally accessed through your web browser allowing you to create and save as many configurations as you'd like. Each configuration is differentiated by a unique `{KEY}` that you decide on:
![Screenshot of GUI - Using Keys](https://raw.githubusercontent.com/caronc/apprise-api/master/Screenshot-1.png)
@@ -28,9 +29,11 @@ Once you've saved your configuration, you'll be able to use the *Notification* t At the end of the day, the GUI just simply offers a user friendly interface to the same API developers can directly interface with if they wish to. ## Installation + The following options should allow you to access the API at: `http://localhost:8000/` from your browser. Using [dockerhub](https://hub.docker.com/r/caronc/apprise) you can do the following: + ```bash # Retrieve container docker pull caronc/apprise:latest @@ -38,9 +41,14 @@ docker pull caronc/apprise:latest # Start it up: # /config is used for a persistent store, you do not have to mount # this if you don't intend to use it. +# /plugin is used for a location you can add your own custom apprise plugins. +# You do not have to mount this if you don't intend to use it. +# /attach is used for file attachments docker run --name apprise \ -p 8000:8000 \ -v /var/lib/apprise/config:/config \ + -v /var/lib/apprise/plugin:/plugin \ + -v /var/lib/apprise/attach:/attach \ -d caronc/apprise:latest ``` @@ -51,8 +59,8 @@ A `docker-compose.yml` file is already set up to grant you an instant production docker-compose up ``` - ### Config Directory Permissions + Under the hood, An NginX services is reading/writing your configuration files as the user (and group) `www-data` which generally has the id of `33`. In preparation so that you don't get the error: `An error occured saving configuration.` consider also setting up your local `/var/lib/apprise/config` permissions as: ```bash @@ -78,6 +86,7 @@ sudo su - $(whoami) ``` Alternatively a dirty solution is to just set the directory with full read/write permissions (which is not ideal in a production environment): + ```bash # Grant full permission to the local directory you're saving your # Apprise configuration to: @@ -87,12 +96,12 @@ chmod 777 /var/lib/apprise/config ## Dockerfile Details The following architectures are supported: `386`, `amd64`, `arm/v6`, `arm/v7`, and `arm64`. The following tags can be used: -* `latest`: Points to the latest stable build. -* `edge`: Points to the last push to the master branch. +- `latest`: Points to the latest stable build. +- `edge`: Points to the last push to the master branch. ## Apprise URLs -📣 In order to trigger a notification, you first need to define one or more [Apprise URLs](https://github.com/caronc/apprise/wiki) to support the services you wish to leverage. Apprise supports over 80+ notification services today and is always expanding to add support for more! Visit https://github.com/caronc/apprise/wiki to see the ever-growing list of the services supported today. +📣 In order to trigger a notification, you first need to define one or more [Apprise URLs](https://github.com/caronc/apprise/wiki) to support the services you wish to leverage. Apprise supports over 80+ notification services today and is always expanding to add support for more! Visit to see the ever-growing list of the services supported today. ## API Details @@ -142,7 +151,7 @@ You can pre-save all of your Apprise configuration and/or set of Apprise URLs an | Path | Method | Description | |------------- | ------ | ----------- | -| `/add/{KEY}` | POST | Saves Apprise Configuration (or set of URLs) to the persistent store.
*Payload Parameters*
📌 **urls**: Define one or more Apprise URL(s) here. Use a comma and/or space to separate one URL from the next.
📌 **config**: Provide the contents of either a YAML or TEXT based Apprise configuration.
📌 **format**: This field is only required if you've specified the _config_ parameter. Used to tell the server which of the supported (Apprise) configuration types you are passing. Valid options are _text_ and _yaml_. This path does not work if `APPRISE_CONFIG_LOCK` is set. +| `/add/{KEY}` | POST | Saves Apprise Configuration (or set of URLs) to the persistent store.
*Payload Parameters*
📌 **urls**: Define one or more Apprise URL(s) here. Use a comma and/or space to separate one URL from the next.
📌 **config**: Provide the contents of either a YAML or TEXT based Apprise configuration.
📌 **format**: This field is only required if you've specified the *config* parameter. Used to tell the server which of the supported (Apprise) configuration types you are passing. Valid options are *text* and *yaml*. This path does not work if `APPRISE_CONFIG_LOCK` is set. | `/del/{KEY}` | POST | Removes Apprise Configuration from the persistent store. This path does not work if `APPRISE_CONFIG_LOCK` is set. | `/get/{KEY}` | POST | Returns the Apprise Configuration from the persistent store. This can be directly used with the *Apprise CLI* and/or the *AppriseConfig()* object ([see here for details](https://github.com/caronc/apprise/wiki/config)). This path does not work if `APPRISE_CONFIG_LOCK` is set. | `/notify/{KEY}` | POST | Sends notification(s) to all of the end points you've previously configured associated with a *{KEY}*.
*Payload Parameters*
📌 **body**: Your message body. This is the *only* required field.
📌 **title**: Optionally define a title to go along with the *body*.
📌 **type**: Defines the message type you want to send as. The valid options are `info`, `success`, `warning`, and `failure`. If no *type* is specified then `info` is the default value used.
📌 **tag**: Optionally notify only those tagged accordingly. Use a comma (`,`) to `OR` your tags and a space (` `) to `AND` them. More details on this can be seen documented below.
📌 **format**: Optionally identify the text format of the data you're feeding Apprise. The valid options are `text`, `markdown`, `html`. The default value if nothing is specified is `text`. @@ -196,7 +205,7 @@ curl -X POST \ http://localhost:8000/notify/abc123 ``` -🏷️ You can also leverage _tagging_ which allows you to associate one or more tags with your Apprise URLs. By doing this, notifications only need to be referred to by their easy to remember notify tag name such as `devops`, `admin`, `family`, etc. You can very easily group more than one notification service under the same _tag_ allowing you to notify a group of services at once. This is accomplished through configuration files ([documented here](https://github.com/caronc/apprise/wiki/config)) that can be saved to the persistent storage previously associated with a `{KEY}`. +🏷️ You can also leverage *tagging* which allows you to associate one or more tags with your Apprise URLs. By doing this, notifications only need to be referred to by their easy to remember notify tag name such as `devops`, `admin`, `family`, etc. You can very easily group more than one notification service under the same *tag* allowing you to notify a group of services at once. This is accomplished through configuration files ([documented here](https://github.com/caronc/apprise/wiki/config)) that can be saved to the persistent storage previously associated with a `{KEY}`. ```bash # Send notification(s) to a {KEY} defined as 'abc123' @@ -214,10 +223,10 @@ curl -X POST -d '{"tag":"devops", "body":"test message"}' \ Leveraging tagging is one of the things that makes Apprise great. Not only can you group one or more notifications together (all sharing the same tag), but you can assign multiple tags to the same URL and trigger it through crafted and selected tag expressions. -| Example | Effect| -| -------------------------------- | ------------------------------ | -| TagA | TagA -| TagA, TagB | TagA **OR** TagB +| Example | Effect | +| --------------------- | ------------------------------ | +| TagA | TagA +| TagA, TagB | TagA **OR** TagB | TagA TagC, TagB | (TagA **AND** TagC) **OR** TagB | TagB TagC | TagB **AND** TagC @@ -274,6 +283,7 @@ The use of environment variables allow you to provide over-rides to default sett | `APPRISE_ALLOW_SERVICES` | A comma separated set of entries identifying what plugins to allow access to. You may only use alpha-numeric characters as is the restriction of Apprise Schemas (schema://) anyway. To exclusively include more the one upstream service, simply specify additional entries separated by a `,` (comma) or ` ` (space). The `APPRISE_DENY_SERVICES` entries are ignored if the `APPRISE_ALLOW_SERVICES` is identified. | `SECRET_KEY` | A Django variable acting as a *salt* for most things that require security. This API uses it for the hash sequences when writing the configuration files to disk (`hash` mode only). | `ALLOWED_HOSTS` | A list of strings representing the host/domain names that this API can serve. This is a security measure to prevent HTTP Host header attacks, which are possible even under many seemingly-safe web server configurations. By default this is set to `*` allowing any host. Use space to delimit more than one host. +| `APPRISE_PLUGIN_PATHS` | 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). You may define one or more paths (separated by comma `,`) here. By default the `apprise_api/var/plugin` directory is scanned (which does not include anything). Feel free to set this to an empty string to disable any custom plugin loading. | `APPRISE_RECURSION_MAX` | This defines the number of times one Apprise API Server can (recursively) call another. This is to both support and mitigate abuse through [the `apprise://` schema](https://github.com/caronc/apprise/wiki/Notify_apprise_api) for those who choose to use it. When leveraged properly, you can increase this (recursion max) value and successfully load balance the handling of many notification requests through many additional API Servers. By default this value is set to `1` (one). | `APPRISE_WORKER_COUNT` | Defines the number of workers to run. by default this is calculated based on the number of threads detected. | `BASE_URL` | Those who are hosting the API behind a proxy that requires a subpath to gain access to this API should specify this path here as well. By default this is not set at all. @@ -313,7 +323,6 @@ pytest apprise_api ## Apprise Integration - First you'll need to have it installed: ```bash # install apprise into your environment @@ -363,6 +372,7 @@ apprise -vvv --body="There are donut's in the front hall if anyone wants any" \ ``` Alternatively we can set this up in a configuration file and even tie our local tags to our upstream ones like so: + ```nginx # Linux users can place this in ~/.apprise # Windows users can place this info in %APPDATA%/Apprise/apprise @@ -376,6 +386,7 @@ devteam=apprise://localhost:8000/{KEY}?tags=devteam ``` We could trigger our notification to our friends now like: + ```bash # Trigger our service: apprise -vvv --tag=devteam --body="Guys, don't forget about the audit tomorrow morning." @@ -405,3 +416,4 @@ a.add(config) # Send a test message a.notify('test message') ``` + diff --git a/apprise_api/api/tests/test_notify.py b/apprise_api/api/tests/test_notify.py index 4b7d887..363c6cb 100644 --- a/apprise_api/api/tests/test_notify.py +++ b/apprise_api/api/tests/test_notify.py @@ -63,6 +63,9 @@ class NotifyTests(SimpleTestCase): form = NotifyForm(data=form_data) assert form.is_valid() + # Required to prevent None from being passed into self.client.post() + del form.cleaned_data['attachment'] + # we always set a type if one wasn't done so already assert form.cleaned_data['type'] == apprise.NotifyType.INFO @@ -502,6 +505,9 @@ class NotifyTests(SimpleTestCase): form = NotifyForm(data=form_data) assert form.is_valid() + # Required to prevent None from being passed into self.client.post() + del form.cleaned_data['attachment'] + # we always set a type if one wasn't done so already assert form.cleaned_data['type'] == apprise.NotifyType.INFO @@ -741,7 +747,7 @@ class NotifyTests(SimpleTestCase): assert mock_notify.call_count == 1 assert response['content-type'] == 'text/html' - @patch('apprise.plugins.NotifyEmail.send') + @patch('apprise.plugins.NotifyEmail.NotifyEmail.send') def test_notify_with_filters(self, mock_send): """ Test workings of APPRISE_DENY_SERVICES and APPRISE_ALLOW_SERVICES diff --git a/apprise_api/api/tests/test_stateful_notify.py b/apprise_api/api/tests/test_stateful_notify.py index eefd73c..6761038 100644 --- a/apprise_api/api/tests/test_stateful_notify.py +++ b/apprise_api/api/tests/test_stateful_notify.py @@ -99,6 +99,9 @@ class StatefulNotifyTests(SimpleTestCase): form = NotifyForm(data=form_data) assert form.is_valid() + # Required to prevent None from being passed into self.client.post() + del form.cleaned_data['attachment'] + response = self.client.post( '/notify/{}'.format(key), form.cleaned_data) assert response.status_code == 200 @@ -143,6 +146,9 @@ class StatefulNotifyTests(SimpleTestCase): form = NotifyForm(data=form_data) assert form.is_valid() + # Required to prevent None from being passed into self.client.post() + del form.cleaned_data['attachment'] + response = self.client.post( '/notify/{}'.format(key), form.cleaned_data) assert response.status_code == 200 diff --git a/apprise_api/api/tests/test_stateless_notify.py b/apprise_api/api/tests/test_stateless_notify.py index 3ebd19c..a9bdcf7 100644 --- a/apprise_api/api/tests/test_stateless_notify.py +++ b/apprise_api/api/tests/test_stateless_notify.py @@ -54,6 +54,9 @@ class StatelessNotifyTests(SimpleTestCase): form = NotifyByUrlForm(data=form_data) assert form.is_valid() + # Required to prevent None from being passed into self.client.post() + del form.cleaned_data['attachment'] + response = self.client.post('/notify', form.cleaned_data) assert response.status_code == 200 assert mock_notify.call_count == 1 @@ -68,6 +71,9 @@ class StatelessNotifyTests(SimpleTestCase): form = NotifyByUrlForm(data=form_data) assert form.is_valid() + # Required to prevent None from being passed into self.client.post() + del form.cleaned_data['attachment'] + response = self.client.post('/notify', form.cleaned_data) assert response.status_code == 200 assert mock_notify.call_count == 1 @@ -109,6 +115,9 @@ class StatelessNotifyTests(SimpleTestCase): form = NotifyByUrlForm(data=form_data) assert form.is_valid() + # Required to prevent None from being passed into self.client.post() + del form.cleaned_data['attachment'] + response = self.client.post('/notify', form.cleaned_data) assert response.status_code == 424 assert mock_notify.call_count == 2 @@ -139,6 +148,9 @@ class StatelessNotifyTests(SimpleTestCase): form = NotifyByUrlForm(data=form_data) assert form.is_valid() + # Required to prevent None from being passed into self.client.post() + del form.cleaned_data['attachment'] + # recursion value is within correct limits response = self.client.post('/notify', form.cleaned_data, **headers) assert response.status_code == 200 @@ -221,6 +233,9 @@ class StatelessNotifyTests(SimpleTestCase): form = NotifyByUrlForm(data=form_data) assert form.is_valid() + # Required to prevent None from being passed into self.client.post() + del form.cleaned_data['attachment'] + # This still works as the environment variable kicks in response = self.client.post('/notify', form.cleaned_data) assert response.status_code == 200 @@ -340,7 +355,7 @@ class StatelessNotifyTests(SimpleTestCase): assert response.status_code == 400 assert mock_notify.call_count == 0 - @patch('apprise.plugins.NotifyJSON.send') + @patch('apprise.plugins.NotifyJSON.NotifyJSON.send') def test_notify_with_filters(self, mock_send): """ Test workings of APPRISE_DENY_SERVICES and APPRISE_ALLOW_SERVICES diff --git a/apprise_api/api/views.py b/apprise_api/api/views.py index c522380..0f9de4b 100644 --- a/apprise_api/api/views.py +++ b/apprise_api/api/views.py @@ -752,14 +752,11 @@ class NotifyView(View): status=status, ) - # - # Apply Any Global Filters (if identified) - # - apply_global_filters() - # Prepare our keyword arguments (to be passed into an AppriseAsset # object) - kwargs = {} + kwargs = { + 'plugin_paths': settings.APPRISE_PLUGIN_PATHS, + } if body_format: # Store our defined body format @@ -792,9 +789,13 @@ class NotifyView(View): if uid: kwargs['_uid'] = uid + # + # Apply Any Global Filters (if identified) + # + apply_global_filters() + # Prepare ourselves a default Asset - asset = None if not body_format else \ - apprise.AppriseAsset(body_format=body_format) + asset = apprise.AppriseAsset(**kwargs) # Prepare our apprise object a_obj = apprise.Apprise(asset=asset) @@ -958,7 +959,9 @@ class StatelessNotifyView(View): # Prepare our keyword arguments (to be passed into an AppriseAsset # object) - kwargs = {} + kwargs = { + 'plugin_paths': settings.APPRISE_PLUGIN_PATHS, + } if body_format: # Store our defined body format @@ -991,15 +994,14 @@ class StatelessNotifyView(View): if uid: kwargs['_uid'] = uid - # Prepare ourselves a default Asset - asset = None if not body_format else \ - apprise.AppriseAsset(body_format=body_format) - # # Apply Any Global Filters (if identified) # apply_global_filters() + # Prepare ourselves a default Asset + asset = apprise.AppriseAsset(**kwargs) + # Prepare our apprise object a_obj = apprise.Apprise(asset=asset) diff --git a/apprise_api/var/plugin/README.md b/apprise_api/var/plugin/README.md new file mode 100644 index 0000000..1b23353 --- /dev/null +++ b/apprise_api/var/plugin/README.md @@ -0,0 +1,3 @@ +# Custom Plugin Directory + +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).