mirror of
https://github.com/dromara/hertzbeat.git
synced 2026-09-17 09:40:58 +00:00
[feat] add ntfy as a new alert notification channel (#4132)
Co-authored-by: lynx009 <2030509072@qq.com> Co-authored-by: Tomsun28 <tomsun28@outlook.com>
This commit is contained in:
co-authored by
lynx009
Tomsun28
parent
2389db7b3b
commit
2fd363dfdd
@@ -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
|
||||
*/
|
||||
|
||||
+182
@@ -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;
|
||||
}
|
||||
}
|
||||
+288
@@ -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;
|
||||
}
|
||||
}
|
||||
+23
-2
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
+26
@@ -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 }}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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トークン",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user