mirror of
https://github.com/caronc/apprise-api.git
synced 2025-02-09 06:19:31 +01:00
Added and/or support with tags (#104)
This commit is contained in:
parent
30d87c8284
commit
4e7610d80f
60
README.md
60
README.md
@ -123,10 +123,10 @@ 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.<br/>*Payload Parameters*<br/>📌 **urls**: Define one or more Apprise URL(s) here. Use a comma and/or space to separate one URL from the next.<br/>📌 **config**: Provide the contents of either a YAML or TEXT based Apprise configuration.<br/>📌 **format**: This field is only required if you've specified the _config_ parameter. Used to tell the server which of the supported (Apprise) configuration types you are passing. Valid options are _text_ and _yaml_.
|
||||
| `/del/{KEY}` | POST | Removes Apprise Configuration from the persistent store.
|
||||
| `/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)).
|
||||
| `/notify/{KEY}` | POST | Sends notification(s) to all of the end points you've previously configured associated with a *{KEY}*.<br/>*Payload Parameters*<br/>📌 **body**: Your message body. This is the *only* required field.<br/>📌 **title**: Optionally define a title to go along with the *body*.<br/>📌 **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.<br/>📌 **tag**: Optionally notify only those tagged accordingly.<br/>📌 **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`.
|
||||
| `/add/{KEY}` | POST | Saves Apprise Configuration (or set of URLs) to the persistent store.<br/>*Payload Parameters*<br/>📌 **urls**: Define one or more Apprise URL(s) here. Use a comma and/or space to separate one URL from the next.<br/>📌 **config**: Provide the contents of either a YAML or TEXT based Apprise configuration.<br/>📌 **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}*.<br/>*Payload Parameters*<br/>📌 **body**: Your message body. This is the *only* required field.<br/>📌 **title**: Optionally define a title to go along with the *body*.<br/>📌 **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.<br/>📌 **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.<br/>📌 **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`.
|
||||
| `/json/urls/{KEY}` | GET | Returns a JSON response object that contains all of the URLS and Tags associated with the key specified.
|
||||
| `/details` | GET | Set the `Accept` Header to `application/json` and retrieve a JSON response object that contains all of the supported Apprise URLs. See [here for more details](https://github.com/caronc/apprise/wiki/Development_Apprise_Details#apprise-details)
|
||||
|
||||
@ -154,6 +154,8 @@ As an example, the `/json/urls/{KEY}` response might return something like this:
|
||||
|
||||
You can pass in attributes to the `/json/urls/{KEY}` such as `privacy=1` which hides the passwords and secret tokens when returning the response. You can also set `tag=` and filter the returned results based on a comma separated set of tags. if no `tag=` is specified, then `tag=all` is used as the default.
|
||||
|
||||
Note, if `APPRISE_CONFIG_LOCK` is set, then `privacy=1` is always enforced preventing credentials from being leaked.
|
||||
|
||||
Here is an example using `curl` as to how someone might send a notification to everyone associated with the tag `abc123` (using `/notify/{key}`):
|
||||
|
||||
```bash
|
||||
@ -181,6 +183,48 @@ curl -X POST -d '{"tag":"devops", "body":"test message"}' \
|
||||
http://localhost:8000/notify/abc123
|
||||
```
|
||||
|
||||
### Tagging
|
||||
|
||||
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
|
||||
| TagA TagC, TagB | (TagA **AND** TagC) **OR** TagB
|
||||
| TagB TagC | TagB **AND** TagC
|
||||
|
||||
```bash
|
||||
# 'AND' Example
|
||||
# Send notification(s) to a {KEY} defined as 'abc123'
|
||||
# Notify the URLs associated with the 'devops' and 'after-hours' tag
|
||||
# The 'space' acts as an 'AND' You can also use '+' character (in spot of the
|
||||
# space to achieve the same results)
|
||||
curl -X POST -d '{"tag":"devops after-hours", "body":"repo outage"}' \
|
||||
-H "Content-Type: application/json" \
|
||||
http://localhost:8000/notify/abc123
|
||||
|
||||
|
||||
# 'OR' Example
|
||||
# Send notification(s) to a {KEY} defined as 'def456'
|
||||
# Notify the URLs associated with the 'dev' OR 'qa' tag
|
||||
# The 'comma' acts as an 'OR'. The whitespace around the comma is ignored (if
|
||||
# defined) You can also use '+' character (in spot of the space to achieve the
|
||||
# same results)
|
||||
curl -X POST -d '{"tag":"dev, qa", "body":"bug #000123 is back :("}' \
|
||||
-H "Content-Type: application/json" \
|
||||
http://localhost:8000/notify/def456
|
||||
|
||||
|
||||
# 'AND' and 'OR' Example
|
||||
# Send notification(s) to a {KEY} defined as 'projectX'
|
||||
# Notify the URLs associated with the 'leaders AND teamA' AND additionally
|
||||
# the 'leaders AND teamB'.
|
||||
curl -X POST -d '{"tag":"leaders teamA, leaders teamB", "body":"meeting now"}' \
|
||||
-H "Content-Type: application/json" \
|
||||
http://localhost:8000/notify/projectX
|
||||
```
|
||||
|
||||
### API Notes
|
||||
|
||||
- `{KEY}` must be 1-64 alphanumeric characters in length. In addition to this, the underscore (`_`) and dash (`-`) are also accepted.
|
||||
@ -198,8 +242,8 @@ The use of environment variables allow you to provide over-rides to default sett
|
||||
| `APPRISE_STATELESS_URLS` | For a non-persistent solution, you can take advantage of this global variable. Use this to define a default set of Apprise URLs to notify when using API calls to `/notify`. If no `{KEY}` is defined when calling `/notify` then the URLs defined here are used instead. By default, nothing is defined for this variable.
|
||||
| `APPRISE_STATEFUL_MODE` | This can be set to the following possible modes:<br/>📌 **hash**: This is also the default. It stores the server configuration in a hash formatted that can be easily indexed and compressed.<br/>📌 **simple**: Configuration is written straight to disk using the `{KEY}.cfg` (if `TEXT` based) and `{KEY}.yml` (if `YAML` based).<br/>📌 **disabled**: Straight up deny any read/write queries to the servers stateful store. Effectively turn off the Apprise Stateful feature completely.
|
||||
| `APPRISE_CONFIG_LOCK` | Locks down your API hosting so that you can no longer delete/update/access stateful information. Your configuration is still referenced when stateful calls are made to `/notify`. The idea of this switch is to allow someone to set their (Apprise) configuration up and then as an added security tactic, they may choose to lock their configuration down (in a read-only state). Those who use the Apprise CLI tool may still do it, however the `--config` (`-c`) switch will not successfully reference this access point anymore. You can however use the `apprise://` plugin without any problem ([see here for more details](https://github.com/caronc/apprise/wiki/Notify_apprise_api)). This defaults to `no` and can however be set to `yes` by simply defining the global variable as such.
|
||||
| `APPRISE_DENY_SERVICES` | A comma separated set of entries identifying what plugins to deny access to. You only need to identify one schema entry associated with a plugin to in turn disable all of it. Hence, if you wanted to disable the `glib` plugin, you do not need to additionally include `qt` as well since it's included as part of the (`dbus`) package; consequently specifying `qt` would in turn disable the `glib` module as well (another way to acomplish the same task). To exclude/disable 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. By default, this is initialized to `windows, dbus, gnome, macos, syslog` (blocking local actions from being issued inside of the docker container)
|
||||
| `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 exclusivly 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.
|
||||
| `APPRISE_DENY_SERVICES` | A comma separated set of entries identifying what plugins to deny access to. You only need to identify one schema entry associated with a plugin to in turn disable all of it. Hence, if you wanted to disable the `glib` plugin, you do not need to additionally include `qt` as well since it's included as part of the (`dbus`) package; consequently specifying `qt` would in turn disable the `glib` module as well (another way to accomplish the same task). To exclude/disable 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. By default, this is initialized to `windows, dbus, gnome, macos, syslog` (blocking local actions from being issued inside of the docker container)
|
||||
| `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_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).
|
||||
@ -318,6 +362,10 @@ import apprise
|
||||
|
||||
# Point our configuration to this API server:
|
||||
config = apprise.AppriseConfig()
|
||||
|
||||
# The following only works if APPRISE_CONFIG_LOCK is not set
|
||||
# if APPRISE_CONFIG_LOCK is set, you can optionally leverage the apprise://
|
||||
# URL instead.
|
||||
config.add('http://localhost:8000/get/{KEY}')
|
||||
|
||||
# Create our Apprise Instance
|
||||
|
@ -28,6 +28,7 @@ import requests
|
||||
from ..forms import NotifyForm
|
||||
import json
|
||||
import apprise
|
||||
from inspect import cleandoc
|
||||
|
||||
|
||||
class NotifyTests(SimpleTestCase):
|
||||
@ -166,6 +167,207 @@ class NotifyTests(SimpleTestCase):
|
||||
assert response['message'] == form_data['body']
|
||||
assert response['type'] == apprise.NotifyType.WARNING
|
||||
|
||||
@patch('requests.post')
|
||||
def test_advanced_notify_with_tags(self, mock_post):
|
||||
"""
|
||||
Test advanced notification handling when setting tags
|
||||
"""
|
||||
|
||||
# Disable Throttling to speed testing
|
||||
apprise.plugins.NotifyBase.request_rate_per_sec = 0
|
||||
# Ensure we're enabled for the purpose of our testing
|
||||
apprise.common.NOTIFY_SCHEMA_MAP['json'].enabled = True
|
||||
|
||||
# Prepare our response
|
||||
response = requests.Request()
|
||||
response.status_code = requests.codes.ok
|
||||
mock_post.return_value = response
|
||||
|
||||
# our key to use
|
||||
key = 'test_adv_notify_with_tags'
|
||||
|
||||
# Valid Yaml Configuration
|
||||
config = cleandoc("""
|
||||
version: 1
|
||||
tag: panic
|
||||
|
||||
urls:
|
||||
- json://user:pass@localhost?+url=1:
|
||||
tag: devops, notify
|
||||
- json://user:pass@localhost?+url=2:
|
||||
tag: devops, high
|
||||
- json://user:pass@localhost?+url=3:
|
||||
tag: cris, emergency
|
||||
""")
|
||||
|
||||
# Load our configuration (it will be detected as YAML)
|
||||
response = self.client.post(
|
||||
'/add/{}'.format(key),
|
||||
{'config': config})
|
||||
assert response.status_code == 200
|
||||
|
||||
# Preare our form data
|
||||
form_data = {
|
||||
'body': 'test notifiction',
|
||||
'type': apprise.NotifyType.INFO,
|
||||
'format': apprise.NotifyFormat.TEXT,
|
||||
}
|
||||
|
||||
# Send our notification
|
||||
response = self.client.post(
|
||||
'/notify/{}'.format(key), form_data)
|
||||
|
||||
# Nothing could be notified as there were no tag matches
|
||||
assert response.status_code == 424
|
||||
assert mock_post.call_count == 0
|
||||
|
||||
# Let's identify a tag, but note that it won't match anything
|
||||
# parameters
|
||||
response = self.client.post(
|
||||
'/notify/{}?tag=nomatch'.format(key), form_data)
|
||||
|
||||
# Nothing could be notified as there were no tag matches
|
||||
assert response.status_code == 424
|
||||
assert mock_post.call_count == 0
|
||||
|
||||
# Now let's do devops AND notify
|
||||
response = self.client.post(
|
||||
'/notify/{}?tag=devops notify'.format(key), form_data)
|
||||
|
||||
# Our notification was sent
|
||||
assert response.status_code == 200
|
||||
assert mock_post.call_count == 1
|
||||
|
||||
# Test our posted data
|
||||
response = json.loads(mock_post.call_args_list[0][1]['data'])
|
||||
headers = mock_post.call_args_list[0][1]['headers']
|
||||
assert response['title'] == ''
|
||||
assert response['message'] == form_data['body']
|
||||
assert response['type'] == apprise.NotifyType.INFO
|
||||
# Verify we matched the first entry only
|
||||
assert headers['url'] == '1'
|
||||
|
||||
# Reset our object
|
||||
mock_post.reset_mock()
|
||||
|
||||
# Now let's do panic
|
||||
response = self.client.post(
|
||||
'/notify/{}?tag=panic'.format(key), form_data)
|
||||
|
||||
# Our notification was sent to each match
|
||||
assert response.status_code == 200
|
||||
assert mock_post.call_count == 3
|
||||
|
||||
# Reset our object
|
||||
mock_post.reset_mock()
|
||||
|
||||
# Let's store our tag in our form
|
||||
form_data = {
|
||||
'body': 'test notifiction',
|
||||
'type': apprise.NotifyType.INFO,
|
||||
'format': apprise.NotifyFormat.TEXT,
|
||||
# (devops AND cris) OR (notify AND high)
|
||||
'tag': 'devops cris, notify high'
|
||||
}
|
||||
|
||||
# Send our notification
|
||||
response = self.client.post(
|
||||
'/notify/{}'.format(key), form_data)
|
||||
|
||||
# Nothing could be notified as there were no tag matches in our
|
||||
# form body that matched the anded comnbination
|
||||
assert response.status_code == 424
|
||||
assert mock_post.call_count == 0
|
||||
|
||||
# Trigger on high OR emergency (some empty garbage at the end to tidy/ignore
|
||||
form_data['tag'] = 'high, emergency, , ,'
|
||||
|
||||
# Send our notification
|
||||
response = self.client.post(
|
||||
'/notify/{}'.format(key), form_data)
|
||||
|
||||
# Our notification was sent
|
||||
assert response.status_code == 200
|
||||
# We'll trigger on 2 entries
|
||||
assert mock_post.call_count == 2
|
||||
|
||||
# Test our posted data
|
||||
response = json.loads(mock_post.call_args_list[0][1]['data'])
|
||||
headers = mock_post.call_args_list[0][1]['headers']
|
||||
assert response['title'] == ''
|
||||
assert response['message'] == form_data['body']
|
||||
assert response['type'] == apprise.NotifyType.INFO
|
||||
# Verify we matched the first entry only
|
||||
assert headers['url'] == '2'
|
||||
|
||||
response = json.loads(mock_post.call_args_list[1][1]['data'])
|
||||
headers = mock_post.call_args_list[1][1]['headers']
|
||||
assert response['title'] == ''
|
||||
assert response['message'] == form_data['body']
|
||||
assert response['type'] == apprise.NotifyType.INFO
|
||||
# Verify we matched the first entry only
|
||||
assert headers['url'] == '3'
|
||||
|
||||
# Reset our object
|
||||
mock_post.reset_mock()
|
||||
|
||||
# Trigger on notify OR cris
|
||||
form_data['tag'] = 'notify, cris'
|
||||
|
||||
# Send our notification
|
||||
response = self.client.post(
|
||||
'/notify/{}'.format(key), form_data)
|
||||
|
||||
# Our notification was sent
|
||||
assert response.status_code == 200
|
||||
# We'll trigger on 2 entries
|
||||
assert mock_post.call_count == 2
|
||||
|
||||
# Test our posted data
|
||||
response = json.loads(mock_post.call_args_list[0][1]['data'])
|
||||
headers = mock_post.call_args_list[0][1]['headers']
|
||||
assert response['title'] == ''
|
||||
assert response['message'] == form_data['body']
|
||||
assert response['type'] == apprise.NotifyType.INFO
|
||||
# Verify we matched the first entry only
|
||||
assert headers['url'] == '1'
|
||||
|
||||
response = json.loads(mock_post.call_args_list[1][1]['data'])
|
||||
headers = mock_post.call_args_list[1][1]['headers']
|
||||
assert response['title'] == ''
|
||||
assert response['message'] == form_data['body']
|
||||
assert response['type'] == apprise.NotifyType.INFO
|
||||
# Verify we matched the first entry only
|
||||
assert headers['url'] == '3'
|
||||
|
||||
# Reset our object
|
||||
mock_post.reset_mock()
|
||||
|
||||
# Trigger on notify AND cris (should not match anything)
|
||||
form_data['tag'] = 'notify cris'
|
||||
|
||||
# Send our notification
|
||||
response = self.client.post(
|
||||
'/notify/{}'.format(key), form_data)
|
||||
|
||||
assert response.status_code == 424
|
||||
assert mock_post.call_count == 0
|
||||
|
||||
# Reset our object
|
||||
mock_post.reset_mock()
|
||||
|
||||
# Invalid characters in our tag
|
||||
form_data['tag'] = '$'
|
||||
|
||||
# Send our notification
|
||||
response = self.client.post(
|
||||
'/notify/{}'.format(key), form_data)
|
||||
|
||||
# Our notification was sent
|
||||
assert response.status_code == 400
|
||||
# We'll trigger on 2 entries
|
||||
assert mock_post.call_count == 0
|
||||
|
||||
@patch('apprise.NotifyBase.notify')
|
||||
def test_partial_notify_by_loaded_urls(self, mock_notify):
|
||||
"""
|
||||
|
@ -68,6 +68,21 @@ MIME_IS_FORM = re.compile(
|
||||
MIME_IS_JSON = re.compile(
|
||||
r'(text|application)/(x-)?json', re.I)
|
||||
|
||||
# Tags separated by space , &, or + are and'ed together
|
||||
# Tags separated by commas (even commas wrapped in spaces) are "or'ed" together
|
||||
# We start with a regular expression used to clean up provided tags
|
||||
TAG_VALIDATION_RE = re.compile(r'^\s*[a-z0-9\s| ,_-]+\s*$', re.IGNORECASE)
|
||||
|
||||
# In order to separate our tags only by comma's or '|' entries found
|
||||
TAG_DETECT_RE = re.compile(
|
||||
r'\s*([a-z0-9\s_&+-]+)(?=$|\s*[|,]\s*[a-z0-9\s&+_-|,])', re.I)
|
||||
|
||||
# Break apart our objects anded together
|
||||
TAG_AND_DELIM_RE = re.compile(r'[\s&+]+')
|
||||
|
||||
MIME_IS_JSON = re.compile(
|
||||
r'(text|application)/(x-)?json', re.I)
|
||||
|
||||
|
||||
class JSONEncoder(DjangoJSONEncoder):
|
||||
"""
|
||||
@ -596,6 +611,43 @@ class NotifyView(View):
|
||||
if not content.get('tag') and 'tag' in request.GET:
|
||||
content['tag'] = request.GET['tag']
|
||||
|
||||
if content.get('tag'):
|
||||
# Validation - Tag Logic:
|
||||
# "TagA" : TagA
|
||||
# "TagA, TagB" : TagA OR TagB
|
||||
# ['TagA', 'TagB'] : TagA OR TagB
|
||||
# [('TagA', 'TagC'), 'TagB'] : (TagA AND TagC) OR TagB
|
||||
# [('TagB', 'TagC')] : TagB AND TagC
|
||||
|
||||
if not TAG_VALIDATION_RE.match(content.get('tag')):
|
||||
msg = _('Unsupported characters found in tag definition.')
|
||||
status = ResponseCode.bad_request
|
||||
return HttpResponse(msg, status=status) \
|
||||
if not json_response else JsonResponse({
|
||||
'error': msg,
|
||||
},
|
||||
encoder=JSONEncoder,
|
||||
safe=False,
|
||||
status=status,
|
||||
)
|
||||
|
||||
# If we get here, our specified tag was valid
|
||||
tags = []
|
||||
for _tag in TAG_DETECT_RE.findall(content.get('tag')):
|
||||
tag = _tag.strip()
|
||||
if not tag:
|
||||
continue
|
||||
|
||||
# Disect our results
|
||||
group = TAG_AND_DELIM_RE.split(tag)
|
||||
if len(group) > 1:
|
||||
tags.append(tuple(group))
|
||||
else:
|
||||
tags.append(tag)
|
||||
|
||||
# Update our tag block
|
||||
content['tag'] = tags
|
||||
|
||||
#
|
||||
# Allow 'format' value to be specified as part of the URL
|
||||
# parameters if not found otherwise defined.
|
||||
|
Loading…
Reference in New Issue
Block a user