omdnotificationforwarder 4.1__tar.gz → 4.2__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.
Files changed (63) hide show
  1. omdnotificationforwarder-4.1/README.md → omdnotificationforwarder-4.2/PKG-INFO +52 -1
  2. omdnotificationforwarder-4.1/PKG-INFO → omdnotificationforwarder-4.2/README.md +30 -21
  3. {omdnotificationforwarder-4.1 → omdnotificationforwarder-4.2}/bin/notificationforwarder +1 -1
  4. {omdnotificationforwarder-4.1 → omdnotificationforwarder-4.2}/pyproject.toml +4 -1
  5. omdnotificationforwarder-4.2/src/notificationforwarder/nostr/__init__.py +1 -0
  6. omdnotificationforwarder-4.2/src/notificationforwarder/nostr/formatter.py +66 -0
  7. omdnotificationforwarder-4.2/src/notificationforwarder/nostr/forwarder.py +141 -0
  8. omdnotificationforwarder-4.2/src/notificationforwarder/oauth2webhook/__init__.py +0 -0
  9. omdnotificationforwarder-4.2/src/notificationforwarder/oauth2webhook/forwarder.py +98 -0
  10. omdnotificationforwarder-4.2/tests/test_builtin_plugins_focus.py +279 -0
  11. omdnotificationforwarder-4.2/tests/test_nostr.py +261 -0
  12. omdnotificationforwarder-4.2/tests/test_oauth2webhook.py +206 -0
  13. {omdnotificationforwarder-4.1 → omdnotificationforwarder-4.2}/tests/test_reporter.py +8 -3
  14. {omdnotificationforwarder-4.1 → omdnotificationforwarder-4.2}/.gitignore +0 -0
  15. {omdnotificationforwarder-4.1 → omdnotificationforwarder-4.2}/src/notificationforwarder/baseclass.py +0 -0
  16. {omdnotificationforwarder-4.1 → omdnotificationforwarder-4.2}/src/notificationforwarder/component_loader.py +0 -0
  17. {omdnotificationforwarder-4.1 → omdnotificationforwarder-4.2}/src/notificationforwarder/email/formatter.py +0 -0
  18. {omdnotificationforwarder-4.1 → omdnotificationforwarder-4.2}/src/notificationforwarder/email/forwarder.py +0 -0
  19. {omdnotificationforwarder-4.1 → omdnotificationforwarder-4.2}/src/notificationforwarder/example/formatter.py +0 -0
  20. {omdnotificationforwarder-4.1 → omdnotificationforwarder-4.2}/src/notificationforwarder/example/forwarder.py +0 -0
  21. {omdnotificationforwarder-4.1 → omdnotificationforwarder-4.2}/src/notificationforwarder/json/__init__.py +0 -0
  22. {omdnotificationforwarder-4.1 → omdnotificationforwarder-4.2}/src/notificationforwarder/json/logger.py +0 -0
  23. {omdnotificationforwarder-4.1 → omdnotificationforwarder-4.2}/src/notificationforwarder/naemonlog/reporter.py +0 -0
  24. {omdnotificationforwarder-4.1 → omdnotificationforwarder-4.2}/src/notificationforwarder/rabbitmq/formatter.py +0 -0
  25. {omdnotificationforwarder-4.1 → omdnotificationforwarder-4.2}/src/notificationforwarder/rabbitmq/forwarder.py +0 -0
  26. {omdnotificationforwarder-4.1 → omdnotificationforwarder-4.2}/src/notificationforwarder/runtime_config.py +0 -0
  27. {omdnotificationforwarder-4.1 → omdnotificationforwarder-4.2}/src/notificationforwarder/runtime_flow.py +0 -0
  28. {omdnotificationforwarder-4.1 → omdnotificationforwarder-4.2}/src/notificationforwarder/spool.py +0 -0
  29. {omdnotificationforwarder-4.1 → omdnotificationforwarder-4.2}/src/notificationforwarder/syslog/formatter.py +0 -0
  30. {omdnotificationforwarder-4.1 → omdnotificationforwarder-4.2}/src/notificationforwarder/syslog/forwarder.py +0 -0
  31. {omdnotificationforwarder-4.1 → omdnotificationforwarder-4.2}/src/notificationforwarder/telegram/forwarder.py +0 -0
  32. {omdnotificationforwarder-4.1 → omdnotificationforwarder-4.2}/src/notificationforwarder/text/__init__.py +0 -0
  33. {omdnotificationforwarder-4.1 → omdnotificationforwarder-4.2}/src/notificationforwarder/text/logger.py +0 -0
  34. {omdnotificationforwarder-4.1 → omdnotificationforwarder-4.2}/src/notificationforwarder/webhook/forwarder.py +0 -0
  35. {omdnotificationforwarder-4.1 → omdnotificationforwarder-4.2}/tests/pythonpath/lib/python/notificationforwarder/split1/forwarder.py +0 -0
  36. {omdnotificationforwarder-4.1 → omdnotificationforwarder-4.2}/tests/pythonpath/lib/python/notificationforwarder/split2/formatter.py +0 -0
  37. {omdnotificationforwarder-4.1 → omdnotificationforwarder-4.2}/tests/pythonpath/lib/python/notificationforwarder/split2/forwarder.py +0 -0
  38. {omdnotificationforwarder-4.1 → omdnotificationforwarder-4.2}/tests/pythonpath/lib/python/notificationforwarder/split3/formatter.py +0 -0
  39. {omdnotificationforwarder-4.1 → omdnotificationforwarder-4.2}/tests/pythonpath/lib/python/notificationforwarder/split3/forwarder.py +0 -0
  40. {omdnotificationforwarder-4.1 → omdnotificationforwarder-4.2}/tests/pythonpath/local/lib/python/notificationforwarder/alertmanager_servicenow/formatter.py +0 -0
  41. {omdnotificationforwarder-4.1 → omdnotificationforwarder-4.2}/tests/pythonpath/local/lib/python/notificationforwarder/bayern/formatter.py +0 -0
  42. {omdnotificationforwarder-4.1 → omdnotificationforwarder-4.2}/tests/pythonpath/local/lib/python/notificationforwarder/datadup/formatter.py +0 -0
  43. {omdnotificationforwarder-4.1 → omdnotificationforwarder-4.2}/tests/pythonpath/local/lib/python/notificationforwarder/datapost/formatter.py +0 -0
  44. {omdnotificationforwarder-4.1 → omdnotificationforwarder-4.2}/tests/pythonpath/local/lib/python/notificationforwarder/discard/formatter.py +0 -0
  45. {omdnotificationforwarder-4.1 → omdnotificationforwarder-4.2}/tests/pythonpath/local/lib/python/notificationforwarder/split1/formatter.py +0 -0
  46. {omdnotificationforwarder-4.1 → omdnotificationforwarder-4.2}/tests/pythonpath/local/lib/python/notificationforwarder/split2/forwarder.py +0 -0
  47. {omdnotificationforwarder-4.1 → omdnotificationforwarder-4.2}/tests/pythonpath/local/lib/python/notificationforwarder/split3/formatter.py +0 -0
  48. {omdnotificationforwarder-4.1 → omdnotificationforwarder-4.2}/tests/pythonpath/local/lib/python/notificationforwarder/split3/forwarder.py +0 -0
  49. {omdnotificationforwarder-4.1 → omdnotificationforwarder-4.2}/tests/pythonpath/local/lib/python/notificationforwarder/split4/formatter.py +0 -0
  50. {omdnotificationforwarder-4.1 → omdnotificationforwarder-4.2}/tests/pythonpath/local/lib/python/notificationforwarder/split4/forwarder.py +0 -0
  51. {omdnotificationforwarder-4.1 → omdnotificationforwarder-4.2}/tests/pythonpath/local/lib/python/notificationforwarder/ticketsystem/forwarder.py +0 -0
  52. {omdnotificationforwarder-4.1 → omdnotificationforwarder-4.2}/tests/pythonpath/local/lib/python/notificationforwarder/ticketsystem/reporter.py +0 -0
  53. {omdnotificationforwarder-4.1 → omdnotificationforwarder-4.2}/tests/pythonpath/local/lib/python/notificationforwarder/vong/formatter.py +0 -0
  54. {omdnotificationforwarder-4.1 → omdnotificationforwarder-4.2}/tests/test_alertmanager.py +0 -0
  55. {omdnotificationforwarder-4.1 → omdnotificationforwarder-4.2}/tests/test_classes.py +0 -0
  56. {omdnotificationforwarder-4.1 → omdnotificationforwarder-4.2}/tests/test_delivery_resilience.py +0 -0
  57. {omdnotificationforwarder-4.1 → omdnotificationforwarder-4.2}/tests/test_discard.py +0 -0
  58. {omdnotificationforwarder-4.1 → omdnotificationforwarder-4.2}/tests/test_formatter.py +0 -0
  59. {omdnotificationforwarder-4.1 → omdnotificationforwarder-4.2}/tests/test_logger.py +0 -0
  60. {omdnotificationforwarder-4.1 → omdnotificationforwarder-4.2}/tests/test_package.py +0 -0
  61. {omdnotificationforwarder-4.1 → omdnotificationforwarder-4.2}/tests/test_paths.py +0 -0
  62. {omdnotificationforwarder-4.1 → omdnotificationforwarder-4.2}/tests/test_runtime_foundation.py +0 -0
  63. {omdnotificationforwarder-4.1 → omdnotificationforwarder-4.2}/tests/test_webhook.py +0 -0
