Skip to content

Improve Apprise integration, with a focus on Ntfy #619

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Feb 13, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ in progress
- Add versioning based on Git tags, using ``versioningit``. This will aid in
telling PR- and nightly releases apart from GA releases when running
``mqttwarn --version``.
- Improve Apprise integration by propagating the mqttwarn data dictionary into
the Apprise plugin template arguments.


2022-11-21 0.31.0
Expand Down
26 changes: 22 additions & 4 deletions HANDBOOK.md
Original file line number Diff line number Diff line change
Expand Up @@ -554,11 +554,29 @@ XBMC, Vonage, Webex Teams.
80+ notification services supported by Apprise.

Notification services are addressed by URL, see [Apprise URL Basics].
Please consult the Apprise documentation about more details.
Please consult the [Apprise documentation] about more details.

Apprise notification plugins obtain different kinds of configuration or
template arguments. mqttwarn supports propagating them from either the
``baseuri`` configuration setting, or from its data dictionary to the Apprise
plugin invocation.

So, for example, you can propagate parameters to the [Apprise Ntfy plugin]
by either pre-setting them as URL query parameters, like
```
ntfy://user:password@ntfy.example.org/topic1/topic2?email=test@example.org
```
or by submitting them within a JSON-formatted MQTT message, like
```json
{"priority": "high", "tags": "foo,bar", "click": "https://httpbin.org/headers"}
```


[Apprise]: https://github.com/caronc/apprise
[Apprise documentation]: https://github.com/caronc/apprise/wiki
[Apprise URL Basics]: https://github.com/caronc/apprise/wiki/URLBasics
[Apprise Notification Services]: https://github.com/caronc/apprise/wiki#notification-services
[Apprise Ntfy plugin]: https://github.com/caronc/apprise/wiki/Notify_ntfy


### `apprise_single`
Expand Down Expand Up @@ -600,15 +618,15 @@ baseuri = 'json://localhost:1234/mqtthook'
; https://github.com/caronc/apprise/wiki/Notify_discord
; https://discord.com/developers/docs/resources/webhook
; discord://{WebhookID}/{WebhookToken}/
module = 'apprise'
module = 'apprise_single'
baseuri = 'discord://4174216298/JHMHI8qBe7bk2ZwO5U711o3dV_js'

[config:apprise-ntfy]
; Dispatch message to ntfy.
; https://github.com/caronc/apprise/wiki/URLBasics
; https://github.com/caronc/apprise/wiki/Notify_ntfy
module = 'apprise_single'
baseuri = 'ntfy://user:password/ntfy.example.org/topic1/topic2'
baseuri = 'ntfy://user:password@ntfy.example.org/topic1/topic2'

