This is an automated email from the ASF dual-hosted git repository.
tomsun28 pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/hertzbeat.git
The following commit(s) were added to refs/heads/master by this push:
new 2fd363dfdd [feat] add ntfy as a new alert notification channel (#4132)
2fd363dfdd is described below
commit 2fd363dfdd8677fcad02bdd37a0b2bd427bc84b3
Author: Zmjjeff7 <[email protected]>
AuthorDate: Fri Jul 3 00:30:57 2026 +0800
[feat] add ntfy as a new alert notification channel (#4132)
Co-authored-by: lynx009 <[email protected]>
Co-authored-by: Tomsun28 <[email protected]>
---
.../apache/hertzbeat/alert/AlerterProperties.java | 5 +
.../notice/impl/NtfyAlertNotifyHandlerImpl.java | 182 +++++++++++++
.../impl/NtfyAlertNotifyHandlerImplTest.java | 288 +++++++++++++++++++++
.../common/entity/alerter/NoticeReceiver.java | 25 +-
web-app/src/app/pojo/NoticeReceiver.ts | 4 +
.../alert-notice-receiver.component.html | 26 ++
web-app/src/assets/i18n/en-US.json | 4 +
web-app/src/assets/i18n/ja-JP.json | 4 +
web-app/src/assets/i18n/pt-BR.json | 4 +
web-app/src/assets/i18n/zh-CN.json | 4 +
web-app/src/assets/i18n/zh-TW.json | 4 +
11 files changed, 548 insertions(+), 2 deletions(-)
diff --git
a/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/AlerterProperties.java
b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/AlerterProperties.java
index 13957e1ace..b855c647bb 100644
---
a/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/AlerterProperties.java
+++
b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/AlerterProperties.java
@@ -74,6 +74,11 @@ public class AlerterProperties {
*/
private String gotifyWebhookUrl = "https://push.example.de/message?token=";
+ /**
+ * Ntfy default server url
+ */
+ private String ntfyDefaultServerUrl = "https://ntfy.sh";
+
/**
* Data entry configuration properties
*/
diff --git
a/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/notice/impl/NtfyAlertNotifyHandlerImpl.java
b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/notice/impl/NtfyAlertNotifyHandlerImpl.java
new file mode 100644
index 0000000000..e1a606f3aa
--- /dev/null
+++
b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/notice/impl/NtfyAlertNotifyHandlerImpl.java
@@ -0,0 +1,182 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hertzbeat.alert.notice.impl;
+
+import java.util.Map;
+import java.util.StringJoiner;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.hertzbeat.alert.notice.AlertNoticeException;
+import org.apache.hertzbeat.common.entity.alerter.GroupAlert;
+import org.apache.hertzbeat.common.entity.alerter.NoticeReceiver;
+import org.apache.hertzbeat.common.entity.alerter.NoticeTemplate;
+import org.springframework.http.HttpEntity;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.MediaType;
+import org.springframework.http.ResponseEntity;
+import org.springframework.stereotype.Component;
+
+/**
+ * Send alert notification through ntfy push notification service.
+ * Supports priority mapping, tags/emoji, click action URL,
+ * and optional Bearer token authentication for self-hosted ntfy servers.
+ *
+ * @see <a href="https://docs.ntfy.sh/publish/">ntfy publish API</a>
+ */
+@Component
+@RequiredArgsConstructor
+@Slf4j
+public class NtfyAlertNotifyHandlerImpl extends AbstractAlertNotifyHandlerImpl
{
+
+ private static final String STATUS_FIRING = "firing";
+ private static final String SEVERITY_CRITICAL = "critical";
+ private static final String SEVERITY_WARNING = "warning";
+
+ @Override
+ public void send(NoticeReceiver receiver, NoticeTemplate noticeTemplate,
GroupAlert alert) throws AlertNoticeException {
+ try {
+ String content = renderContent(noticeTemplate, alert);
+ String url = buildNtfyUrl(receiver);
+
+ HttpHeaders headers = new HttpHeaders();
+ headers.setContentType(MediaType.TEXT_PLAIN);
+ headers.set("Title", bundle.getString("alerter.notify.title"));
+ headers.set("Markdown", "yes");
+ headers.set("Priority", String.valueOf(mapPriority(alert)));
+ headers.set("Tags", buildTags(alert));
+
+ // Set click action to console URL if available
+ if (alerterProperties != null && alerterProperties.getConsoleUrl()
!= null
+ && !alerterProperties.getConsoleUrl().isEmpty()) {
+ headers.set("Click", alerterProperties.getConsoleUrl());
+ }
+
+ // Set Bearer token authentication for self-hosted ntfy servers
+ String token = receiver.getNtfyToken();
+ if (token != null && !token.isEmpty()) {
+ headers.set("Authorization", "Bearer " + token);
+ }
+
+ HttpEntity<String> httpEntity = new HttpEntity<>(content, headers);
+ ResponseEntity<String> responseEntity =
restTemplate.postForEntity(url, httpEntity, String.class);
+ if (responseEntity.getStatusCode() == HttpStatus.OK) {
+ log.debug("Send ntfy notification to {} Success", url);
+ } else {
+ log.warn("Send ntfy notification to {} Failed: {}", url,
responseEntity.getBody());
+ throw new AlertNoticeException("Http StatusCode " +
responseEntity.getStatusCode());
+ }
+ } catch (Exception e) {
+ throw new AlertNoticeException("[Ntfy Notify Error] " +
e.getMessage());
+ }
+ }
+
+ @Override
+ public byte type() {
+ return 15;
+ }
+
+ /**
+ * Build the ntfy publish URL from receiver configuration.
+ */
+ private String buildNtfyUrl(NoticeReceiver receiver) {
+ String serverUrl = receiver.getNtfyServerUrl();
+ if (serverUrl == null || serverUrl.isEmpty()) {
+ serverUrl = alerterProperties.getNtfyDefaultServerUrl();
+ }
+ if (serverUrl.endsWith("/")) {
+ serverUrl = serverUrl.substring(0, serverUrl.length() - 1);
+ }
+ return serverUrl + "/" + receiver.getNtfyTopic();
+ }
+
+ /**
+ * Map alert severity to ntfy priority level (1-5).
+ * <ul>
+ * <li>5 (max) - critical severity while firing</li>
+ * <li>4 (high) - warning severity while firing</li>
+ * <li>3 (default) - info or unknown severity while firing</li>
+ * <li>2 (low) - resolved alerts</li>
+ * </ul>
+ *
+ * @see <a href="https://docs.ntfy.sh/publish/#message-priority">ntfy
message priority</a>
+ */
+ protected int mapPriority(GroupAlert alert) {
+ String status = alert.getStatus();
+ if (!STATUS_FIRING.equalsIgnoreCase(status)) {
+ return 2;
+ }
+
+ String severity = extractSeverity(alert);
+ if (SEVERITY_CRITICAL.equalsIgnoreCase(severity)) {
+ return 5;
+ } else if (SEVERITY_WARNING.equalsIgnoreCase(severity)) {
+ return 4;
+ }
+ return 3;
+ }
+
+ /**
+ * Build ntfy tags string with emoji based on alert status and severity.
+ * Tags appear as emoji icons in the notification.
+ *
+ * @see <a href="https://docs.ntfy.sh/publish/#tags-emojis">ntfy tags
& emojis</a>
+ */
+ protected String buildTags(GroupAlert alert) {
+ StringJoiner joiner = new StringJoiner(",");
+ String status = alert.getStatus();
+
+ if (STATUS_FIRING.equalsIgnoreCase(status)) {
+ String severity = extractSeverity(alert);
+ if (SEVERITY_CRITICAL.equalsIgnoreCase(severity)) {
+ joiner.add("rotating_light");
+ joiner.add("skull");
+ } else if (SEVERITY_WARNING.equalsIgnoreCase(severity)) {
+ joiner.add("warning");
+ } else {
+ joiner.add("information_source");
+ }
+ } else {
+ joiner.add("white_check_mark");
+ }
+
+ // Append alert name as a plain-text tag if available
+ Map<String, String> commonLabels = alert.getCommonLabels();
+ if (commonLabels != null && commonLabels.containsKey("alertname")) {
+ joiner.add(commonLabels.get("alertname"));
+ }
+
+ return joiner.toString();
+ }
+
+ /**
+ * Extract severity from alert's common labels.
+ * Checks "severity" key first, then falls back to "priority".
+ */
+ private String extractSeverity(GroupAlert alert) {
+ Map<String, String> commonLabels = alert.getCommonLabels();
+ if (commonLabels == null) {
+ return null;
+ }
+ String severity = commonLabels.get("severity");
+ if (severity == null) {
+ severity = commonLabels.get("priority");
+ }
+ return severity;
+ }
+}
diff --git
a/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/notice/impl/NtfyAlertNotifyHandlerImplTest.java
b/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/notice/impl/NtfyAlertNotifyHandlerImplTest.java
new file mode 100644
index 0000000000..a64ed6cebc
--- /dev/null
+++
b/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/notice/impl/NtfyAlertNotifyHandlerImplTest.java
@@ -0,0 +1,288 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hertzbeat.alert.notice.impl;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.lenient;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.ResourceBundle;
+import org.apache.hertzbeat.alert.AlerterProperties;
+import org.apache.hertzbeat.alert.notice.AlertNoticeException;
+import org.apache.hertzbeat.common.entity.alerter.GroupAlert;
+import org.apache.hertzbeat.common.entity.alerter.NoticeReceiver;
+import org.apache.hertzbeat.common.entity.alerter.NoticeTemplate;
+import org.apache.hertzbeat.common.entity.alerter.SingleAlert;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.springframework.http.HttpEntity;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.client.RestTemplate;
+
+/**
+ * Test case for {@link NtfyAlertNotifyHandlerImpl}
+ */
+@ExtendWith(MockitoExtension.class)
+class NtfyAlertNotifyHandlerImplTest {
+
+ @Mock
+ private RestTemplate restTemplate;
+
+ @Mock
+ private AlerterProperties alerterProperties;
+
+ @Mock
+ private ResourceBundle bundle;
+
+ @InjectMocks
+ private NtfyAlertNotifyHandlerImpl ntfyHandler;
+
+ private NoticeReceiver receiver;
+ private NoticeTemplate template;
+
+ @BeforeEach
+ public void setUp() {
+ receiver = new NoticeReceiver();
+ receiver.setId(1L);
+ receiver.setName("ntfy-test");
+ receiver.setNtfyServerUrl("https://ntfy.example.com");
+ receiver.setNtfyTopic("hertzbeat-alerts");
+
+ template = new NoticeTemplate();
+ template.setId(1L);
+ template.setName("test-template");
+ template.setContent("test alert content");
+
+
lenient().when(alerterProperties.getNtfyDefaultServerUrl()).thenReturn("https://ntfy.sh");
+
lenient().when(alerterProperties.getConsoleUrl()).thenReturn("https://console.hertzbeat.com");
+
lenient().when(bundle.getString("alerter.notify.title")).thenReturn("HertzBeat
Alert");
+ }
+
+ @Test
+ void testType() {
+ assertEquals(15, ntfyHandler.type());
+ }
+
+ @Test
+ void testSendSuccess() {
+ GroupAlert alert = buildGroupAlert("firing", "critical");
+
+ ResponseEntity<String> responseEntity = new
ResponseEntity<>("{\"id\":\"abc123\"}", HttpStatus.OK);
+ when(restTemplate.postForEntity(anyString(), any(HttpEntity.class),
eq(String.class)))
+ .thenReturn(responseEntity);
+
+ ntfyHandler.send(receiver, template, alert);
+
+ ArgumentCaptor<String> urlCaptor =
ArgumentCaptor.forClass(String.class);
+ verify(restTemplate).postForEntity(urlCaptor.capture(),
any(HttpEntity.class), eq(String.class));
+ assertEquals("https://ntfy.example.com/hertzbeat-alerts",
urlCaptor.getValue());
+ }
+
+ @Test
+ void testSendWithAuthToken() {
+ receiver.setNtfyToken("tk_testtoken123");
+ GroupAlert alert = buildGroupAlert("firing", "warning");
+
+ ResponseEntity<String> responseEntity = new ResponseEntity<>("{}",
HttpStatus.OK);
+ when(restTemplate.postForEntity(anyString(), any(HttpEntity.class),
eq(String.class)))
+ .thenReturn(responseEntity);
+
+ ntfyHandler.send(receiver, template, alert);
+
+ @SuppressWarnings("unchecked")
+ ArgumentCaptor<HttpEntity<String>> entityCaptor =
ArgumentCaptor.forClass(HttpEntity.class);
+ verify(restTemplate).postForEntity(anyString(),
entityCaptor.capture(), eq(String.class));
+ assertEquals("Bearer tk_testtoken123",
entityCaptor.getValue().getHeaders().getFirst("Authorization"));
+ }
+
+ @Test
+ void testSendUsesDefaultServerWhenEmpty() {
+ receiver.setNtfyServerUrl("");
+ GroupAlert alert = buildGroupAlert("firing", "info");
+
+ ResponseEntity<String> responseEntity = new ResponseEntity<>("{}",
HttpStatus.OK);
+ when(restTemplate.postForEntity(anyString(), any(HttpEntity.class),
eq(String.class)))
+ .thenReturn(responseEntity);
+
+ ntfyHandler.send(receiver, template, alert);
+
+ ArgumentCaptor<String> urlCaptor =
ArgumentCaptor.forClass(String.class);
+ verify(restTemplate).postForEntity(urlCaptor.capture(),
any(HttpEntity.class), eq(String.class));
+ assertEquals("https://ntfy.sh/hertzbeat-alerts", urlCaptor.getValue());
+ }
+
+ @Test
+ void testSendFailure() {
+ GroupAlert alert = buildGroupAlert("firing", "critical");
+
+ ResponseEntity<String> responseEntity = new ResponseEntity<>("error",
HttpStatus.INTERNAL_SERVER_ERROR);
+ when(restTemplate.postForEntity(anyString(), any(HttpEntity.class),
eq(String.class)))
+ .thenReturn(responseEntity);
+
+ assertThrows(AlertNoticeException.class, () ->
ntfyHandler.send(receiver, template, alert));
+ }
+
+ @Test
+ void testSendNetworkError() {
+ GroupAlert alert = buildGroupAlert("firing", "warning");
+
+ when(restTemplate.postForEntity(anyString(), any(HttpEntity.class),
eq(String.class)))
+ .thenThrow(new
org.springframework.web.client.ResourceAccessException("Connection refused"));
+
+ assertThrows(AlertNoticeException.class, () ->
ntfyHandler.send(receiver, template, alert));
+ }
+
+ @Test
+ void testMapPriorityCriticalFiring() {
+ GroupAlert alert = buildGroupAlert("firing", "critical");
+ assertEquals(5, ntfyHandler.mapPriority(alert));
+ }
+
+ @Test
+ void testMapPriorityWarningFiring() {
+ GroupAlert alert = buildGroupAlert("firing", "warning");
+ assertEquals(4, ntfyHandler.mapPriority(alert));
+ }
+
+ @Test
+ void testMapPriorityInfoFiring() {
+ GroupAlert alert = buildGroupAlert("firing", "info");
+ assertEquals(3, ntfyHandler.mapPriority(alert));
+ }
+
+ @Test
+ void testMapPriorityResolved() {
+ GroupAlert alert = buildGroupAlert("resolved", "critical");
+ assertEquals(2, ntfyHandler.mapPriority(alert));
+ }
+
+ @Test
+ void testMapPriorityUnknownSeverity() {
+ GroupAlert alert = buildGroupAlert("firing", null);
+ assertEquals(3, ntfyHandler.mapPriority(alert));
+ }
+
+ @Test
+ void testBuildTagsCriticalFiring() {
+ GroupAlert alert = buildGroupAlert("firing", "critical");
+ String tags = ntfyHandler.buildTags(alert);
+ assertTrue(tags.contains("rotating_light"));
+ assertTrue(tags.contains("skull"));
+ }
+
+ @Test
+ void testBuildTagsWarningFiring() {
+ GroupAlert alert = buildGroupAlert("firing", "warning");
+ String tags = ntfyHandler.buildTags(alert);
+ assertTrue(tags.contains("warning"));
+ }
+
+ @Test
+ void testBuildTagsResolved() {
+ GroupAlert alert = buildGroupAlert("resolved", "critical");
+ String tags = ntfyHandler.buildTags(alert);
+ assertTrue(tags.contains("white_check_mark"));
+ }
+
+ @Test
+ void testBuildTagsIncludesAlertName() {
+ GroupAlert alert = buildGroupAlert("firing", "warning");
+ alert.getCommonLabels().put("alertname", "HighCPU");
+ String tags = ntfyHandler.buildTags(alert);
+ assertTrue(tags.contains("HighCPU"));
+ }
+
+ @Test
+ void testSendHeadersContainPriorityAndTags() {
+ GroupAlert alert = buildGroupAlert("firing", "critical");
+
+ ResponseEntity<String> responseEntity = new ResponseEntity<>("{}",
HttpStatus.OK);
+ when(restTemplate.postForEntity(anyString(), any(HttpEntity.class),
eq(String.class)))
+ .thenReturn(responseEntity);
+
+ ntfyHandler.send(receiver, template, alert);
+
+ @SuppressWarnings("unchecked")
+ ArgumentCaptor<HttpEntity<String>> entityCaptor =
ArgumentCaptor.forClass(HttpEntity.class);
+ verify(restTemplate).postForEntity(anyString(),
entityCaptor.capture(), eq(String.class));
+
+ HttpEntity<String> captured = entityCaptor.getValue();
+ assertEquals("5", captured.getHeaders().getFirst("Priority"));
+ assertEquals("yes", captured.getHeaders().getFirst("Markdown"));
+ assertEquals("https://console.hertzbeat.com",
captured.getHeaders().getFirst("Click"));
+
assertTrue(captured.getHeaders().getFirst("Tags").contains("rotating_light"));
+ }
+
+ @Test
+ void testServerUrlTrailingSlashRemoved() {
+ receiver.setNtfyServerUrl("https://ntfy.example.com/");
+ GroupAlert alert = buildGroupAlert("firing", "info");
+
+ ResponseEntity<String> responseEntity = new ResponseEntity<>("{}",
HttpStatus.OK);
+ when(restTemplate.postForEntity(anyString(), any(HttpEntity.class),
eq(String.class)))
+ .thenReturn(responseEntity);
+
+ ntfyHandler.send(receiver, template, alert);
+
+ ArgumentCaptor<String> urlCaptor =
ArgumentCaptor.forClass(String.class);
+ verify(restTemplate).postForEntity(urlCaptor.capture(),
any(HttpEntity.class), eq(String.class));
+ assertEquals("https://ntfy.example.com/hertzbeat-alerts",
urlCaptor.getValue());
+ }
+
+ private GroupAlert buildGroupAlert(String status, String severity) {
+ GroupAlert groupAlert = new GroupAlert();
+ groupAlert.setStatus(status);
+
+ Map<String, String> commonLabels = new HashMap<>();
+ if (severity != null) {
+ commonLabels.put("severity", severity);
+ }
+ groupAlert.setCommonLabels(commonLabels);
+ groupAlert.setCommonAnnotations(new HashMap<>());
+ groupAlert.setGroupLabels(new HashMap<>());
+
+ SingleAlert singleAlert = new SingleAlert();
+ singleAlert.setLabels(new HashMap<>());
+ singleAlert.setAnnotations(new HashMap<>());
+ if (severity != null) {
+ singleAlert.getLabels().put("severity", severity);
+ }
+
+ List<SingleAlert> alerts = new ArrayList<>();
+ alerts.add(singleAlert);
+ groupAlert.setAlerts(alerts);
+
+ return groupAlert;
+ }
+}
diff --git
a/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/entity/alerter/NoticeReceiver.java
b/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/entity/alerter/NoticeReceiver.java
index 262ac733f5..cc7dcd6d6b 100644
---
a/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/entity/alerter/NoticeReceiver.java
+++
b/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/entity/alerter/NoticeReceiver.java
@@ -69,12 +69,12 @@ public class NoticeReceiver {
@Schema(title = "Notification information method: 0-SMS 1-Email 2-webhook
3-WeChat Official Account 4-Enterprise WeChat Robot "
+ "5-DingTalk Robot 6-FeiShu Robot 7-Telegram Bot 8-SlackWebHook
9-Discord Bot 10-Enterprise WeChat app message "
- + "11-Slack 12-Discord 13-Gotify 14-FeiShu app message",
+ + "11-Huawei Cloud SMN 12-ServerChan 13-Gotify 14-FeiShu app
message 15-Ntfy",
description = "Notification information method: "
+ "0-SMS 1-Email 2-webhook 3-WeChat Official Account "
+ "4-Enterprise WeChat Robot 5-DingTalk Robot 6-FeiShu
Robot "
+ "7-Telegram Bot 8-SlackWebHook 9-Discord Bot
10-Enterprise "
- + "WeChat app message 11-Slack 12-Discord 13-Gotify
14-FeiShu app message",
+ + "WeChat app message 11-Huawei Cloud SMN 12-ServerChan
13-Gotify 14-FeiShu app message 15-Ntfy",
accessMode = READ_WRITE)
@Min(0)
@NotNull(message = "type can not null")
@@ -258,6 +258,27 @@ public class NoticeReceiver {
@Column(length = 300)
private String gotifyToken;
+ @Schema(title = "Ntfy server URL : The notification method is valid for
Ntfy",
+ description = "Ntfy server URL : The notification method is valid
for Ntfy, default is https://ntfy.sh",
+ example = "https://ntfy.sh", accessMode = READ_WRITE)
+ @Size(max = 300)
+ @Column(length = 300)
+ private String ntfyServerUrl;
+
+ @Schema(title = "Ntfy topic : The notification method is valid for Ntfy",
+ description = "Ntfy topic : The notification method is valid for
Ntfy",
+ example = "hertzbeat-alerts", accessMode = READ_WRITE)
+ @Size(max = 300)
+ @Column(length = 300)
+ private String ntfyTopic;
+
+ @Schema(title = "Ntfy access token : Bearer token for self-hosted ntfy
servers with authentication",
+ description = "Ntfy access token : Bearer token for self-hosted
ntfy servers with authentication",
+ example = "tk_AgQdq7mVBoFD37zQVN29RhuMzNIz2", accessMode =
READ_WRITE)
+ @Size(max = 300)
+ @Column(length = 300)
+ private String ntfyToken;
+
@Schema(title = "The creator of this record", example = "tom",
accessMode = READ_ONLY)
@CreatedBy
diff --git a/web-app/src/app/pojo/NoticeReceiver.ts
b/web-app/src/app/pojo/NoticeReceiver.ts
index d63372b363..d9bf032d32 100644
--- a/web-app/src/app/pojo/NoticeReceiver.ts
+++ b/web-app/src/app/pojo/NoticeReceiver.ts
@@ -22,6 +22,7 @@ export class NoticeReceiver {
name!: string;
// notification mode: 0-sms 1-email 2-webhook 3-wechat public account 4-work
wechat robot 5-Dingding robot 6-Feishu robot
// 7-Telegram robot 8-SlackWebHook 9-Discord robot 10-work wechat app
message 11-Huawei cloud SMN 12-ServerChan 13-Gotify
+ // 14-FeiShu app message 15-Ntfy
type: number = 1;
phone!: string;
email!: string;
@@ -49,6 +50,9 @@ export class NoticeReceiver {
smnTopicUrn!: string;
serverChanToken!: string;
gotifyToken!: string;
+ ntfyServerUrl!: string;
+ ntfyTopic!: string;
+ ntfyToken!: string;
creator!: string;
modifier!: string;
gmtCreate!: number;
diff --git
a/web-app/src/app/routes/alert/alert-notice/alert-notice-receiver/alert-notice-receiver.component.html
b/web-app/src/app/routes/alert/alert-notice/alert-notice-receiver/alert-notice-receiver.component.html
index abdb11330e..1469982ce0 100644
---
a/web-app/src/app/routes/alert/alert-notice/alert-notice-receiver/alert-notice-receiver.component.html
+++
b/web-app/src/app/routes/alert/alert-notice/alert-notice-receiver/alert-notice-receiver.component.html
@@ -131,6 +131,10 @@
<i nz-icon nzTheme="outline" nzType="notification"></i>
<span>{{ 'alert.notice.type.lark-app' | i18n }}</span>
</nz-tag>
+ <nz-tag *ngIf="data.type == 15" nzColor="orange">
+ <i nz-icon nzTheme="outline" nzType="notification"></i>
+ <span>{{ 'alert.notice.type.ntfy' | i18n }}</span>
+ </nz-tag>
</td>
<td nzAlign="center">
<span *ngIf="data.type == 0">{{ data.phone }}</span>
@@ -148,6 +152,7 @@
<span *ngIf="data.type == 12">{{ data.serverChanToken }}</span>
<span *ngIf="data.type == 13">{{ data.gotifyToken }}</span>
<span *ngIf="data.type == 14">{{ data.appId }}</span>
+ <span *ngIf="data.type == 15">{{ data.ntfyServerUrl }}/{{
data.ntfyTopic }}</span>
</td>
<td nzAlign="center">{{ (data.gmtUpdate ? data.gmtUpdate :
data.gmtCreate) | date : 'YYYY-MM-dd HH:mm:ss' }}</td>
<td nzAlign="center" nzRight>
@@ -230,6 +235,7 @@
<nz-option [nzLabel]="'alert.notice.type.serverchan' | i18n"
[nzValue]="12"></nz-option>
<nz-option [nzLabel]="'alert.notice.type.gotify' | i18n"
[nzValue]="13"></nz-option>
<nz-option [nzLabel]="'alert.notice.type.lark-app' | i18n"
[nzValue]="14"></nz-option>
+ <nz-option [nzLabel]="'alert.notice.type.ntfy' | i18n"
[nzValue]="15"></nz-option>
</nz-select>
</nz-form-control>
</nz-form-item>
@@ -528,6 +534,26 @@
<input [(ngModel)]="receiver.gotifyToken" [required]="receiver.type
=== 13" name="gotifyToken" nz-input type="text" />
</nz-form-control>
</nz-form-item>
+ <nz-form-item *ngIf="receiver.type === 15">
+ <nz-form-label [nzSpan]="7" nzFor="ntfyServerUrl">{{
'alert.notice.type.ntfy-server-url' | i18n }}</nz-form-label>
+ <nz-form-control [nzSpan]="12">
+ <input [(ngModel)]="receiver.ntfyServerUrl" name="ntfyServerUrl"
nz-input type="url" placeholder="https://ntfy.sh" />
+ </nz-form-control>
+ </nz-form-item>
+ <nz-form-item *ngIf="receiver.type === 15">
+ <nz-form-label [nzRequired]="receiver.type === 15" [nzSpan]="7"
nzFor="ntfyTopic"
+ >{{ 'alert.notice.type.ntfy-topic' | i18n }}
+ </nz-form-label>
+ <nz-form-control [nzErrorTip]="'validation.required' | i18n"
[nzSpan]="12">
+ <input [(ngModel)]="receiver.ntfyTopic" [required]="receiver.type
=== 15" name="ntfyTopic" nz-input type="text" />
+ </nz-form-control>
+ </nz-form-item>
+ <nz-form-item *ngIf="receiver.type === 15">
+ <nz-form-label [nzSpan]="7" nzFor="ntfyToken">{{
'alert.notice.type.ntfy-token' | i18n }}</nz-form-label>
+ <nz-form-control [nzSpan]="12">
+ <input [(ngModel)]="receiver.ntfyToken" name="ntfyToken" nz-input
type="password" placeholder="" />
+ </nz-form-control>
+ </nz-form-item>
<nz-form-item *ngIf="receiver.type === 14">
<nz-form-label [nzRequired]="receiver.type === 14" [nzSpan]="7"
nzFor="appId">
{{ 'alert.notice.type.lark-app-appId' | i18n }}
diff --git a/web-app/src/assets/i18n/en-US.json
b/web-app/src/assets/i18n/en-US.json
index b4ad4a6677..211fd3387c 100644
--- a/web-app/src/assets/i18n/en-US.json
+++ b/web-app/src/assets/i18n/en-US.json
@@ -218,6 +218,10 @@
"alert.notice.type.lark-app-chatId": "Chat Id(separated by , symbol)",
"alert.notice.type.gotify": "Gotify",
"alert.notice.type.gotify-token": "Gotify Token",
+ "alert.notice.type.ntfy": "Ntfy",
+ "alert.notice.type.ntfy-server-url": "Ntfy Server URL",
+ "alert.notice.type.ntfy-topic": "Ntfy Topic",
+ "alert.notice.type.ntfy-token": "Ntfy Access Token",
"alert.notice.type.phone": "Phone",
"alert.notice.type.serverchan": "ServerChan",
"alert.notice.type.serverchan-token": "ServerChanToken",
diff --git a/web-app/src/assets/i18n/ja-JP.json
b/web-app/src/assets/i18n/ja-JP.json
index ad17160459..68336fff41 100644
--- a/web-app/src/assets/i18n/ja-JP.json
+++ b/web-app/src/assets/i18n/ja-JP.json
@@ -205,6 +205,10 @@
"alert.notice.type.fei-shu-key": "FeiShu ロボットキー",
"alert.notice.type.gotify": "Gotify",
"alert.notice.type.gotify-token": "Gotify トークン",
+ "alert.notice.type.ntfy": "Ntfy",
+ "alert.notice.type.ntfy-server-url": "Ntfy サーバーURL",
+ "alert.notice.type.ntfy-topic": "Ntfy トピック",
+ "alert.notice.type.ntfy-token": "Ntfy アクセストークン",
"alert.notice.type.phone": "電話",
"alert.notice.type.serverchan": "ServerChan",
"alert.notice.type.serverchan-token": "ServerChanトークン",
diff --git a/web-app/src/assets/i18n/pt-BR.json
b/web-app/src/assets/i18n/pt-BR.json
index 72bff765b1..b9b9c520dd 100644
--- a/web-app/src/assets/i18n/pt-BR.json
+++ b/web-app/src/assets/i18n/pt-BR.json
@@ -200,6 +200,10 @@
"alert.notice.type.serverchan-token": "Token do ServerChan",
"alert.notice.type.gotify": "Gotify",
"alert.notice.type.gotify-token": "Token do Gotify",
+ "alert.notice.type.ntfy": "Ntfy",
+ "alert.notice.type.ntfy-server-url": "URL do Servidor Ntfy",
+ "alert.notice.type.ntfy-topic": "Tópico Ntfy",
+ "alert.notice.type.ntfy-token": "Token de Acesso Ntfy",
"alert.notice.rule": "Política de Notificação",
"alert.notice.rule.new": "Nova Política de Notificação",
"alert.notice.rule.edit": "Editar Política de Notificação",
diff --git a/web-app/src/assets/i18n/zh-CN.json
b/web-app/src/assets/i18n/zh-CN.json
index 768473d64f..db4ef2f856 100644
--- a/web-app/src/assets/i18n/zh-CN.json
+++ b/web-app/src/assets/i18n/zh-CN.json
@@ -218,6 +218,10 @@
"alert.notice.type.lark-app-chatId": "群聊id(多个使用,符号分隔)",
"alert.notice.type.gotify": "Gotify",
"alert.notice.type.gotify-token": "Gotify Token",
+ "alert.notice.type.ntfy": "Ntfy",
+ "alert.notice.type.ntfy-server-url": "Ntfy 服务器地址",
+ "alert.notice.type.ntfy-topic": "Ntfy 主题",
+ "alert.notice.type.ntfy-token": "Ntfy 访问令牌",
"alert.notice.type.phone": "手机号",
"alert.notice.type.serverchan": "Server酱(ServerChan)",
"alert.notice.type.serverchan-token": "Server酱Token",
diff --git a/web-app/src/assets/i18n/zh-TW.json
b/web-app/src/assets/i18n/zh-TW.json
index a05dc1b7c5..6cb39791dd 100644
--- a/web-app/src/assets/i18n/zh-TW.json
+++ b/web-app/src/assets/i18n/zh-TW.json
@@ -204,6 +204,10 @@
"alert.notice.type.fei-shu-key": "飛書機器人KEY",
"alert.notice.type.gotify": "Gotify",
"alert.notice.type.gotify-token": "Gotify Token",
+ "alert.notice.type.ntfy": "Ntfy",
+ "alert.notice.type.ntfy-server-url": "Ntfy 伺服器地址",
+ "alert.notice.type.ntfy-topic": "Ntfy 主題",
+ "alert.notice.type.ntfy-token": "Ntfy 存取權杖",
"alert.notice.type.phone": "手機號",
"alert.notice.type.serverchan": "ServerChan",
"alert.notice.type.serverchan-token": "ServerChanToken",
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]