@@ -1,3 +1,25 @@
1
+ Metadata-Version: 2.4
2
+ Name: omdnotificationforwarder
3
+ Version: 4.2
4
+ Summary: A framework for notification scripts for OMD
5
+ Project-URL: Homepage, https://github.com/lausser/noteventificationforhandlerwarder
6
+ Project-URL: Bug Tracker, https://github.com/lausser/noteventificationforhandlerwarder/issues
7
+ Author-email: Gerhard Lausser <lausser@yahoo.com>
8
+ Classifier: License :: OSI Approved :: MIT License
9
+ Classifier: Operating System :: OS Independent
10
+ Classifier: Programming Language :: Python :: 3
11
+ Requires-Python: >=3.6.1
12
+ Requires-Dist: coshsh
13
+ Requires-Dist: jinja2
14
+ Provides-Extra: nostr
15
+ Requires-Dist: pynostr; extra == 'nostr'
16
+ Provides-Extra: test
17
+ Requires-Dist: flake8; extra == 'test'
18
+ Requires-Dist: pytest; extra == 'test'
19
+ Requires-Dist: pytest-cov; extra == 'test'
20
+ Requires-Dist: requests; extra == 'test'
21
+ Description-Content-Type: text/markdown
22
+
1
23
  # notificationforwarder
