Skip to content
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

[ntfy] Add dedicated service plugin ntfy #638

Merged
merged 6 commits into from
Apr 26, 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
3 changes: 3 additions & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ in progress
- [file] Allow writing of binary content. Thanks, @sevmonster.
- [ux] Rename subcommand ``mqttwarn make-samplefuncs`` to ``mqttwarn make-udf``,
and adjust naming.
- [ntfy] Add dedicated service plugin ``ntfy``
- [ntfy] Use RFC 2047 for encoding HTTP header values
- [ntfy] Add more fields: icon, cache, firebase, unifiedpush


2023-04-11 0.33.0
Expand Down
18 changes: 11 additions & 7 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -183,18 +183,22 @@ you an idea how to pass relevant information on the command line using JSON::
# Launch "pushover" service plugin
mqttwarn --plugin=pushover --options='{"title": "About", "message": "Hello world", "addrs": ["userkey", "token"], "priority": 6}'

# Launch "ssh" service plugin from the command line
# Launch "ntfy" service plugin
mqttwarn --plugin=ntfy --options='{"addrs": {"url": "http://localhost:5555/testdrive"}, "title": "Example notification", "message": "Hello world"}' --data='{"tags": "foo,bar,äöü", "priority": "high"}'

# Launch "ntfy" service plugin, and add remote attachment
mqttwarn --plugin=ntfy --options='{"addrs": {"url": "http://localhost:5555/testdrive"}, "title": "Example notification", "message": "Hello world"}' --data='{"attach": "https://unsplash.com/photos/spdQ1dVuIHw/download?w=320", "filename": "goat.jpg"}'

# Launch "ntfy" service plugin, and add attachment from local filesystem
mqttwarn --plugin=ntfy --options='{"addrs": {"url": "http://localhost:5555/testdrive", "file": "goat.jpg"}, "title": "Example notification", "message": "Hello world"}'

# Launch "ssh" service plugin
mqttwarn --plugin=ssh --config='{"host": "ssh.example.org", "port": 22, "user": "foo", "password": "bar"}' --options='{"addrs": ["command with substitution %s"], "payload": "{\"args\": \"192.168.0.1\"}"}'

# Launch "cloudflare_zone" service plugin from "mqttwarn-contrib", passing "--config" parameters via command line
# Launch "cloudflare_zone" service plugin from "mqttwarn-contrib"
pip install mqttwarn-contrib
mqttwarn --plugin=mqttwarn_contrib.services.cloudflare_zone --config='{"auth-email": "foo", "auth-key": "bar"}' --options='{"addrs": ["0815", "www.example.org", ""], "message": "192.168.0.1"}'

# Submit notification to "ntfy", using Apprise service plugin.
mqttwarn --plugin=apprise \
--config='{"baseuri": "ntfy://user:[email protected]/topic1/topic2"}' \
--options='{"addrs": [], "title": "Example notification", "message": "Hello world"}'


Also, the ``--config-file`` parameter can be used to optionally specify the
path to a configuration file.
Expand Down
172 changes: 154 additions & 18 deletions docs/notifier-catalog.md
Original file line number Diff line number Diff line change
Expand Up @@ -223,22 +223,22 @@ 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
So, for example, you can propagate parameters to the [Apprise JSON HTTP POST
Notifications plugin] by either pre-setting them as URL query parameters, like
```
ntfy://user:[email protected]/topic1/[email protected]
json://localhost/?:sound=oceanwave
```
or by submitting them within a JSON-formatted MQTT message, like
```json
{"priority": "high", "tags": "foo,bar", "click": "https://httpbin.org/headers"}
{":sound": "oceanwave", "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 JSON HTTP POST Notifications plugin]: https://github.com/caronc/apprise/wiki/Notify_Custom_JSON


### `apprise_single`
Expand All @@ -256,7 +256,7 @@ Apprise to E-Mail, an HTTP endpoint, and a Discord channel.

```ini
[defaults]
launch = apprise-mail, apprise-json, apprise-discord, apprise-ntfy
launch = apprise-mail, apprise-json, apprise-discord

[config:apprise-mail]
; Dispatch message as e-mail.
Expand All @@ -283,16 +283,9 @@ baseuri = 'json://localhost:1234/mqtthook'
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:[email protected]/topic1/topic2'

[apprise-single-test]
topic = apprise/single/#
targets = apprise-mail:demo, apprise-json, apprise-discord, apprise-ntfy
targets = apprise-mail:demo, apprise-json, apprise-discord
format = Alarm from {device}: {payload}
title = Alarm from {device}
```
Expand Down Expand Up @@ -325,7 +318,6 @@ 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:[email protected]/topic1/topic2' } ],
'demo-mailto' : [ {
'baseuri': 'mailtos://smtp_username:[email protected]',
'recipients': ['[email protected]', '[email protected]'],
Expand All @@ -336,7 +328,7 @@ targets = {

[apprise-multi-test]
topic = apprise/multi/#
targets = apprise-multi:demo-http, apprise-multi:demo-discord, apprise-multi:demo-mailto, apprise-multi:demo-ntfy
targets = apprise-multi:demo-http, apprise-multi:demo-discord, apprise-multi:demo-mailto
format = Alarm from {device}: {payload}
title = Alarm from {device}
```
Expand Down Expand Up @@ -1746,10 +1738,154 @@ Requires:

