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 2 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
1 change: 1 addition & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ 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``


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", "attachment": "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
169 changes: 151 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,151 @@ 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',
},
}
```

:::{important}
[ntfy publishing options] outlines different ways to marshal data to the ntfy
HTTP API. mqttwarn is using HTTP headers for serializing values, because the
HTTP body will already be used for the attachment file. Because of this, you
are not able to use UTF-8 characters within your message text, they will be
replaced by placeholder characters like `?`.
:::
Copy link
Member Author

@amotl amotl Apr 21, 2023

Choose a reason for hiding this comment

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

mqttwarn is using HTTP headers for serializing values. Because of this, you are not able to use UTF-8 characters within your message text, they will be replaced by placeholder characters like ?.

@binwiederhier outlined a potential solution for this slight drawback, and figured it would be trivial to implement on behalf of ntfy, see caronc/apprise#866 (comment). 🌻

Choose a reason for hiding this comment

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

As I said in the other comment; ntfy server does support UTF-8 in headers, so if you send valid UTF-8 in headers it'll work. But because the HTTP spec does not officially support UTF-8, your libraries or language may not support it.

The suggested RFC encoding would fix this, obviously.

Copy link
Member Author

@amotl amotl Apr 21, 2023

Choose a reason for hiding this comment

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

Hi Philipp.

ntfy server does support UTF-8 in headers, so if you send valid UTF-8 in headers it'll work.

Ah. Thanks for mentioning it once again.

But because the HTTP spec does not officially support UTF-8, your libraries or language may not support it.

Right, the Python client libraries would currently already croak on that detail, raising a corresponding exception. I will see if a workaround can be applied in form of a monkey patch or such.

But we agree it's dangerous, because there may be HTTP intermediaries in between, which might scramble those headers, right?

Encoding the headers using RFC 2047 would fix this, obviously.

I will very much appreciate such an improvement to ntfy. Until it will converge, I will eventually look into the monkey patch solution, or just let it sit like it is.

Thank you so much for your swift responses, and keep up the spirit.

With kind regards,
Andreas.

Choose a reason for hiding this comment

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

Works wonderfully: binwiederhier/ntfy#707

Choose a reason for hiding this comment

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

It's merged and will be in the next release

Copy link
Member Author

Choose a reason for hiding this comment

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

That was fast. Thank you very much!

Copy link
Member Author

@amotl amotl Apr 23, 2023

Choose a reason for hiding this comment

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

I've adjusted the client-side implementation correspondingly with 609d79f. Thank you again!

Copy link
Member Author

Choose a reason for hiding this comment

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

Hi again,

at 609d79f#r110220447, you've commented:

I didn't do tags. Only title and message. Though tags are probably a good idea too.

This has already been added with binwiederhier/ntfy@59a5077713 now - thank you so much! I've complemented this improvement with b67e390, where RFC 2047 encoding will now only be applied to specific fields, title, message, and tags.

When looking through the available fields once more, I only discovered that the Action's label value might be another candidate for being encoded with RFC 2047, in order to use UTF-8 characters on the action buttons, when using HTTP header transport mode. But please consider this only as a discovery on my end, I personally do not have a need for that.

Thanks again for the quick turnaround on this matter.

With kind regards,
Andreas.


{#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',
'attachment': '/tmp/ntfy-attachment-{slot}-{label}.png',
Copy link
Member Author

Choose a reason for hiding this comment

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

I've introduced the attachment option/field to signal to mqttwarn that it should attach a local file to the ntfy notification, in order to support @sevmonster's use case.

However, I do not like the naming yet, as it's easy to confuse with the attach field. We should choose another name. I am leaning towards just using file instead. Do you have any other suggestions?

Copy link
Member Author

@amotl amotl Apr 21, 2023

Choose a reason for hiding this comment

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

I've renamed the option to file with 40e2efd, as proposed. I think it's a better choice to avoid confusion.

}
}
```
:::{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`, and `email`.

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.


[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


### `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