[apprise-single-test]
topic = apprise/single/#
Expand Down Expand Up @@ -645,7 +663,7 @@ module = 'apprise_multi'
targets = {
'demo-http' : [ { 'baseuri': 'json://localhost:1234/mqtthook' }, { 'baseuri': 'json://daq.example.org:5555/foobar' } ],
'demo-discord' : [ { 'baseuri': 'discord://4174216298/JHMHI8qBe7bk2ZwO5U711o3dV_js' } ],
'demo-ntfy' : [ { 'baseuri': 'ntfy://user:password/ntfy.example.org/topic1/topic2' } ],
'demo-ntfy' : [ { 'baseuri': 'ntfy://user:password@ntfy.example.org/topic1/topic2' } ],
'demo-mailto' : [ {
'baseuri': 'mailtos://smtp_username:smtp_password@mail.example.org',
'recipients': ['foo@example.org', 'bar@example.org'],
Expand Down
6 changes: 6 additions & 0 deletions mqttwarn/services/apprise.py
Original file line number Diff line number Diff line change
@@ -1 +1,7 @@
import warnings

from mqttwarn.services.apprise_single import plugin

warnings.warn("`mqttwarn.services.apprise` will be removed in a future release of mqttwarn. "
"Please use `mqttwarn.services.apprise_single` or `mqttwarn.services.apprise_multi` instead.",
category=DeprecationWarning)
15 changes: 10 additions & 5 deletions mqttwarn/services/apprise_multi.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,14 @@
__license__ = 'Eclipse Public License - v 1.0 (http://www.eclipse.org/legal/epl-v10.html)'

# https://github.com/caronc/apprise#developers
from urllib.parse import urlencode
from collections import OrderedDict

import apprise

from mqttwarn.services.apprise_util import obtain_apprise_arguments, add_url_params, get_all_template_argument_names

APPRISE_ALL_ARGUMENT_NAMES = get_all_template_argument_names()


def plugin(srv, item):
"""Send a message to multiple Apprise plugins."""
Expand Down Expand Up @@ -39,6 +42,10 @@ def plugin(srv, item):
# Collect URL parameters.
params = OrderedDict()

# Obtain and apply all possible Ntfy parameters from data dictionary.
params.update(obtain_apprise_arguments(item, APPRISE_ALL_ARGUMENT_NAMES))

# Apply addressee information.
if "recipients" in address:
to = ','.join(address["recipients"])
if to:
Expand All @@ -49,10 +56,8 @@ def plugin(srv, item):
if "sender_name" in address:
params["name"] = address["sender_name"]

# Add notification services by server url.
uri = baseuri
if params:
uri += '?' + urlencode(params)
# Add parameters to Apprise notification URL.
uri = add_url_params(baseuri, params)
srv.logging.info("Adding notification to: {}".format(uri))
apobj.add(uri)

Expand Down
16 changes: 11 additions & 5 deletions mqttwarn/services/apprise_single.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,14 @@
__license__ = 'Eclipse Public License - v 1.0 (http://www.eclipse.org/legal/epl-v10.html)'

# https://github.com/caronc/apprise#developers
from urllib.parse import urlencode
from collections import OrderedDict

import apprise

from mqttwarn.services.apprise_util import obtain_apprise_arguments, add_url_params, get_all_template_argument_names

APPRISE_ALL_ARGUMENT_NAMES = get_all_template_argument_names()


def plugin(srv, item):
"""Send a message to a single Apprise plugin."""
Expand Down Expand Up @@ -39,17 +42,20 @@ def plugin(srv, item):

# Collect URL parameters.
params = OrderedDict()

# Obtain and apply all possible Ntfy parameters from data dictionary.
params.update(obtain_apprise_arguments(item, APPRISE_ALL_ARGUMENT_NAMES))

# Apply addressee information.
if sender:
params["from"] = sender
if to:
params["to"] = to
if sender_name:
params["name"] = sender_name

# Add notification services by server url.
uri = baseuri
if params:
uri += '?' + urlencode(params)
# Add parameters to Apprise notification URL.
uri = add_url_params(baseuri, params)
apobj.add(uri)

# Submit notification.
Expand Down
53 changes: 53 additions & 0 deletions mqttwarn/services/apprise_util.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# -*- coding: utf-8 -*-
# (c) 2021-2023 The mqttwarn developers
from __future__ import absolute_import

from functools import lru_cache
from urllib.parse import urlparse, urlencode

from apprise import Apprise, ContentLocation

from mqttwarn.model import ProcessorItem


@lru_cache(maxsize=None)
def get_all_template_argument_names():
"""
Inquire all possible parameter names from all Apprise plugins.
"""
a = Apprise(asset=None, location=ContentLocation.LOCAL)
results = a.details()
plugin_infos = results['schemas']

all_arg_names = []
for plugin_info in plugin_infos:
arg_names = plugin_info["details"]["args"].keys()
all_arg_names += arg_names

return sorted(set(all_arg_names))


def obtain_apprise_arguments(item: ProcessorItem, arg_names: list) -> dict:
"""
Obtain eventual Apprise parameters from data dictionary.

https://github.com/caronc/apprise/wiki/Notify_ntfy#parameter-breakdown
"""
params = dict()
for arg_name in arg_names:
if isinstance(item.data, dict) and arg_name in item.data:
params[arg_name] = item.data[arg_name]
return params


def add_url_params(url: str, params: dict) -> str:
"""
Serialize query parameter dictionary and add it to URL.
"""
url_parsed = urlparse(url)
if params:
seperator = "?"
if url_parsed.query:
seperator = "&"
url += seperator + urlencode(params)
return url
3 changes: 3 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@

import pytest

# Needed to make Apprise not be mocked too much.
from mqttwarn.services.apprise_util import get_all_template_argument_names # noqa:F401

# Import custom fixtures.
from mqttwarn.testing.fixtures import mqttwarn_service as srv # noqa:F401

Expand Down
39 changes: 39 additions & 0 deletions tests/services/test_ntfy.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# -*- coding: utf-8 -*-
# (c) 2021-2023 The mqttwarn developers
from unittest import mock
from unittest.mock import call

from mqttwarn.model import ProcessorItem as Item
from mqttwarn.util import load_module_by_name


@mock.patch("apprise.Apprise", create=True)
@mock.patch("apprise.AppriseAsset", create=True)
def test_ntfy_success(apprise_asset, apprise_mock, srv, caplog):
module = load_module_by_name("mqttwarn.services.apprise_multi")

item = Item(
addrs=[
{
"baseuri": "ntfy://user:password@ntfy.example.org/topic1/topic2?email=test@example.org",
}
],
title="⚽ Message title ⚽",
message="⚽ Notification message ⚽",
data={"priority": "high", "tags": "foo,bar", "click": "https://httpbin.org/headers"},
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Currently, tags are propagated 1:1 from a string to a string, without any transformation in between.

The Apprise Ntfy parameter breakdown documentation says:

Variable Required Description
tags No The ntfy tags to associate with the ntfy post. Use a comma and/or space to specify more then one.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

However, @zoic21 outlined at #607 (comment), that they would like to submit tags as a list? Please raise your voice if this would be important to you, then we can try to add a corresponding translator.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hello,
No string to string is better, I used array because it's mandatory by ntfy when we used json but it's no necessary by apprise

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Excellent. Then let's just keep it as is, for the sake of simplicity.

)

outcome = module.plugin(srv, item)

assert apprise_mock.mock_calls == [
call(asset=mock.ANY),
call().add(
"ntfy://user:password@ntfy.example.org/topic1/topic2?email=test@example.org"
"&click=https%3A%2F%2Fhttpbin.org%2Fheaders&priority=high&tags=foo%2Cbar"
),
call().notify(body="⚽ Notification message ⚽", title="⚽ Message title ⚽"),
call().notify().__bool__(),
]

assert "Successfully sent message using Apprise" in caplog.messages
assert outcome is True