2
24
 
3
25
  In this framework, two aspects are in the focus. How to transport a notification to the recipient system and in which format.
@@ -18,6 +40,8 @@ Let me list some of the formatter/forwarder combinations which are usually found
18
40
  |json |Splunk HEC |
19
41
  |json |RabbitMQ |
20
42
 
43
+ |markdown-like Nostr note|Nostr relays |
44
+
21
45
  Of course json is not json, the attributes and values are different depending on the recipient.
22
46
 
23
47
 
@@ -240,6 +264,34 @@ There is also a SyslogFormatter, which creates the log line as:
240
264
 
241
265
  If you want a different format, then copy *lib/python/notificationforwarder/syslog/formatter.py* to *local/lib/python/notificationforwarder/syslog/formatter.py* and modify it like you want. Or, with *--formatter*, you can use whatever formatter is suitable, as long as it's payload attribute consists of a line of text.
242
266
 
267
+ ### Nostr
268
+
269
+ The Nostr plugin pair publishes an encrypted direct message with labeled lines and default monitoring tags so the alert appears in chat-oriented clients.
270
+
271
+ Install the optional Nostr support with `pip install notificationforwarder[nostr]`.
272
+
273
+ |parameter|description|default|
274
+ |---------|-----------|-------|
275
+ |relays|comma-separated relay URLs|`wss://relay.damus.io`|
276
+ |nsec|bech32 private key for signing|required|
277
+ |tags|extra Nostr tags to include|`monitoring` plus host/service/state tags|
278
+
279
+ Example:
280
+
281
+ ```bash
282
+ $USER1$/notificationforwarder \
283
+ --forwarder nostr \
284
+ --forwarderopt relays='wss://relay.damus.io,wss://nostr-pub.wellorder.net' \
285
+ --forwarderopt nsec='$NSEC$' \
286
+ --formatter nostr \
287
+ --eventopt HOSTNAME='$HOSTNAME$' \
288
+ --eventopt SERVICEDESC='$SERVICEDESC$' \
289
+ --eventopt SERVICESTATE='$SERVICESTATE$' \
290
+ --eventopt SERVICEOUTPUT='$SERVICEOUTPUT$'
291
+ ```
292
+
293
+ The recipient `p` tag can be provided through the `tags` forwarder option or custom formatter output. The message content is encrypted before relay publication, so it shows up as a DM in clients that support NIP-04 direct messages.
294
+
243
295
  ## Loggers
