aboutsummaryrefslogtreecommitdiff
path: root/internal/integration/ntfy/ntfy.go
blob: 04fd978a88c2893508ae6884e9d6c2d75dda65bc (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
// SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

package ntfy // import "miniflux.app/v2/internal/integration/ntfy"

import (
	"bytes"
	"encoding/json"
	"fmt"
	"log/slog"
	"net/http"
	"time"

	"miniflux.app/v2/internal/model"
	"miniflux.app/v2/internal/version"
)

const (
	defaultClientTimeout = 10 * time.Second
	defaultNtfyURL       = "https://ntfy.sh"
)

type Client struct {
	ntfyURL, ntfyTopic, ntfyApiToken, ntfyUsername, ntfyPassword, ntfyIconURL string
	ntfyPriority                                                              int
}

func NewClient(ntfyURL, ntfyTopic, ntfyApiToken, ntfyUsername, ntfyPassword, ntfyIconURL string, ntfyPriority int) *Client {
	if ntfyURL == "" {
		ntfyURL = defaultNtfyURL
	}
	return &Client{ntfyURL, ntfyTopic, ntfyApiToken, ntfyUsername, ntfyPassword, ntfyIconURL, ntfyPriority}
}

func (c *Client) SendMessages(feed *model.Feed, entries model.Entries) error {
	for _, entry := range entries {
		ntfyMessage := &ntfyMessage{
			Topic:    c.ntfyTopic,
			Message:  entry.Title,
			Title:    feed.Title,
			Priority: c.ntfyPriority,
			Click:    entry.URL,
		}

		if c.ntfyIconURL != "" {
			ntfyMessage.Icon = c.ntfyIconURL
		}

		slog.Debug("Sending Ntfy message",
			slog.String("url", c.ntfyURL),
			slog.String("topic", c.ntfyTopic),
			slog.Int("priority", ntfyMessage.Priority),
			slog.String("message", ntfyMessage.Message),
			slog.String("entry_url", entry.URL),
		)

		if err := c.makeRequest(ntfyMessage); err != nil {
			return err
		}
	}

	return nil
}

func (c *Client) makeRequest(payload any) error {
	requestBody, err := json.Marshal(payload)
	if err != nil {
		return fmt.Errorf("ntfy: unable to encode request body: %v", err)
	}

	request, err := http.NewRequest(http.MethodPost, c.ntfyURL, bytes.NewReader(requestBody))
	if err != nil {
		return fmt.Errorf("ntfy: unable to create request: %v", err)
	}

	request.Header.Set("Content-Type", "application/json")
	request.Header.Set("User-Agent", "Miniflux/"+version.Version)

	// See https://docs.ntfy.sh/publish/#access-tokens
	if c.ntfyApiToken != "" {
		request.Header.Set("Authorization", fmt.Sprintf("Bearer %s", c.ntfyApiToken))
	}

	// See https://docs.ntfy.sh/publish/#username-password
	if c.ntfyUsername != "" && c.ntfyPassword != "" {
		request.SetBasicAuth(c.ntfyUsername, c.ntfyPassword)
	}

	httpClient := &http.Client{Timeout: defaultClientTimeout}
	response, err := httpClient.Do(request)
	if err != nil {
		return fmt.Errorf("ntfy: unable to send request: %v", err)
	}
	defer response.Body.Close()

	if response.StatusCode >= 400 {
		return fmt.Errorf("ntfy: incorrect response status code %d for url %s", response.StatusCode, c.ntfyURL)
	}

	return nil
}

// See https://docs.ntfy.sh/publish/#publish-as-json
type ntfyMessage struct {
	Topic    string       `json:"topic"`
	Message  string       `json:"message"`
	Title    string       `json:"title"`
	Tags     []string     `json:"tags,omitempty"`
	Priority int          `json:"priority,omitempty"`
	Icon     string       `json:"icon,omitempty"` // https://docs.ntfy.sh/publish/#icons
	Click    string       `json:"click,omitempty"`
	Actions  []ntfyAction `json:"actions,omitempty"`
}

// See https://docs.ntfy.sh/publish/#action-buttons
type ntfyAction struct {
	Action string `json:"action"`
	Label  string `json:"label"`
	URL    string `json:"url"`
}