moat-link-notify 0.0.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- moat_link_notify-0.0.1/LICENSE.txt +14 -0
- moat_link_notify-0.0.1/Makefile +20 -0
- moat_link_notify-0.0.1/PKG-INFO +50 -0
- moat_link_notify-0.0.1/README.md +25 -0
- moat_link_notify-0.0.1/debian/.gitignore +6 -0
- moat_link_notify-0.0.1/debian/changelog +11 -0
- moat_link_notify-0.0.1/debian/control +26 -0
- moat_link_notify-0.0.1/debian/moat-link-notify.service +23 -0
- moat_link_notify-0.0.1/debian/rules +8 -0
- moat_link_notify-0.0.1/pyproject.toml +42 -0
- moat_link_notify-0.0.1/setup.cfg +4 -0
- moat_link_notify-0.0.1/src/moat/link/notify/__init__.py +227 -0
- moat_link_notify-0.0.1/src/moat/link/notify/_cfg.yaml +32 -0
- moat_link_notify-0.0.1/src/moat/link/notify/_main.py +37 -0
- moat_link_notify-0.0.1/src/moat/link/notify/ntfy.py +86 -0
- moat_link_notify-0.0.1/src/moat_link_notify.egg-info/PKG-INFO +50 -0
- moat_link_notify-0.0.1/src/moat_link_notify.egg-info/SOURCES.txt +19 -0
- moat_link_notify-0.0.1/src/moat_link_notify.egg-info/dependency_links.txt +1 -0
- moat_link_notify-0.0.1/src/moat_link_notify.egg-info/requires.txt +6 -0
- moat_link_notify-0.0.1/src/moat_link_notify.egg-info/top_level.txt +1 -0
- moat_link_notify-0.0.1/systemd/moat-link-notify.service +23 -0
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
The code in this repository, and all MoaT submodules it refers to,
|
|
2
|
+
is part of the MoaT project.
|
|
3
|
+
|
|
4
|
+
Unless a submodule's LICENSE.txt states otherwise, all included files are
|
|
5
|
+
licensed under the LGPL V3, as published by the FSF at
|
|
6
|
+
https://www.gnu.org/licenses/lgpl-3.0.html .
|
|
7
|
+
|
|
8
|
+
In addition to the LGPL's terms, the author(s) respectfully ask all users of
|
|
9
|
+
this code to contribute any bug fixes or enhancements. Also, please link back to
|
|
10
|
+
https://M-o-a-T.org.
|
|
11
|
+
|
|
12
|
+
Thank you.
|
|
13
|
+
|
|
14
|
+
Copyright © 2021 ff.: the MoaT contributor(s), as per the git changelog(s).
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
#!/usr/bin/make -f
|
|
2
|
+
|
|
3
|
+
PACKAGE = moat-link-notify
|
|
4
|
+
MAKEINCL ?= $(shell python3 -mmoat src path)/make/py
|
|
5
|
+
|
|
6
|
+
install: installsystemd
|
|
7
|
+
|
|
8
|
+
installsystemd:
|
|
9
|
+
mkdir -p $(PREFIX)/lib/systemd/system
|
|
10
|
+
cp systemd/*.service $(PREFIX)/lib/systemd/system/
|
|
11
|
+
|
|
12
|
+
ifneq ($(wildcard $(MAKEINCL)),)
|
|
13
|
+
include $(MAKEINCL)
|
|
14
|
+
# availabe via http://github.com/smurfix/sourcemgr
|
|
15
|
+
|
|
16
|
+
else
|
|
17
|
+
%:
|
|
18
|
+
@echo "Please fix 'python3 -mmoat src path'."
|
|
19
|
+
@exit 1
|
|
20
|
+
endif
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: moat-link-notify
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: Notification gateway for MoaT-Link
|
|
5
|
+
Author-email: Matthias Urlichs <matthias@urlichs.de>
|
|
6
|
+
Project-URL: homepage, https://m-o-a-t.org
|
|
7
|
+
Project-URL: repository, https://github.com/M-o-a-T/moat
|
|
8
|
+
Keywords: MoaT
|
|
9
|
+
Classifier: Intended Audience :: Developers
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Framework :: AsyncIO
|
|
12
|
+
Classifier: Framework :: Trio
|
|
13
|
+
Classifier: Framework :: AnyIO
|
|
14
|
+
Classifier: Development Status :: 4 - Beta
|
|
15
|
+
Requires-Python: >=3.8
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
License-File: LICENSE.txt
|
|
18
|
+
Requires-Dist: anyio~=4.2
|
|
19
|
+
Requires-Dist: asyncclick
|
|
20
|
+
Requires-Dist: attrs
|
|
21
|
+
Requires-Dist: httpx
|
|
22
|
+
Requires-Dist: moat-util~=0.62.2
|
|
23
|
+
Requires-Dist: moat-link~=0.2.46
|
|
24
|
+
Dynamic: license-file
|
|
25
|
+
|
|
26
|
+
# moat-link-notify
|
|
27
|
+
|
|
28
|
+
Notification gateway for MoaT-Link.
|
|
29
|
+
|
|
30
|
+
This package provides notification services for MoaT-Link, allowing you to send
|
|
31
|
+
notifications to external services like ntfy.sh.
|
|
32
|
+
|
|
33
|
+
## Installation
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
pip install moat-link-notify
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Usage
|
|
40
|
+
|
|
41
|
+
The notification gateway can be configured through MoaT-Link's configuration system.
|
|
42
|
+
|
|
43
|
+
## Dependencies
|
|
44
|
+
|
|
45
|
+
- moat-link: Core MoaT-Link functionality
|
|
46
|
+
- httpx: HTTP client for notification delivery
|
|
47
|
+
|
|
48
|
+
## License
|
|
49
|
+
|
|
50
|
+
This project is licensed under the same terms as MoaT.
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# moat-link-notify
|
|
2
|
+
|
|
3
|
+
Notification gateway for MoaT-Link.
|
|
4
|
+
|
|
5
|
+
This package provides notification services for MoaT-Link, allowing you to send
|
|
6
|
+
notifications to external services like ntfy.sh.
|
|
7
|
+
|
|
8
|
+
## Installation
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
pip install moat-link-notify
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## Usage
|
|
15
|
+
|
|
16
|
+
The notification gateway can be configured through MoaT-Link's configuration system.
|
|
17
|
+
|
|
18
|
+
## Dependencies
|
|
19
|
+
|
|
20
|
+
- moat-link: Core MoaT-Link functionality
|
|
21
|
+
- httpx: HTTP client for notification delivery
|
|
22
|
+
|
|
23
|
+
## License
|
|
24
|
+
|
|
25
|
+
This project is licensed under the same terms as MoaT.
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
moat-link-notify (0.0.1-2) unstable; urgency=medium
|
|
2
|
+
|
|
3
|
+
* New release for 25.8.6
|
|
4
|
+
|
|
5
|
+
-- Matthias Urlichs <matthias@urlichs.de> Mon, 05 Jan 2026 16:50:07 +0100
|
|
6
|
+
|
|
7
|
+
moat-link-notify (0.0.0-0) unstable; urgency=medium
|
|
8
|
+
|
|
9
|
+
* Initial release as separate package
|
|
10
|
+
|
|
11
|
+
-- Matthias Urlichs <matthias@urlichs.de> Sun, 05 Jan 2025 13:00:00 +0100
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
Source: moat-link-notify
|
|
2
|
+
Maintainer: Matthias Urlichs <matthias@urlichs.de>
|
|
3
|
+
Section: python
|
|
4
|
+
Priority: optional
|
|
5
|
+
Build-Depends: dh-python, python3-all, debhelper (>= 13),
|
|
6
|
+
python3-setuptools,
|
|
7
|
+
python3-wheel,
|
|
8
|
+
moat-src,
|
|
9
|
+
Standards-Version: 3.9.6
|
|
10
|
+
Homepage: https://github.com/smurfix/moat
|
|
11
|
+
X-DH-Compat: 13
|
|
12
|
+
|
|
13
|
+
Package: moat-link-notify
|
|
14
|
+
Architecture: all
|
|
15
|
+
Depends: ${misc:Depends}, ${python3:Depends},
|
|
16
|
+
moat-util,
|
|
17
|
+
moat-link,
|
|
18
|
+
python3-httpx,
|
|
19
|
+
systemd (>= 241),
|
|
20
|
+
Recommends:
|
|
21
|
+
python3-trio (>= 0.22),
|
|
22
|
+
Description: Notification gateway for MoaT-Link
|
|
23
|
+
This package provides notification services for MoaT-Link, allowing you to
|
|
24
|
+
send notifications to external services like ntfy.sh.
|
|
25
|
+
.
|
|
26
|
+
Part of the MoaT ecosystem for distributed home automation.
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
[Unit]
|
|
2
|
+
Description=MoaT-Link Notification Forwarder
|
|
3
|
+
|
|
4
|
+
ConditionFileNotEmpty=/etc/moat/moat.cfg
|
|
5
|
+
|
|
6
|
+
[Install]
|
|
7
|
+
WantedBy=multi-user.target
|
|
8
|
+
|
|
9
|
+
[Service]
|
|
10
|
+
Type=notify
|
|
11
|
+
ExecStart=/usr/bin/moat link notify run
|
|
12
|
+
|
|
13
|
+
RuntimeDirectory=moat/link
|
|
14
|
+
|
|
15
|
+
User=MoaT-Link
|
|
16
|
+
Group=MoaT
|
|
17
|
+
|
|
18
|
+
EnvironmentFile=/usr/lib/moat/link/env
|
|
19
|
+
EnvironmentFile=-/etc/moat/link.env
|
|
20
|
+
|
|
21
|
+
TimeoutSec=300
|
|
22
|
+
WatchdogSec=10
|
|
23
|
+
Restart=always
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
build-backend = "setuptools.build_meta"
|
|
3
|
+
requires = [ "setuptools", "wheel",]
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
classifiers = [
|
|
7
|
+
"Intended Audience :: Developers",
|
|
8
|
+
"Programming Language :: Python :: 3",
|
|
9
|
+
"Framework :: AsyncIO",
|
|
10
|
+
"Framework :: Trio",
|
|
11
|
+
"Framework :: AnyIO",
|
|
12
|
+
"Development Status :: 4 - Beta",
|
|
13
|
+
]
|
|
14
|
+
dependencies = [
|
|
15
|
+
"anyio ~= 4.2",
|
|
16
|
+
"asyncclick",
|
|
17
|
+
"attrs",
|
|
18
|
+
"httpx",
|
|
19
|
+
"moat-util ~= 0.62.2",
|
|
20
|
+
"moat-link ~= 0.2.46",
|
|
21
|
+
]
|
|
22
|
+
version = "0.0.1"
|
|
23
|
+
keywords = [ "MoaT",]
|
|
24
|
+
requires-python = ">=3.8"
|
|
25
|
+
name = "moat-link-notify"
|
|
26
|
+
description = "Notification gateway for MoaT-Link"
|
|
27
|
+
readme = "README.md"
|
|
28
|
+
|
|
29
|
+
[[project.authors]]
|
|
30
|
+
name = "Matthias Urlichs"
|
|
31
|
+
email = "matthias@urlichs.de"
|
|
32
|
+
|
|
33
|
+
[project.urls]
|
|
34
|
+
homepage = "https://m-o-a-t.org"
|
|
35
|
+
repository = "https://github.com/M-o-a-T/moat"
|
|
36
|
+
|
|
37
|
+
[tool.setuptools]
|
|
38
|
+
[tool.setuptools.packages.find]
|
|
39
|
+
where = ["src"]
|
|
40
|
+
|
|
41
|
+
[tool.setuptools.package-data]
|
|
42
|
+
"*" = ["*.yaml"]
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Notification package
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import anyio
|
|
8
|
+
import logging
|
|
9
|
+
import time
|
|
10
|
+
from abc import ABCMeta, abstractmethod
|
|
11
|
+
from contextlib import AsyncExitStack, asynccontextmanager
|
|
12
|
+
|
|
13
|
+
from moat.util import CtxObj, NotGiven, P, Path, as_service, attrdict
|
|
14
|
+
from moat.lib.priomap import PrioMap
|
|
15
|
+
|
|
16
|
+
from typing import TYPE_CHECKING
|
|
17
|
+
|
|
18
|
+
if TYPE_CHECKING:
|
|
19
|
+
from moat.link.client import Link
|
|
20
|
+
|
|
21
|
+
from collections.abc import AsyncIterator
|
|
22
|
+
from typing import NoReturn, Self
|
|
23
|
+
|
|
24
|
+
__all__ = ["Notifier", "Notify", "get_backend"]
|
|
25
|
+
|
|
26
|
+
logger = logging.getLogger(__name__)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class Notify:
|
|
30
|
+
"""
|
|
31
|
+
Notification runner.
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
link: Link
|
|
35
|
+
|
|
36
|
+
def __init__(self, cfg):
|
|
37
|
+
self.cfg = cfg
|
|
38
|
+
self.dropped: PrioMap[Notifier] = PrioMap()
|
|
39
|
+
|
|
40
|
+
async def run(self, link: Link, evt: anyio.Event | None = None):
|
|
41
|
+
"""
|
|
42
|
+
Task that reads notifications from MoaT-Link and posts them.
|
|
43
|
+
"""
|
|
44
|
+
self.link = link
|
|
45
|
+
async with AsyncExitStack() as ex:
|
|
46
|
+
backend = self.cfg.backend
|
|
47
|
+
if isinstance(backend, str):
|
|
48
|
+
backend = (backend,)
|
|
49
|
+
|
|
50
|
+
self._backends = {}
|
|
51
|
+
for name in backend:
|
|
52
|
+
cfg = self.cfg.get(name, attrdict())
|
|
53
|
+
try:
|
|
54
|
+
notifier = await ex.enter_async_context(get_backend(name, link, cfg))
|
|
55
|
+
except Exception as exc:
|
|
56
|
+
logger.warning("Backend %r", name, exc_info=exc)
|
|
57
|
+
else:
|
|
58
|
+
self._backends[name] = notifier
|
|
59
|
+
|
|
60
|
+
if not self._backends:
|
|
61
|
+
raise RuntimeError("No backend worked.")
|
|
62
|
+
|
|
63
|
+
try:
|
|
64
|
+
await self._run(evt)
|
|
65
|
+
finally:
|
|
66
|
+
with anyio.move_on_after(2, shield=True):
|
|
67
|
+
await self.send(
|
|
68
|
+
topic="error.notify",
|
|
69
|
+
title="Backend stopped",
|
|
70
|
+
msg="The backend terminated.",
|
|
71
|
+
prio="fatal",
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
async def send(self, **kw) -> None:
|
|
75
|
+
"""Send this notification to that topic."""
|
|
76
|
+
# reactivate dropped items
|
|
77
|
+
tm = time.monotonic()
|
|
78
|
+
retry: set[Notifier] = set()
|
|
79
|
+
error_seen = False
|
|
80
|
+
|
|
81
|
+
# Collect failed backends for retrying
|
|
82
|
+
while self.dropped:
|
|
83
|
+
b_e, t = self.dropped.peek()
|
|
84
|
+
if t > tm:
|
|
85
|
+
break
|
|
86
|
+
del self.dropped[b_e]
|
|
87
|
+
retry.add(b_e)
|
|
88
|
+
|
|
89
|
+
for name, b_e in list(self._backends.items()):
|
|
90
|
+
if b_e in self.dropped:
|
|
91
|
+
continue
|
|
92
|
+
try:
|
|
93
|
+
await b_e.send(**kw)
|
|
94
|
+
except Exception as exc:
|
|
95
|
+
# Log failed backends only once
|
|
96
|
+
if b_e in self.dropped:
|
|
97
|
+
continue
|
|
98
|
+
error_seen = True
|
|
99
|
+
|
|
100
|
+
self.dropped[b_e] = tm + self.cfg.timeout.retry
|
|
101
|
+
await self.link.e_exc(
|
|
102
|
+
P("run.notify.backend") / name,
|
|
103
|
+
exc,
|
|
104
|
+
msg=f"The notification backend {name} failed.",
|
|
105
|
+
val=kw,
|
|
106
|
+
level=3,
|
|
107
|
+
)
|
|
108
|
+
else:
|
|
109
|
+
if b_e in retry:
|
|
110
|
+
# No lock because this can only happen in one branch
|
|
111
|
+
await self.link.e_ok(P("run.notify.backend") / name)
|
|
112
|
+
|
|
113
|
+
if len(self.dropped) >= len(self._backends):
|
|
114
|
+
# This might conceivably happen in more than one branch
|
|
115
|
+
# if more than one backend fails at a time, but we don't care
|
|
116
|
+
if error_seen:
|
|
117
|
+
await self.link.e_info(
|
|
118
|
+
P("run.notify.backend"), "All notification backends failed.", level=4
|
|
119
|
+
)
|
|
120
|
+
elif any(x not in self.dropped for x in retry):
|
|
121
|
+
await self.link.e_ok(P("run.notify.backend"))
|
|
122
|
+
|
|
123
|
+
async def _run(self, evt) -> NoReturn:
|
|
124
|
+
"""
|
|
125
|
+
A bridge that monitors the MoaT-Link notify subtree.
|
|
126
|
+
"""
|
|
127
|
+
async with as_service(attrdict(debug=False)) as srv:
|
|
128
|
+
try:
|
|
129
|
+
await srv.tg.start(self._keepalive)
|
|
130
|
+
|
|
131
|
+
async with self.link.d_watch(
|
|
132
|
+
self.cfg.path, subtree=True, meta=True, state=None
|
|
133
|
+
) as mon:
|
|
134
|
+
srv.set()
|
|
135
|
+
if evt is not None:
|
|
136
|
+
evt.set()
|
|
137
|
+
async for path, msg, meta in mon:
|
|
138
|
+
t = time.time()
|
|
139
|
+
if meta.timestamp < t - self.cfg.max_age:
|
|
140
|
+
continue
|
|
141
|
+
if isinstance(msg, NotGiven):
|
|
142
|
+
# Treat as empty message, for now
|
|
143
|
+
msg = {"msg": "Deleted"} # noqa:PLW2901
|
|
144
|
+
if isinstance(msg, dict):
|
|
145
|
+
if "title" not in msg:
|
|
146
|
+
msg["title"] = str(path)
|
|
147
|
+
await self.send(topic=path, **msg)
|
|
148
|
+
else:
|
|
149
|
+
await self.send(topic=path, title="?", msg=str(msg))
|
|
150
|
+
|
|
151
|
+
# not reached, loop doesn't terminate
|
|
152
|
+
|
|
153
|
+
except Exception as exc:
|
|
154
|
+
await self.send(
|
|
155
|
+
topic=P("error.notify"), msg=repr(exc), prio="error", title="Gateway failure"
|
|
156
|
+
)
|
|
157
|
+
raise
|
|
158
|
+
|
|
159
|
+
async def _keepalive(self, *, task_status=anyio.TASK_STATUS_IGNORED) -> NoReturn:
|
|
160
|
+
"Monitor the keepalive topic"
|
|
161
|
+
bad = True
|
|
162
|
+
link = self.link
|
|
163
|
+
keep = self.cfg.keepalive
|
|
164
|
+
ok_keep = keep.ok
|
|
165
|
+
|
|
166
|
+
timeout = keep.get("timeout", link.cfg.timeout.ping.timeout)
|
|
167
|
+
|
|
168
|
+
# The main host watcher publishes on the empty path
|
|
169
|
+
async with link.d_watch(P("run.host"), state=None) as mon:
|
|
170
|
+
task_status.started()
|
|
171
|
+
mon = aiter(mon) # noqa:PLW2901
|
|
172
|
+
while True:
|
|
173
|
+
if bad:
|
|
174
|
+
msg = await anext(mon)
|
|
175
|
+
else:
|
|
176
|
+
try:
|
|
177
|
+
with anyio.fail_after(timeout):
|
|
178
|
+
msg = await anext(mon)
|
|
179
|
+
except TimeoutError:
|
|
180
|
+
msg = None
|
|
181
|
+
|
|
182
|
+
if isinstance(msg, dict) and "id" in msg:
|
|
183
|
+
async with link.d_watch(P("run.ping.id") / msg["id"]) as mon2:
|
|
184
|
+
mon2 = aiter(mon2) # noqa:PLW2901
|
|
185
|
+
while True:
|
|
186
|
+
try:
|
|
187
|
+
with anyio.fail_after(timeout):
|
|
188
|
+
msg = await anext(mon2)
|
|
189
|
+
if not msg.get("up", False):
|
|
190
|
+
break
|
|
191
|
+
if bad and "msg" in ok_keep:
|
|
192
|
+
await self.send(topic="error.notify", **ok_keep)
|
|
193
|
+
bad = False
|
|
194
|
+
except TimeoutError:
|
|
195
|
+
break
|
|
196
|
+
|
|
197
|
+
await self.send(topic="error.notify", **keep)
|
|
198
|
+
bad = True
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def get_backend(name: str, link: Link, cfg: dict) -> Notify:
|
|
202
|
+
"""
|
|
203
|
+
Fetch the backend named in the config and initialize it.
|
|
204
|
+
"""
|
|
205
|
+
from importlib import import_module # noqa: PLC0415
|
|
206
|
+
|
|
207
|
+
if "." not in name:
|
|
208
|
+
name = "moat.link.notify." + name
|
|
209
|
+
return import_module(name).Notifier(link, cfg)
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
class Notifier(CtxObj, metaclass=ABCMeta):
|
|
213
|
+
"Base class for notification backends"
|
|
214
|
+
|
|
215
|
+
def __init__(self, link: Link, cfg: attrdict):
|
|
216
|
+
self.cfg = cfg
|
|
217
|
+
self.link = link
|
|
218
|
+
|
|
219
|
+
@asynccontextmanager
|
|
220
|
+
async def _ctx(self) -> AsyncIterator[Self]:
|
|
221
|
+
yield self
|
|
222
|
+
|
|
223
|
+
@abstractmethod
|
|
224
|
+
async def send(
|
|
225
|
+
self, topic: str | Path, title: str | None = None, msg: str | None = None, **kw
|
|
226
|
+
):
|
|
227
|
+
"""Send this notification to that topic."""
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# Notifications are messages below :R.notify.TOPIC topic.
|
|
2
|
+
keepalive:
|
|
3
|
+
# timeout: 30 # default: ping *1.5
|
|
4
|
+
#
|
|
5
|
+
# topic: my_secret_topic
|
|
6
|
+
title: Server Link down?
|
|
7
|
+
msg: |
|
|
8
|
+
The MoaT system at 'some_host' is silent. (MQTT-Keepalive)
|
|
9
|
+
prio: fatal
|
|
10
|
+
ok:
|
|
11
|
+
title: Server Link OK
|
|
12
|
+
msg: |
|
|
13
|
+
The MoaT system at 'some_host' is back up. (MQTT-Keepalive)
|
|
14
|
+
prio: info
|
|
15
|
+
|
|
16
|
+
timeout:
|
|
17
|
+
retry: 300
|
|
18
|
+
|
|
19
|
+
path: !P notify
|
|
20
|
+
# ignore messages older than … seconds
|
|
21
|
+
max_age: 30
|
|
22
|
+
|
|
23
|
+
# Backends
|
|
24
|
+
backend:
|
|
25
|
+
- ntfy
|
|
26
|
+
ntfy:
|
|
27
|
+
# See https://docs.nfty.sh/
|
|
28
|
+
|
|
29
|
+
url: https://ntfy.sh.example/
|
|
30
|
+
# token: tk_whatever
|
|
31
|
+
# messages are posted to notify.TOPIC.WHATEVER if "topic" is not set
|
|
32
|
+
# topic: my_secret_topic
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# command line interface
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import asyncclick as click
|
|
5
|
+
|
|
6
|
+
from moat.link.announce import announcing
|
|
7
|
+
from moat.link.client import Link
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@click.group(short_help="Manage notifications.") # pylint: disable=undefined-variable
|
|
11
|
+
@click.pass_context
|
|
12
|
+
async def cli(ctx):
|
|
13
|
+
"""
|
|
14
|
+
Handle notifications.
|
|
15
|
+
"""
|
|
16
|
+
obj = ctx.obj
|
|
17
|
+
cfg = obj.cfg["link"]
|
|
18
|
+
obj.conn = await ctx.with_async_resource(Link(cfg))
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@cli.command()
|
|
22
|
+
@click.option("-b", "--backend", type=str, multiple=True, help="Restrict to this backend")
|
|
23
|
+
@click.pass_obj
|
|
24
|
+
async def run(obj, backend):
|
|
25
|
+
"""
|
|
26
|
+
Forward notification messages.
|
|
27
|
+
|
|
28
|
+
This command monitors the 'notify' subpath and forwards messages to
|
|
29
|
+
your ntfy.sh instance.
|
|
30
|
+
"""
|
|
31
|
+
from moat.link.notify import Notify # noqa: PLC0415
|
|
32
|
+
|
|
33
|
+
cfg = obj.cfg.link.notify
|
|
34
|
+
if backend:
|
|
35
|
+
cfg.backends = backend
|
|
36
|
+
async with announcing(obj.conn) as ann:
|
|
37
|
+
await Notify(cfg).run(obj.conn, evt=ann)
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"""
|
|
2
|
+
This module forwards notifications to NTFY.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
from contextlib import asynccontextmanager
|
|
8
|
+
|
|
9
|
+
import httpx
|
|
10
|
+
|
|
11
|
+
from moat.util import P, Path
|
|
12
|
+
|
|
13
|
+
from . import Notifier as BaseNotifier
|
|
14
|
+
|
|
15
|
+
from typing import TYPE_CHECKING
|
|
16
|
+
|
|
17
|
+
if TYPE_CHECKING:
|
|
18
|
+
from collections.abc import Sequence
|
|
19
|
+
|
|
20
|
+
__all__ = ["Notifier"]
|
|
21
|
+
|
|
22
|
+
prio_map = {
|
|
23
|
+
"debug": 1,
|
|
24
|
+
"info": 2,
|
|
25
|
+
"warning": 3,
|
|
26
|
+
"error": 4,
|
|
27
|
+
"fatal": 5,
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class Notifier(BaseNotifier): # noqa:D101
|
|
32
|
+
@asynccontextmanager
|
|
33
|
+
async def _ctx(self):
|
|
34
|
+
async with (
|
|
35
|
+
super()._ctx(),
|
|
36
|
+
httpx.AsyncClient() as self.http,
|
|
37
|
+
):
|
|
38
|
+
yield self
|
|
39
|
+
|
|
40
|
+
async def send(
|
|
41
|
+
self,
|
|
42
|
+
topic: str | Path,
|
|
43
|
+
title: str | None = None,
|
|
44
|
+
msg: str | None = None,
|
|
45
|
+
prio: str | None = None,
|
|
46
|
+
tags: Sequence[str] = (),
|
|
47
|
+
**kw, # noqa: ARG002
|
|
48
|
+
):
|
|
49
|
+
"Forward a message to NTFY"
|
|
50
|
+
|
|
51
|
+
if "topic" in self.cfg:
|
|
52
|
+
top = self.cfg.topic
|
|
53
|
+
else:
|
|
54
|
+
if isinstance(topic, str):
|
|
55
|
+
topic = P(topic)
|
|
56
|
+
try:
|
|
57
|
+
top = topic[0]
|
|
58
|
+
except IndexError:
|
|
59
|
+
top = "TOP"
|
|
60
|
+
else:
|
|
61
|
+
topic = Path.build(topic[1:])
|
|
62
|
+
|
|
63
|
+
url = self.cfg.url + "/" + top
|
|
64
|
+
tags = list(tags)
|
|
65
|
+
tags.append(str(topic))
|
|
66
|
+
|
|
67
|
+
hdr = {}
|
|
68
|
+
|
|
69
|
+
if title:
|
|
70
|
+
hdr["title"] = title
|
|
71
|
+
if prio is not None:
|
|
72
|
+
try:
|
|
73
|
+
prio = prio_map[prio]
|
|
74
|
+
hdr["prio"] = str(prio)
|
|
75
|
+
if prio > 3:
|
|
76
|
+
tags.append("warning")
|
|
77
|
+
except KeyError:
|
|
78
|
+
hdr["prio"] = "high"
|
|
79
|
+
msg += f" (prio:{prio})"
|
|
80
|
+
hdr["tags"] = ",".join(str(x) for x in tags)
|
|
81
|
+
|
|
82
|
+
if "token" in self.cfg:
|
|
83
|
+
hdr["authorization"] = f"Bearer {self.cfg['token']}"
|
|
84
|
+
|
|
85
|
+
resp = await self.http.post(url, data=msg, headers=hdr)
|
|
86
|
+
resp.raise_for_status()
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: moat-link-notify
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: Notification gateway for MoaT-Link
|
|
5
|
+
Author-email: Matthias Urlichs <matthias@urlichs.de>
|
|
6
|
+
Project-URL: homepage, https://m-o-a-t.org
|
|
7
|
+
Project-URL: repository, https://github.com/M-o-a-T/moat
|
|
8
|
+
Keywords: MoaT
|
|
9
|
+
Classifier: Intended Audience :: Developers
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Framework :: AsyncIO
|
|
12
|
+
Classifier: Framework :: Trio
|
|
13
|
+
Classifier: Framework :: AnyIO
|
|
14
|
+
Classifier: Development Status :: 4 - Beta
|
|
15
|
+
Requires-Python: >=3.8
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
License-File: LICENSE.txt
|
|
18
|
+
Requires-Dist: anyio~=4.2
|
|
19
|
+
Requires-Dist: asyncclick
|
|
20
|
+
Requires-Dist: attrs
|
|
21
|
+
Requires-Dist: httpx
|
|
22
|
+
Requires-Dist: moat-util~=0.62.2
|
|
23
|
+
Requires-Dist: moat-link~=0.2.46
|
|
24
|
+
Dynamic: license-file
|
|
25
|
+
|
|
26
|
+
# moat-link-notify
|
|
27
|
+
|
|
28
|
+
Notification gateway for MoaT-Link.
|
|
29
|
+
|
|
30
|
+
This package provides notification services for MoaT-Link, allowing you to send
|
|
31
|
+
notifications to external services like ntfy.sh.
|
|
32
|
+
|
|
33
|
+
## Installation
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
pip install moat-link-notify
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Usage
|
|
40
|
+
|
|
41
|
+
The notification gateway can be configured through MoaT-Link's configuration system.
|
|
42
|
+
|
|
43
|
+
## Dependencies
|
|
44
|
+
|
|
45
|
+
- moat-link: Core MoaT-Link functionality
|
|
46
|
+
- httpx: HTTP client for notification delivery
|
|
47
|
+
|
|
48
|
+
## License
|
|
49
|
+
|
|
50
|
+
This project is licensed under the same terms as MoaT.
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
LICENSE.txt
|
|
2
|
+
Makefile
|
|
3
|
+
README.md
|
|
4
|
+
pyproject.toml
|
|
5
|
+
debian/.gitignore
|
|
6
|
+
debian/changelog
|
|
7
|
+
debian/control
|
|
8
|
+
debian/moat-link-notify.service
|
|
9
|
+
debian/rules
|
|
10
|
+
src/moat/link/notify/__init__.py
|
|
11
|
+
src/moat/link/notify/_cfg.yaml
|
|
12
|
+
src/moat/link/notify/_main.py
|
|
13
|
+
src/moat/link/notify/ntfy.py
|
|
14
|
+
src/moat_link_notify.egg-info/PKG-INFO
|
|
15
|
+
src/moat_link_notify.egg-info/SOURCES.txt
|
|
16
|
+
src/moat_link_notify.egg-info/dependency_links.txt
|
|
17
|
+
src/moat_link_notify.egg-info/requires.txt
|
|
18
|
+
src/moat_link_notify.egg-info/top_level.txt
|
|
19
|
+
systemd/moat-link-notify.service
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
moat
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
[Unit]
|
|
2
|
+
Description=MoaT-Link Notification Forwarder
|
|
3
|
+
|
|
4
|
+
ConditionFileNotEmpty=/etc/moat/moat.cfg
|
|
5
|
+
|
|
6
|
+
[Install]
|
|
7
|
+
WantedBy=multi-user.target
|
|
8
|
+
|
|
9
|
+
[Service]
|
|
10
|
+
Type=notify
|
|
11
|
+
ExecStart=/usr/bin/moat link notify run
|
|
12
|
+
|
|
13
|
+
RuntimeDirectory=moat/link
|
|
14
|
+
|
|
15
|
+
User=MoaT-Link
|
|
16
|
+
Group=MoaT
|
|
17
|
+
|
|
18
|
+
EnvironmentFile=/usr/lib/moat/link/env
|
|
19
|
+
EnvironmentFile=-/etc/moat/link.env
|
|
20
|
+
|
|
21
|
+
TimeoutSec=300
|
|
22
|
+
WatchdogSec=10
|
|
23
|
+
Restart=always
|