### `ntfy`

Support for [ntfy] is provided through Apprise, see [apprise_single](#apprise_single)
and [apprise_multi](#apprise_multi).
> [ntfy] (pronounce: _notify_) is a simple HTTP-based [pub-sub] notification service.
> It allows you to send notifications to your phone or desktop via scripts from
> any computer, entirely without signup, cost or setup.
> [ntfy is also open source](https://github.com/binwiederhier/ntfy), if you want to
> run an instance on your own premises.

ntfy uses topics to address communication channels. This topic is part of the
HTTP API URL.

To use the hosted variant on `ntfy.sh`, just provide an URL including the topic.
```ini
[config:ntfy]
targets = {
'test': 'https://ntfy.sh/testdrive',
}
```

When running your own instance, you would use a custom URL here.
```ini
[config:ntfy]
targets = {
'test': 'http://username:password@localhost:5555/testdrive',
}
```

In order to specify more options, please wrap your ntfy URL into a dictionary
under the `url` key. This way, additional options can be added.
```ini
[config:ntfy]
targets = {
'test': {
'url': 'https://ntfy.sh/testdrive',
},
}
```

:::{note}
[ntfy publishing options] outlines different ways to marshal data to the ntfy
HTTP API. mqttwarn is using the HTTP PUT method, where the HTTP body is used
for the attachment file, and HTTP headers are used for all other ntfy option
fields, encoded with [RFC 2047] MIME [quoted-printable encoding].
:::

{#ntfy-remote-attachments}
#### Remote attachments
In order to submit notifications with an attachment file at a remote location,
use the `attach` field. Optionally, the `filename` field can be used to assign
a different name to the file.
```ini
[config:ntfy]
targets = {
'test': {
'url': 'https://ntfy.sh/testdrive',
'attach': 'https://unsplash.com/photos/spdQ1dVuIHw/download?w=320',
Copy link
Member Author

Choose a reason for hiding this comment

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

attach is the standard ntfy field to attach a file from a URL.

'filename': 'goat.jpg',
},
}
```

{#ntfy-local-attachments}
#### Local attachments
By using the `attachment` option, you can add an attachment to your message, local
to the machine mqttwarn is running on. The file will be uploaded when submitting
the notification, and ntfy will serve it for clients so that you don't have to. In
order to address the file, you can provide a path template, where the transformation
data will also get interpolated into.
```ini
[config:ntfy]
targets = {
'test': {
'url': 'https://ntfy.sh/testdrive',
'file': '/tmp/ntfy-attachment-{slot}-{label}.png',
}
}
```
:::{important}
In order to allow users to **upload** and attach files to notifications, you will
need to enable the corresponding ntfy feature by simply configuring an attachment
cache directory and a base URL (`attachment-cache-dir`, `base-url`), see
[ntfy stored attachments].
:::
:::{note}
When mqttwarn processes a message, and accessing the file raises an error, it gets
handled gracefully. In this way, notifications will be triggered even when attaching
the file fails for whatever reasons.
:::

#### Publishing options
You can use all the available [ntfy publishing options], by using the corresponding
option names listed within `NTFY_FIELD_NAMES`, which are: `message`, `title`, `tags`,
`priority`, `actions`, `click`, `attach`, `filename`, `delay`, `icon`, `email`,
`cache`, `firebase`, and `unifiedpush`. See also the [list of all ntfy option fields].

You can obtain ntfy option fields from _three_ contexts in total, as implemented
by the `obtain_ntfy_fields` function. Effectively, that means that you can place
them either within the `targets` address descriptor, within the configuration
section, or submit them using a JSON MQTT message and a corresponding decoder
function into the transformation data dictionary.

For example, you can always send a `priority` field using MQTT/JSON, or use one of
those configuration snippets, which are equivalent.
```ini
[config:ntfy]
targets = {
'test': {
'url': 'https://ntfy.sh/testdrive',
'priority': 'high',
}
}
```
```ini
[config:ntfy]
targets = {
'test': {
'url': 'https://ntfy.sh/testdrive',
}
}
priority = high
```

The highest precedence takes data coming in from the transformation data dictionary,
followed by option fields coming in from the per-recipient `targets` address descriptor,
followed by option fields defined on the `[config:ntfy]` configuration section.

#### Examples

1. This is another way to write the "[remote attachments](#ntfy-remote-attachments)"
example, where all ntfy options are located on the configuration section, so they
will apply for all configured target addresses.
```ini
[config:ntfy]
targets = {'test': 'https://ntfy.sh/testdrive'}
attach = https://unsplash.com/photos/spdQ1dVuIHw/download?w=320
filename = goat.jpg
```

2. The tutorial [](#processing-frigate-events) explains how to configure mqttwarn to
notify the user with events emitted by Frigate, a network video recorder (NVR)
with realtime local object detection for IP cameras.


[list of all ntfy option fields]: https://docs.ntfy.sh/publish/#list-of-all-parameters
[ntfy]: https://ntfy.sh/
[ntfy publishing options]: https://docs.ntfy.sh/publish/
[ntfy stored attachments]: https://docs.ntfy.sh/config/#attachments
[pub-sub]: https://en.wikipedia.org/wiki/Publish%E2%80%93subscribe_pattern
[quoted-printable encoding]: https://en.wikipedia.org/wiki/Quoted-printable
[RFC 2047]: https://datatracker.ietf.org/doc/html/rfc2047


### `desktopnotify`
Expand Down
15 changes: 8 additions & 7 deletions mqttwarn/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ def run():
Usage:
{program} [make-config]
{program} [make-udf]
{program} [--config=] [--config-file=] [--plugin=] [--options=]
{program} [--config=] [--config-file=] [--plugin=] [--options=] [--data=]
{program} --version
{program} (-h | --help)

Expand All @@ -39,8 +39,8 @@ def run():
[--plugin=] The plugin name to load. This can either be a
full qualified Python package/module name or a
path to a Python file.
[--options=] Configuration options to propagate to the plugin
entrypoint.
[--options=] Configuration options to propagate to the plugin entrypoint.
[--data=] Data to propagate to the plugin entrypoint.

Bootstrapping options:
make-config Dump configuration file blueprint to STDOUT,
Expand Down Expand Up @@ -76,22 +76,23 @@ def run():

# Decode arguments
arg_plugin = options["--plugin"]
arg_options = json.loads(options["--options"])
arg_options = options["--options"] and json.loads(options["--options"]) or {}
arg_data = options["--data"] and json.loads(options["--data"]) or {}
arg_config = None
if "--config" in options and options["--config"] is not None:
arg_config = json.loads(options["--config"])

# Launch service plugin in standalone mode
launch_plugin_standalone(
arg_plugin, arg_options, configfile=options.get("--config-file"), config_more=arg_config
arg_plugin, arg_options, arg_data, configfile=options.get("--config-file"), config_more=arg_config
)

# Run mqttwarn in service mode when no command line arguments are given
else:
run_mqttwarn()


def launch_plugin_standalone(plugin, options, configfile=None, config_more=None):
def launch_plugin_standalone(plugin, options, data, configfile=None, config_more=None):

# Optionally load configuration file
does_not_exist = False
Expand Down Expand Up @@ -120,7 +121,7 @@ def launch_plugin_standalone(plugin, options, configfile=None, config_more=None)
logger.info('Running service plugin "{}" with options "{}"'.format(plugin, options))

# Launch service plugin
run_plugin(config=config, name=plugin, options=options)
run_plugin(config=config, name=plugin, options=options, data=data)


def run_mqttwarn():
Expand Down
4 changes: 2 additions & 2 deletions mqttwarn/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -784,7 +784,7 @@ def bootstrap(config=None, scriptname=None):
SCRIPTNAME = scriptname


def run_plugin(config=None, name=None, options=None):
def run_plugin(config=None, name=None, options=None, data=None):
"""
Run service plugins directly without the
dispatching and transformation machinery.
Expand Down Expand Up @@ -817,7 +817,7 @@ def run_plugin(config=None, name=None, options=None):
item.config = config.config("config:" + name)
item.service = srv
item.target = "mqttwarn"
item.data = {} # FIXME
item.data = data or {}

# Launch plugin
module = service_plugins[name]["module"]
Expand Down
3 changes: 2 additions & 1 deletion mqttwarn/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ def enum(self):
# Covering old- and new-style configuration layouts. `addrs` has
# originally been a list of strings, has been expanded to be a
# list of dictionaries (Apprise), and to be a dictionary (Pushsafer).
addrs_type = Union[List[Union[str, Dict[str, str]]], Dict[str, str]]
addrs_type = Union[List[Union[str, Dict[str, str]]], Dict[str, str], str]


@dataclass
Expand All @@ -52,6 +52,7 @@ class ProcessorItem:
service: Optional[str] = None
target: Optional[str] = None
config: Dict = field(default_factory=dict)
# TODO: `addrs` can also be a string or dictionary now.
addrs: addrs_type = field(default_factory=list) # type: ignore[assignment]
priority: Optional[int] = None
topic: Optional[str] = None
Expand Down
2 changes: 1 addition & 1 deletion mqttwarn/services/apprise_multi.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ def plugin(srv, item):
# Collect URL parameters.
params = OrderedDict()

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

# Apply addressee information.
Expand Down
2 changes: 1 addition & 1 deletion mqttwarn/services/apprise_single.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ def plugin(srv, item):
# Collect URL parameters.
params = OrderedDict()

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

# Apply addressee information.
Expand Down
2 changes: 0 additions & 2 deletions mqttwarn/services/apprise_util.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,6 @@ def get_all_template_argument_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:
Expand Down
Loading