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 
&amp; 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]

Reply via email to