NTFY integration #2
|
|
@ -20,6 +20,8 @@ type FormValues = {
|
|||
type: string
|
||||
telegramApiKey?: string
|
||||
telegramTargetChannel?: number
|
||||
ntfyEndpoint?: string
|
||||
ntfyAuthToken?: string
|
||||
}
|
||||
|
||||
export const ContactPointFormModal = ({
|
||||
|
|
@ -45,6 +47,13 @@ export const ContactPointFormModal = ({
|
|||
}
|
||||
}
|
||||
|
||||
if (v.type === 'ntfy') {
|
||||
return {
|
||||
endpoint: v.ntfyEndpoint,
|
||||
authToken: v.ntfyAuthToken,
|
||||
}
|
||||
}
|
||||
|
||||
return {}
|
||||
}
|
||||
|
||||
|
|
@ -56,6 +65,10 @@ export const ContactPointFormModal = ({
|
|||
telegramApiKey: parsedTypeConfig.apiKey ?? '',
|
||||
telegramTargetChannel: parsedTypeConfig.targetChannel,
|
||||
}),
|
||||
...(parsedTypeConfig?.type === 'ntfy' && {
|
||||
ntfyEndpoint: parsedTypeConfig.endpoint ?? '',
|
||||
ntfyAuthToken: parsedTypeConfig.authToken ?? '',
|
||||
}),
|
||||
}),
|
||||
onSubmit: async (v) => {
|
||||
if (isLoading) {
|
||||
|
|
@ -105,6 +118,7 @@ export const ContactPointFormModal = ({
|
|||
<label>Type</label>
|
||||
<select required {...register('type')}>
|
||||
<option value="telegram">Telegram</option>
|
||||
<option value="ntfy">Ntfy</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
|
|
@ -132,6 +146,26 @@ export const ContactPointFormModal = ({
|
|||
</>
|
||||
)}
|
||||
|
||||
{type === 'ntfy' && (
|
||||
<>
|
||||
<div className="input">
|
||||
<label>Endpoint</label>
|
||||
<input
|
||||
type="text"
|
||||
minLength={1}
|
||||
required
|
||||
placeholder="https://ntfy.sh"
|
||||
{...register('ntfyEndpoint')}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="input">
|
||||
<label>Auth Token (optional)</label>
|
||||
<input type="text" {...register('ntfyAuthToken')} />
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
<div className="actions">
|
||||
<button
|
||||
className="test"
|
||||
|
|
|
|||
|
|
@ -7,7 +7,15 @@ type ContactPointTelegramConfig = {
|
|||
targetChannel: number
|
||||
}
|
||||
|
||||
export type ParsedContactPointConfig = ContactPointTelegramConfig
|
||||
type ContactPointNtfyConfig = {
|
||||
type: 'ntfy'
|
||||
endpoint: string
|
||||
authToken?: string
|
||||
}
|
||||
|
||||
export type ParsedContactPointConfig =
|
||||
| ContactPointTelegramConfig
|
||||
| ContactPointNtfyConfig
|
||||
|
||||
export const tryParseContactPointConfig = (
|
||||
contactPoint: ContactPointInfo
|
||||
|
|
@ -26,5 +34,13 @@ export const tryParseContactPointConfig = (
|
|||
}
|
||||
}
|
||||
|
||||
if (contactPoint.type === 'ntfy') {
|
||||
return {
|
||||
type: 'ntfy',
|
||||
endpoint: data.endpoint,
|
||||
authToken: data.authToken,
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,158 @@
|
|||
package integrations
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type NtfyIntegration struct{}
|
||||
|
||||
var ntfyHTTPClient = &http.Client{
|
||||
Timeout: 15 * time.Second,
|
||||
}
|
||||
|
||||
type NtfyNotificationChannelConfig struct {
|
||||
Endpoint string `json:"endpoint"`
|
||||
AuthToken string `json:"authToken"`
|
||||
}
|
||||
|
||||
func (s NtfyIntegration) ProcessEvent(evt *ContactPointEvent, rawConfig string) error {
|
||||
config := NtfyNotificationChannelConfig{}
|
||||
err := json.Unmarshal([]byte(rawConfig), &config)
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to parse ntfy integration config - %w", err)
|
||||
}
|
||||
|
||||
if err := s.validate(config); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
switch evt.Type {
|
||||
case ContactPointEventAlertTriggered:
|
||||
data := evt.AlertTriggeredEvent
|
||||
|
||||
if data.SensorValueCondition != nil {
|
||||
text := fmt.Sprintf("%s is at {value}", data.Sensor.Name)
|
||||
|
||||
if data.Alert.CustomMessage != "" {
|
||||
text = data.Alert.CustomMessage
|
||||
}
|
||||
|
||||
text = strings.Replace(text, "{value}", strconv.FormatFloat(data.LastValue, 'f', -1, 64), -1)
|
||||
|
||||
return s.send(config, "Alert Triggered", text)
|
||||
}
|
||||
|
||||
if data.SensorLastContactCondition != nil {
|
||||
text := fmt.Sprintf(
|
||||
"%s has not reported in for last %s %s",
|
||||
data.Sensor.Name,
|
||||
strconv.FormatFloat(data.SensorLastContactCondition.Value, 'f', -1, 64),
|
||||
data.SensorLastContactCondition.ValueUnit,
|
||||
)
|
||||
|
||||
if data.Alert.CustomMessage != "" {
|
||||
text = data.Alert.CustomMessage
|
||||
}
|
||||
|
||||
return s.send(config, "Alert Triggered", text)
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
case ContactPointEventAlertResolved:
|
||||
data := evt.AlertResolvedEvent
|
||||
|
||||
if data.SensorValueCondition != nil {
|
||||
text := fmt.Sprintf("%s is at {value}", data.Sensor.Name)
|
||||
|
||||
if data.Alert.CustomResolvedMessage != "" {
|
||||
text = data.Alert.CustomResolvedMessage
|
||||
}
|
||||
|
||||
text = strings.Replace(text, "{value}", strconv.FormatFloat(data.LastValue, 'f', -1, 64), -1)
|
||||
|
||||
return s.send(config, "Alert Resolved", text)
|
||||
}
|
||||
|
||||
if data.SensorLastContactCondition != nil {
|
||||
text := fmt.Sprintf(
|
||||
"%s has reported in last %s %s",
|
||||
data.Sensor.Name,
|
||||
strconv.FormatFloat(data.SensorLastContactCondition.Value, 'f', -1, 64),
|
||||
data.SensorLastContactCondition.ValueUnit,
|
||||
)
|
||||
|
||||
if data.Alert.CustomResolvedMessage != "" {
|
||||
text = data.Alert.CustomResolvedMessage
|
||||
}
|
||||
|
||||
return s.send(config, "Alert Resolved", text)
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
case ContactPointEventTest:
|
||||
return s.send(config, "Test", "Test message from Basic Sensor Receiver")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s NtfyIntegration) ValidateConfig(rawConfig string) error {
|
||||
config := NtfyNotificationChannelConfig{}
|
||||
err := json.Unmarshal([]byte(rawConfig), &config)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return s.validate(config)
|
||||
}
|
||||
|
||||
func (s NtfyIntegration) validate(config NtfyNotificationChannelConfig) error {
|
||||
if strings.TrimSpace(config.Endpoint) == "" {
|
||||
return fmt.Errorf("ntfy endpoint is required")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s NtfyIntegration) send(config NtfyNotificationChannelConfig, title string, text string) error {
|
||||
url := strings.TrimSpace(config.Endpoint)
|
||||
|
||||
req, err := http.NewRequest(http.MethodPost, url, strings.NewReader(text))
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create ntfy request - %w", err)
|
||||
}
|
||||
|
||||
req.Header.Set("Content-Type", "text/plain; charset=utf-8")
|
||||
req.Header.Set("Title", title)
|
||||
|
||||
if config.AuthToken != "" {
|
||||
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", config.AuthToken))
|
||||
}
|
||||
|
||||
resp, err := ntfyHTTPClient.Do(req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to send ntfy notification - %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode < 200 || resp.StatusCode > 299 {
|
||||
body, readErr := io.ReadAll(resp.Body)
|
||||
if readErr != nil {
|
||||
return fmt.Errorf("ntfy returned status %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
return fmt.Errorf("ntfy returned status %d: %s", resp.StatusCode, strings.TrimSpace(string(body)))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
@ -9,12 +9,12 @@ import (
|
|||
|
||||
type postOrPutContactPointsBody struct {
|
||||
Name string `json:"name" binding:"required"`
|
||||
Type string `json:"type" binding:"required,oneof=telegram"`
|
||||
Type string `json:"type" binding:"required,oneof=telegram ntfy"`
|
||||
TypeConfig string `json:"typeConfig" binding:"required"`
|
||||
}
|
||||
|
||||
type testContactPointBody struct {
|
||||
Type string `json:"type" binding:"required,oneof=telegram"`
|
||||
Type string `json:"type" binding:"required,oneof=telegram ntfy"`
|
||||
TypeConfig string `json:"typeConfig" binding:"required"`
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -62,6 +62,14 @@ func (s *ContactPointsService) Create(name string, contactType string, contactTy
|
|||
return nil, fmt.Errorf("failed to validate telegram config - %w", err)
|
||||
}
|
||||
}
|
||||
case "ntfy":
|
||||
{
|
||||
err := s.ctx.Integrations.Ntfy.ValidateConfig(contactTypeConfig)
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to validate ntfy config - %w", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
item := ContactPointItem{
|
||||
|
|
@ -144,6 +152,8 @@ func (channel *ContactPointItem) getService(ctx *Context) (integrations.ContactP
|
|||
switch channel.Type {
|
||||
case "telegram":
|
||||
return ctx.Integrations.Telegram, nil
|
||||
case "ntfy":
|
||||
return ctx.Integrations.Ntfy, nil
|
||||
}
|
||||
|
||||
return nil, errors.New("unknown channel type")
|
||||
|
|
@ -153,6 +163,8 @@ func (channel *ContactPointItem) getTestService(ctx *Context) (integrations.Cont
|
|||
switch channel.Type {
|
||||
case "telegram":
|
||||
return ctx.Integrations.Telegram, nil
|
||||
case "ntfy":
|
||||
return ctx.Integrations.Ntfy, nil
|
||||
}
|
||||
|
||||
return nil, errors.New("unknown channel type")
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ type Services struct {
|
|||
|
||||
type Integrations struct {
|
||||
Telegram *integrations.TelegramIntegration
|
||||
Ntfy *integrations.NtfyIntegration
|
||||
MQTT *integrations.MQTTIntegration
|
||||
}
|
||||
|
||||
|
|
@ -50,6 +51,7 @@ func InitializeServices(ctx *Context) *Services {
|
|||
|
||||
ctx.Integrations = &Integrations{}
|
||||
ctx.Integrations.Telegram = &integrations.TelegramIntegration{}
|
||||
ctx.Integrations.Ntfy = &integrations.NtfyIntegration{}
|
||||
ctx.Integrations.MQTT = &integrations.MQTTIntegration{}
|
||||
|
||||
return &services
|
||||
|
|
|
|||
Loading…
Reference in New Issue