244
296
 
245
297
  The framework uses a modular logging architecture similar to formatters, forwarders, and reporters. By default, notificationforwarder uses **text format logging** - you don't need to do anything, logging works exactly as it did before. The traditional text format is backward compatible with all existing installations.
@@ -411,4 +463,3 @@ define command{
411
463
  >> $USER4$/var/log/notificationforwarder_errors.log 2>&1
412
464
  }
413
465
  ```
414
-
@@ -1,23 +1,3 @@
1
- Metadata-Version: 2.4
2
- Name: omdnotificationforwarder
3
- Version: 4.1
4
- Summary: A framework for notification scripts for OMD
5
- Project-URL: Homepage, https://github.com/lausser/noteventificationforhandlerwarder
6
- Project-URL: Bug Tracker, https://github.com/lausser/noteventificationforhandlerwarder/issues
7
- Author-email: Gerhard Lausser <lausser@yahoo.com>
8
- Classifier: License :: OSI Approved :: MIT License
9
- Classifier: Operating System :: OS Independent
10
- Classifier: Programming Language :: Python :: 3
11
- Requires-Python: >=3.6.1
12
- Requires-Dist: coshsh
13
- Requires-Dist: jinja2
14
- Provides-Extra: test
15
- Requires-Dist: flake8; extra == 'test'
16
- Requires-Dist: pytest; extra == 'test'
17
- Requires-Dist: pytest-cov; extra == 'test'
18
- Requires-Dist: requests; extra == 'test'
19
- Description-Content-Type: text/markdown
20
-
21
1
  # notificationforwarder
22
2
 
23
3
  In this framework, two aspects are in the focus. How to transport a notification to the recipient system and in which format.
@@ -38,6 +18,8 @@ Let me list some of the formatter/forwarder combinations which are usually found
38
18
  |json |Splunk HEC |
39
19
  |json |RabbitMQ |
40
20
 
21
+ |markdown-like Nostr note|Nostr relays |
22
+
41
23
  Of course json is not json, the attributes and values are different depending on the recipient.
42
24
 
43
25
 
@@ -260,6 +242,34 @@ There is also a SyslogFormatter, which creates the log line as:
260
242
 
261
243
  If you want a different format, then copy *lib/python/notificationforwarder/syslog/formatter.py* to *local/lib/python/notificationforwarder/syslog/formatter.py* and modify it like you want. Or, with *--formatter*, you can use whatever formatter is suitable, as long as it's payload attribute consists of a line of text.
262
244
 
245
+ ### Nostr
246
+
247
+ The Nostr plugin pair publishes an encrypted direct message with labeled lines and default monitoring tags so the alert appears in chat-oriented clients.
248
+
249
+ Install the optional Nostr support with `pip install notificationforwarder[nostr]`.
250
+
251
+ |parameter|description|default|
252
+ |---------|-----------|-------|
253
+ |relays|comma-separated relay URLs|`wss://relay.damus.io`|
254
+ |nsec|bech32 private key for signing|required|
255
+ |tags|extra Nostr tags to include|`monitoring` plus host/service/state tags|
256
+
257
+ Example:
258
+
259
+ ```bash
260
+ $USER1$/notificationforwarder \
261
+ --forwarder nostr \
262
+ --forwarderopt relays='wss://relay.damus.io,wss://nostr-pub.wellorder.net' \
263
+ --forwarderopt nsec='$NSEC$' \
264
+ --formatter nostr \
265
+ --eventopt HOSTNAME='$HOSTNAME$' \
266
+ --eventopt SERVICEDESC='$SERVICEDESC$' \
267
+ --eventopt SERVICESTATE='$SERVICESTATE$' \
268
+ --eventopt SERVICEOUTPUT='$SERVICEOUTPUT$'
269
+ ```
270
+
271
+ The recipient `p` tag can be provided through the `tags` forwarder option or custom formatter output. The message content is encrypted before relay publication, so it shows up as a DM in clients that support NIP-04 direct messages.
272
+
263
273
  ## Loggers
