diff --git a/CHANGES.rst b/CHANGES.rst index 085ff7d2..70923d22 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -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 diff --git a/HANDBOOK.md b/HANDBOOK.md index af218330..bda48b72 100644 --- a/HANDBOOK.md +++ b/HANDBOOK.md @@ -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` @@ -600,7 +618,7 @@ 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] @@ -608,7 +626,7 @@ baseuri = 'discord://4174216298/JHMHI8qBe7bk2ZwO5U711o3dV_js' ; 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/# @@ -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'], diff --git a/mqttwarn/services/apprise.py b/mqttwarn/services/apprise.py index d3084c34..e7a8338b 100644 --- a/mqttwarn/services/apprise.py +++ b/mqttwarn/services/apprise.py @@ -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) diff --git a/mqttwarn/services/apprise_multi.py b/mqttwarn/services/apprise_multi.py index 59cab007..4847b017 100644 --- a/mqttwarn/services/apprise_multi.py +++ b/mqttwarn/services/apprise_multi.py @@ -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.""" @@ -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: @@ -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) diff --git a/mqttwarn/services/apprise_single.py b/mqttwarn/services/apprise_single.py index b2258c91..7a8ae482 100644 --- a/mqttwarn/services/apprise_single.py +++ b/mqttwarn/services/apprise_single.py @@ -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.""" @@ -39,6 +42,11 @@ 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: @@ -46,10 +54,8 @@ def plugin(srv, item): 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. diff --git a/mqttwarn/services/apprise_util.py b/mqttwarn/services/apprise_util.py new file mode 100644 index 00000000..e8693bf2 --- /dev/null +++ b/mqttwarn/services/apprise_util.py @@ -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 diff --git a/tests/conftest.py b/tests/conftest.py index 9140115e..2edab302 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -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 diff --git a/tests/services/test_ntfy.py b/tests/services/test_ntfy.py new file mode 100644 index 00000000..10965775 --- /dev/null +++ b/tests/services/test_ntfy.py @@ -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"}, + ) + + 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