Skip to content

Commit 2e9b135

Browse files
ezhil56xshahargl
andauthored
feat: added ntfy provider (#1125)
Co-authored-by: shahargl <[email protected]>
1 parent 195cc9d commit 2e9b135

File tree

8 files changed

+300
-0
lines changed

8 files changed

+300
-0
lines changed

README.md

+2
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,8 @@ Workflow triggers can either be executed manually when an alert is activated or
165165
<img width=32 height=32 src="https://github.com/keephq/keep/blob/main/keep-ui/public/icons/discord-icon.png?raw=true"/>
166166
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
167167
<img width=32 height=32 src="https://github.com/keephq/keep/blob/main/keep-ui/public/icons/twilio-icon.png?raw=true"/>
168+
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
169+
<img width=32 height=32 src="https://github.com/keephq/keep/blob/main/keep-ui/public/icons/ntfy-icon.png?raw=true"/>
168170
</p>
169171
<h3 align="center">Incident Management tools</h2>
170172
<p align="center">

docs/mint.json

+1
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,7 @@
102102
"providers/documentation/mock-provider",
103103
"providers/documentation/mysql-provider",
104104
"providers/documentation/new-relic-provider",
105+
"providers/documentation/ntfy-provider",
105106
"providers/documentation/openshift-provider",
106107
"providers/documentation/opsgenie-provider",
107108
"providers/documentation/pagerduty-provider",
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
---
2+
title: 'Ntfy.sh'
3+
sidebarTitle: 'Ntfy.sh Provider'
4+
description: 'Ntfy.sh allows you to send notifications to your devices'
5+
---
6+
7+
## Authentication Parameters
8+
9+
The Ntfy.sh provider requires the following authentication parameters:
10+
11+
- `Ntfy Access Token`: The access token for the Ntfy.sh account. This is required for the Ntfy.sh provider.
12+
- `Ntfy Host URL`: (For self-hosted Ntfy) The URL of the self-hosted Ntfy instance in the format `https://ntfy.example.com`.
13+
- `Ntfy Username`: (For self-hosted Ntfy) The username for the self-hosted Ntfy instance.
14+
- `Ntfy Password`: (For self-hosted Ntfy) The password for the self-hosted Ntfy instance.
15+
- `Ntfy Subcription Topic`: Required. The topic to which the notification will be sent.
16+
17+
## Connecting with the Provider
18+
19+
Obtain Ntfy Access Token (For Ntfy.sh only)
20+
21+
1. Create an account on [Ntfy.sh](https://ntfy.sh/).
22+
2. After logging in, go to the [Access token](https://ntfy.sh/account) page.
23+
3. Click on the `CREATE ACCESS TOKEN`. Give it a label and select token expiration time and click on the `CREATE TOKEN` button.
24+
4. Copy the generated token. This will be used as the `Ntfy Access Token` in the provider settings.
25+
26+
Self-Hosted Ntfy
27+
28+
1. To self-host Ntfy, you can follow the instructions [here](https://docs.ntfy.sh/install/).
29+
2. For self-hosted Ntfy, you will need to provide the `Ntfy Host URL`, `Ntfy Username`, and `Ntfy Password` in the provider settings instead of the `Ntfy Access Token`.
30+
3. Create a new user for the self-hosted Ntfy instance and use the generated username and password in the provider settings.
31+
32+
Subscribing to a Topic (For Ntfy.sh and self-hosted Ntfy)
33+
34+
1. Login to your Ntfy.sh account.
35+
2. Click on `Subscribe to a topic` button and generate name for the topic and subscribe to it.
36+
3. Copy the generated topic name. This will be used as the `Ntfy Subcription Topic` in the provider settings.
37+
4. Reserve the topic and confiure access (Requires ntfy Pro)
38+
39+
## Usefull Links
40+
41+
- [Ntfy.sh](https://ntfy.sh/)
42+
- [To self-host Ntfy](https://docs.ntfy.sh/install/)

keep-ui/public/icons/ntfy-icon.png

3.54 KB
Loading
+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
## Change the variables
2+
3+
1. Change the UID:GID in the docker-compose file to match your user and group id.
4+
5+
## Run the docker-compose file
6+
7+
```bash
8+
docker-compose up -d
9+
```

keep/providers/ntfy_provider/__init__.py

Whitespace-only changes.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
version: '2.3'
2+
3+
services:
4+
ntfy:
5+
image: binwiederhier/ntfy
6+
container_name: ntfy
7+
command:
8+
- serve
9+
environment:
10+
- TZ=UTC # optional: set desired timezone
11+
user: UID:GID # optional: replace with your own user/group or uid/gid
12+
volumes:
13+
- /var/cache/ntfy:/var/cache/ntfy
14+
- /etc/ntfy:/etc/ntfy
15+
ports:
16+
- 80:80
17+
healthcheck: # optional: remember to adapt the host:port to your environment
18+
test:
19+
[
20+
'CMD-SHELL',
21+
"wget -q --tries=1 http://localhost:80/v1/health -O - | grep -Eo '\"healthy\"\\s*:\\s*true' || exit 1",
22+
]
23+
interval: 60s
24+
timeout: 10s
25+
retries: 3
26+
start_period: 40s
27+
restart: unless-stopped
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,219 @@
1+
"""
2+
NtfyProvider is a class that provides a way to send notifications to the user.
3+
"""
4+
5+
import dataclasses
6+
7+
import pydantic
8+
import base64
9+
import requests
10+
from urllib.parse import urljoin
11+
12+
from keep.contextmanager.contextmanager import ContextManager
13+
from keep.exceptions.provider_exception import ProviderException
14+
from keep.providers.base.base_provider import BaseProvider
15+
from keep.providers.models.provider_config import ProviderConfig, ProviderScope
16+
17+
18+
@pydantic.dataclasses.dataclass
19+
class NtfyProviderAuthConfig:
20+
"""
21+
NtfyProviderAuthConfig is a class that holds the authentication information for the NtfyProvider.
22+
"""
23+
24+
access_token: str = dataclasses.field(
25+
metadata={
26+
"required": False,
27+
"description": "Ntfy Access Token",
28+
"sensitive": True,
29+
},
30+
default=None,
31+
)
32+
33+
host: str = dataclasses.field(
34+
metadata={
35+
"required": False,
36+
"description": "Ntfy Host URL (For self-hosted Ntfy only)",
37+
"sensitive": False,
38+
},
39+
default=None,
40+
)
41+
42+
username: str = dataclasses.field(
43+
metadata={
44+
"required": False,
45+
"description": "Ntfy Username (For self-hosted Ntfy only)",
46+
"sensitive": False,
47+
},
48+
default=None,
49+
)
50+
51+
password: str = dataclasses.field(
52+
metadata={
53+
"required": False,
54+
"description": "Ntfy Password (For self-hosted Ntfy only)",
55+
"sensitive": True,
56+
},
57+
default=None,
58+
)
59+
60+
subcription_topic: str = dataclasses.field(
61+
metadata={
62+
"required": True,
63+
"description": "Ntfy Subcription Topic",
64+
"sensitive": False,
65+
},
66+
default=None,
67+
)
68+
69+
70+
class NtfyProvider(BaseProvider):
71+
PROVIDER_DISPLAY_NAME = "Ntfy.sh"
72+
73+
PROVIDER_SCOPES = [
74+
ProviderScope(
75+
name="send_alert",
76+
mandatory=True,
77+
alias="Send Alert",
78+
)
79+
]
80+
81+
def __init__(
82+
self, context_manager: ContextManager, provider_id: str, config: ProviderConfig
83+
):
84+
super().__init__(context_manager, provider_id, config)
85+
86+
def dispose(self):
87+
pass
88+
89+
def validate_scopes(self) -> dict[str, bool | str]:
90+
validated_scopes = {}
91+
validated_scopes["send_alert"] = True
92+
return validated_scopes
93+
94+
def validate_config(self):
95+
self.authentication_config = NtfyProviderAuthConfig(
96+
**self.config.authentication
97+
)
98+
if self.authentication_config.access_token is None and self.authentication_config.host is None:
99+
raise ProviderException(
100+
"Either Access Token or Host is required"
101+
)
102+
if self.authentication_config.subcription_topic is None:
103+
raise ProviderException(
104+
"Subcription Topic is required"
105+
)
106+
if self.authentication_config.host is not None:
107+
if self.authentication_config.host.startswith("https://") or self.authentication_config.host.startswith("http://"):
108+
raise ProviderException(
109+
"Host should not start with https:// or http://"
110+
)
111+
if self.authentication_config.username is None:
112+
raise ProviderException(
113+
"Username is required when host is provided"
114+
)
115+
if self.authentication_config.password is None:
116+
raise ProviderException(
117+
"Password is required when host is provided"
118+
)
119+
120+
def __get_auth_headers(self):
121+
if self.authentication_config.access_token is not None:
122+
return {
123+
"Authorization": f"Bearer {self.authentication_config.access_token}"
124+
}
125+
126+
else:
127+
username = self.authentication_config.username
128+
password = self.authentication_config.password
129+
token = base64.b64encode(f"{username}:{password}".encode("utf-8")).decode("utf-8")
130+
131+
return {
132+
"Authorization": f"Basic {token}"
133+
}
134+
135+
def __send_alert(self, message=""):
136+
self.logger.debug(f"Sending notification to {self.authentication_config.subcription_topic}")
137+
138+
NTFY_SUBSCRIPTION_TOPIC = self.authentication_config.subcription_topic
139+
140+
if self.authentication_config.host is not None:
141+
base_url = self.authentication_config.host
142+
if not base_url.endswith("/"):
143+
base_url += "/"
144+
NTFY_URL = urljoin(base=base_url, url=NTFY_SUBSCRIPTION_TOPIC)
145+
else:
146+
NTFY_URL = urljoin(base="https://ntfy.sh/", url=NTFY_SUBSCRIPTION_TOPIC)
147+
148+
try:
149+
response = requests.post(NTFY_URL, headers=self.__get_auth_headers(), data=message)
150+
151+
if response.status_code == 401:
152+
raise ProviderException(
153+
f"Failed to send notification to {NTFY_URL}. Error: Unauthorized"
154+
)
155+
156+
response.raise_for_status()
157+
return response.json()
158+
159+
except Exception as e:
160+
raise ProviderException(
161+
f"Failed to send notification to {NTFY_URL}. Error: {e}"
162+
)
163+
164+
def _notify(self, message=""):
165+
return self.__send_alert(message)
166+
167+
if __name__ == "__main__":
168+
import logging
169+
170+
logging.basicConfig(level=logging.DEBUG, handlers=[logging.StreamHandler()])
171+
context_manager = ContextManager(
172+
tenant_id="singletenant",
173+
workflow_id="test",
174+
)
175+
176+
import os
177+
178+
ntfy_access_token = os.environ.get("NTFY_ACCESS_TOKEN")
179+
ntfy_host = os.environ.get("NTFY_HOST")
180+
ntfy_username = os.environ.get("NTFY_USERNAME")
181+
ntfy_password = os.environ.get("NTFY_PASSWORD")
182+
ntfy_subscription_topic = os.environ.get("NTFY_SUBSCRIPTION_TOPIC")
183+
184+
if ntfy_access_token is None and ntfy_host is None:
185+
raise Exception("NTFY_ACCESS_TOKEN or NTFY_HOST is required")
186+
187+
if ntfy_host is not None:
188+
if ntfy_username is None:
189+
raise Exception("NTFY_USERNAME is required")
190+
if ntfy_password is None:
191+
raise Exception("NTFY_PASSWORD is required")
192+
193+
if ntfy_access_token is not None:
194+
config = ProviderConfig(
195+
description="Ntfy Provider",
196+
authentication={
197+
"access_token": ntfy_access_token,
198+
"subcription_topic": ntfy_subscription_topic,
199+
},
200+
)
201+
202+
else:
203+
config = ProviderConfig(
204+
description="Ntfy Provider",
205+
authentication={
206+
"host": ntfy_host,
207+
"username": ntfy_username,
208+
"password": ntfy_password,
209+
"subcription_topic": ntfy_subscription_topic,
210+
},
211+
)
212+
213+
provider = NtfyProvider(
214+
context_manager,
215+
provider_id="ntfy-keephq",
216+
config=config,
217+
)
218+
219+
provider.notify(message="Test message from Keephq")

0 commit comments

Comments
 (0)