264
274
 
265
275
  The framework uses a modular logging architecture similar to formatters, forwarders, and reporters. By default, notificationforwarder uses **text format logging** - you don't need to do anything, logging works exactly as it did before. The traditional text format is backward compatible with all existing installations.
@@ -431,4 +441,3 @@ define command{
431
441
  >> $USER4$/var/log/notificationforwarder_errors.log 2>&1
432
442
  }
433
443
  ```
434
-
@@ -78,7 +78,7 @@ Example for an HTTP-based reporter:
78
78
  help='Logger type: text (default) or json, or custom logger module',
79
79
  default='text')
80
80
  parser.add_argument('--version', action='version',
81
- version=f'%(prog)s 4.1')
81
+ version=f'%(prog)s 4.2')
82
82
 
83
83
  args = parser.parse_args()
84
84
  if not hasattr(args, 'formatter'):
@@ -21,7 +21,7 @@ packages = ["src/notificationforwarder"]
21
21
 
22
22
  [project]
23
23
  name = "omdnotificationforwarder"
24
- version = "4.1"
24
+ version = "4.2"
25
25
  authors = [
26
26
  { name="Gerhard Lausser", email="lausser@yahoo.com" },
27
27
  ]
@@ -39,6 +39,9 @@ dependencies = [
39
39
  ]
40
40
 
41
41
  [project.optional-dependencies]
42
+ nostr = [
43
+ "pynostr",
44
+ ]
42
45
  test = [
43
46
  "requests",
44
47
  "flake8",
@@ -0,0 +1 @@
1
+ """Nostr formatter and forwarder for notificationforwarder."""
@@ -0,0 +1,66 @@
1
+ """Generate a Nostr keypair and derive bech32 values.
2
+
3
+ ```python
4
+ from pynostr.key import PrivateKey
5
+
6
+ private_key = PrivateKey()
7
+ print(private_key.bech32()) # nsec1...
8
+ print(private_key.public_key.bech32()) # npub1...
9
+ ```
10
+ """
11
+
12
+ import json
13
+
14
+ from notificationforwarder.baseclass import NotificationFormatter
15
+
16
+
17
+ class NostrFormatter(NotificationFormatter):
18
+ def _pick(self, event, *names, default=""):
19
+ for name in names:
20
+ value = event.eventopts.get(name)
21
+ if value not in (None, ""):
22
+ return value
23
+ return default
24
+
25
+ def _build_tags(self, host, service, state, extra_tags=None):
26
+ tags = [["t", "monitoring"]]
27
+ if host:
28
+ tags.append(["host", host])
29
+ if service:
30
+ tags.append(["service", service])
31
+ if state:
32
+ tags.append(["state", state])
33
+ if isinstance(extra_tags, str):
34
+ try:
35
+ extra_tags = json.loads(extra_tags)
36
+ except Exception:
37
+ extra_tags = []
38
+ for tag in extra_tags or []:
39
+ if isinstance(tag, (list, tuple)) and tag:
40
+ tags.append(list(tag))
41
+ return tags
42
+
43
+ def format_event(self, event):
44
+ host = self._pick(event, "HOSTNAME", "HOST", "HOSTNAME_SHORT", "HOSTNAME_FULL")
45
+ service = self._pick(event, "SERVICEDESC", "SERVICE", default="")
46
+ state = self._pick(event, "SERVICESTATE", "HOSTSTATE", "STATE", default="")
47
+ output = self._pick(event, "SERVICEOUTPUT", "HOSTOUTPUT", "OUTPUT", default="")
48
+
49
+ lines = [
50
+ f"Host: {host}",
51
+ f"Service: {service or '-'}",
52
+ f"State: {state or '-'}",
53
+ f"Output: {output or '-'}",
54
+ ]
55
+ content = "\n".join(lines)
56
+ event.payload = {
57
+ "kind": 4,
58
+ "content": content,
59
+ "tags": self._build_tags(
60
+ host,
61
+ service,
62
+ state,
63
+ event.forwarderopts.get("tags"),
64
+ ),
65
+ }
66
+ event.summary = f"{host or 'unknown'}/{service or '-'}: {state or '-'}"
@@ -0,0 +1,141 @@
1
+ import json
2
+
3
+ from pynostr.encrypted_dm import EncryptedDirectMessage
4
+ from pynostr.event import Event
5
+ from pynostr.key import PrivateKey, PublicKey
6
+ import pynostr.relay as relay_mod
7
+ from pynostr.relay_manager import RelayManager
8
+
9
+ from notificationforwarder.baseclass import NotificationForwarder, timeout
10
+
11
+
12
+ class NostrForwarder(NotificationForwarder):
13
+ def __init__(self, opts):
14
+ super(self.__class__, self).__init__(opts)
15
+ setattr(self, "relays", getattr(self, "relays", "wss://relay.damus.io"))
16
+ setattr(self, "relay_urls", getattr(self, "relay_urls", self.relays))
17
+ setattr(self, "nsec", getattr(self, "nsec", None))
18
+ setattr(self, "timeout_seconds", int(getattr(self, "timeout_seconds", 6)))
19
+ setattr(self, "kind", int(getattr(self, "kind", 1)))
20
+
21
+ def _relay_list(self):
22
+ relays = self.relay_urls if self.relay_urls is not None else self.relays
23
+ if isinstance(relays, str):
24
+ return [relay.strip() for relay in relays.split(",") if relay.strip()]
25
+ if isinstance(relays, (list, tuple)):
26
+ return [str(relay).strip() for relay in relays if str(relay).strip()]
27
+ return []
28
+
29
+ def _private_key(self):
30
+ if not self.nsec:
31
+ raise ValueError("nsec is required for Nostr publishing")
32
+ if hasattr(PrivateKey, "from_nsec"):
33
+ return PrivateKey.from_nsec(self.nsec)
34
+ return PrivateKey(self.nsec)
35
+
36
+ def _build_event(self, formatted_event):
37
+ payload = formatted_event.payload or {}
38
+ if not isinstance(payload, dict):
39
+ raise ValueError("Nostr formatter must produce a dictionary payload")
40
+ tags = self._normalize_tags(payload.get("tags", []))
41
+ extra_tags = []
42
+ event_forwarder_tags = formatted_event.forwarderopts.get("tags", [])
43
+ if isinstance(event_forwarder_tags, str):
44
+ try:
45
+ event_forwarder_tags = json.loads(event_forwarder_tags)
46
+ except Exception:
47
+ event_forwarder_tags = []
48
+ if isinstance(event_forwarder_tags, (list, tuple)):
49
+ extra_tags.extend(event_forwarder_tags)
50
+ forwarder_tags = getattr(self, "tags", [])
51
+ if isinstance(forwarder_tags, str):
52
+ try:
53
+ forwarder_tags = json.loads(forwarder_tags)
54
+ except Exception:
55
+ forwarder_tags = []
56
+ if isinstance(forwarder_tags, (list, tuple)):
57
+ extra_tags.extend(forwarder_tags)
58
+ if extra_tags:
59
+ tags.extend(self._normalize_tags(extra_tags))
60
+ event = Event(payload.get("content", ""), tags=tags)
61
+ if hasattr(event, "kind"):
62
+ event.kind = payload.get("kind", self.kind)
63
+ return event
64
+
65
+ def _recipient_pubkey(self, event):
66
+ for tag in getattr(event, "tags", []) or []:
67
+ if isinstance(tag, (list, tuple)) and len(tag) > 1 and tag[0] == "p":
68
+ return tag[1]
69
+ raise ValueError("Nostr DM requires a recipient p tag")
70
+
71
+ def _normalize_pubkey(self, value):
72
+ # Accept recipient public keys as either npub1... or raw hex.
73
+ if not isinstance(value, str):
74
+ return value
75
+ value = value.strip()
76
+ if value.startswith("npub1"):
77
+ try:
78
+ return PublicKey.from_npub(value).hex()
79
+ except Exception:
80
+ return value
81
+ return value
82
+
83
+ def _normalize_tags(self, tags):
84
+ normalized = []
85
+ for tag in tags or []:
86
+ if isinstance(tag, (list, tuple)) and tag:
87
+ item = list(tag)
88
+ if item[0] == "p" and len(item) > 1:
89
+ item[1] = self._normalize_pubkey(item[1])
90
+ normalized.append(item)
91
+ else:
92
+ normalized.append(["t", str(tag)])
93
+ return normalized
94
+
95
+ def _close_manager(self, relay_manager):
96
+ close_methods = [
97
+ "close_all_relay_connections",
98
+ "close_connections",
99
+ ]
100
+ for method_name in close_methods:
101
+ method = getattr(relay_manager, method_name, None)
102
+ if callable(method):
103
+ method()
104
+ break
105
+
106
+ @timeout(30)
107
+ def submit(self, event):
108
+ relay_urls = self._relay_list()
109
+ if not relay_urls:
110
+ logger.critical("Nostr publish failed: no relays configured")
111
+ return False
112
+ try:
113
+ private_key = self._private_key()
114
+ nostr_event = self._build_event(event)
115
+ recipient_pubkey = self._normalize_pubkey(self._recipient_pubkey(nostr_event))
116
+ dm = EncryptedDirectMessage()
117
+ dm.encrypt(
118
+ private_key.hex(),
119
+ cleartext_content=nostr_event.content,
120
+ recipient_pubkey=recipient_pubkey,
121
+ )
122
+ nostr_event = dm.to_event()
123
+ nostr_event.sign(private_key.hex())
124
+
125
+ orig_websocket_connect = relay_mod.websocket_connect
126
+ relay_mod.websocket_connect = lambda *a, **k: orig_websocket_connect(
127
+ *a, **{**k, "ping_timeout": min(k.get("ping_timeout", 0), k.get("ping_interval", 0))}
128
+ )
129
+
130
+ relay_manager = RelayManager(timeout=self.timeout_seconds)
131
+ for relay_url in relay_urls:
132
+ relay_manager.add_relay(relay_url)
133
+ relay_manager.publish_event(nostr_event)
134
+ relay_manager.run_sync()
135
+ self._close_manager(relay_manager)
136
+ logger.info("published Nostr DM: {}".format(event.summary))
137
+ self.no_more_logging()
138
+ return True
139
+ except Exception as exc:
140
+ logger.critical("Nostr publish failed: {}".format(str(exc)))
141
+ return False
@@ -0,0 +1,98 @@
1
+ import json
2
+ import os
3
+ import time
4
+ import fcntl
5
+ import requests
6
+ requests.packages.urllib3.disable_warnings(requests.packages.urllib3.exceptions.InsecureRequestWarning)
7
+
8
+ import notificationforwarder.webhook.forwarder as _webhook_module
9
+ from notificationforwarder.webhook.forwarder import WebhookForwarder
10
+ from notificationforwarder.baseclass import NotificationForwarder, timeout
11
+
12
+ logger = None
13
+
14
+
15
+ class Oauth2WebhookForwarder(WebhookForwarder):
16
+ def __init__(self, opts):
17
+ # Call NotificationForwarder directly to avoid infinite recursion from
18
+ # WebhookForwarder's super(self.__class__, self) pattern when subclassed.
19
+ NotificationForwarder.__init__(self, opts)
20
+ # Webhook attribute defaults
21
+ setattr(self, "url", getattr(self, "url", "http://localhost:12345"))
22
+ setattr(self, "username", getattr(self, "username", None))
23
+ setattr(self, "password", getattr(self, "password", None))
24
+ setattr(self, "insecure", getattr(self, "insecure", "yes"))
25
+ setattr(self, "headers", getattr(self, "headers", None))
26
+ # OAuth2 attribute defaults
27
+ setattr(self, "token_url", getattr(self, "token_url", None))
28
+ setattr(self, "client_id", getattr(self, "client_id", None))
29
+ setattr(self, "client_secret", getattr(self, "client_secret", None))
30
+ setattr(self, "token_scope", getattr(self, "token_scope", None))
31
+ setattr(self, "token_grant_type", getattr(self, "token_grant_type", "client_credentials"))
32
+ setattr(self, "token_expiry_buffer", int(getattr(self, "token_expiry_buffer", 30)))
33
+
34
+ def _token_cache_path(self):
35
+ return os.path.join(
36
+ os.environ["OMD_ROOT"], "var", "tmp",
37
+ "notificationforwarder_{}_oauth2token.json".format(self.forwarder_name),
38
+ )
39
+
40
+ def acquire_token(self):
41
+ cache_path = self._token_cache_path()
42
+ lock_path = cache_path + ".lock"
43
+
44
+ with open(lock_path, "w") as lf:
45
+ fcntl.flock(lf, fcntl.LOCK_EX)
46
+ try:
47
+ if os.path.exists(cache_path):
48
+ with open(cache_path) as f:
49
+ cached = json.load(f)
50
+ if time.time() < cached.get("expires_at", 0) - self.token_expiry_buffer:
51
+ logger.debug("oauth2: using cached token")
52
+ return cached["access_token"]
53
+
54
+ logger.debug("oauth2: acquiring new token from {}".format(self.token_url))
55
+ data = {
56
+ "grant_type": self.token_grant_type,
57
+ "client_id": self.client_id,
58
+ "client_secret": self.client_secret,
59
+ }
60
+ if self.token_scope:
61
+ data["scope"] = self.token_scope
62
+
63
+ resp = requests.post(
64
+ self.token_url,
65
+ data=data,
66
+ timeout=10,
67
+ verify=(self.insecure != "yes"),
68
+ )
69
+ resp.raise_for_status()
70
+ token_data = resp.json()
71
+ token = token_data["access_token"]
72
+ expires_in = int(token_data.get("expires_in", 3600))
73
+ expires_at = time.time() + expires_in
74
+
75
+ with open(cache_path, "w") as f:
76
+ json.dump({"access_token": token, "expires_at": expires_at}, f)
77
+
78
+ logger.debug("oauth2: token acquired, valid for {}s".format(expires_in))
79
+ return token
80
+ except Exception as e:
81
+ logger.critical("oauth2: token acquisition failed: {}".format(e))
82
+ raise
83
+ finally:
84
+ fcntl.flock(lf, fcntl.LOCK_UN)
85
+
86
+ def submit_one(self, event):
87
+ # Sync logger into the webhook module so WebhookForwarder.submit_one can use it.
88
+ _webhook_module.logger = logger
89
+ try:
90
+ token = self.acquire_token()
91
+ except Exception:
92
+ return False
93
+ hdrs = event.forwarderopts.get("headers") or {}
94
+ if isinstance(hdrs, str):
95
+ hdrs = json.loads(hdrs)
96
+ hdrs["Authorization"] = "Bearer {}".format(token)
97
+ event.forwarderopts["headers"] = hdrs
98
+ return super().submit_one(event)