171 lines
4.5 KiB
Go
171 lines
4.5 KiB
Go
package pusher
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"regexp"
|
|
"strings"
|
|
"wdd.io/agent-common/logger"
|
|
)
|
|
|
|
var (
|
|
topicRegex = regexp.MustCompile(`^[-_A-Za-z0-9]{1,64}$`) // Same as in server/server.go
|
|
log = logger.Log
|
|
DefaultPusherClient = NewDefaultClient()
|
|
)
|
|
|
|
const (
|
|
maxResponseBytes = 4096
|
|
)
|
|
|
|
type Client struct {
|
|
config *Config
|
|
}
|
|
|
|
// Message is a struct that represents a ntfy message
|
|
type Message struct { // TODO combine with server.message
|
|
ID string
|
|
Event string
|
|
Time int64
|
|
Topic string
|
|
Message string
|
|
Title string
|
|
Priority int
|
|
Tags []string
|
|
Click string
|
|
Icon string
|
|
Attachment *Attachment
|
|
|
|
// Additional fields
|
|
TopicURL string
|
|
SubscriptionID string
|
|
Raw string
|
|
}
|
|
|
|
// Attachment represents a message attachment
|
|
type Attachment struct {
|
|
Name string `json:"name"`
|
|
Type string `json:"type,omitempty"`
|
|
Size int64 `json:"size,omitempty"`
|
|
Expires int64 `json:"expires,omitempty"`
|
|
URL string `json:"url"`
|
|
Owner string `json:"-"` // IP address of uploader, used for rate limiting
|
|
}
|
|
|
|
// New creates a new Client using a given Config
|
|
func New(config *Config) *Client {
|
|
return &Client{
|
|
config: config,
|
|
}
|
|
}
|
|
|
|
func NewDefaultClient() *Client {
|
|
defaultConfig := NewDefaultConfig()
|
|
return New(defaultConfig)
|
|
}
|
|
|
|
func (c *Client) ChangeTopicName(topicName string) {
|
|
c.config.DefaultTopic = topicName
|
|
}
|
|
|
|
func (c *Client) PublishDefault(message bytes.Buffer, options []PublishOption) (*Message, error) {
|
|
if c.config.DefaultTopic == "" {
|
|
return nil, errors.New("[PublishDefault] - topic empty")
|
|
}
|
|
// parse default
|
|
options = c.parseConfigToOption(options)
|
|
|
|
return c.PublishReader(c.config.DefaultTopic, bytes.NewReader(message.Bytes()), options)
|
|
}
|
|
|
|
// Publish sends a message to a specific topic, optionally using options.
|
|
// See PublishReader for details.
|
|
func (c *Client) Publish(topic, message string, options []PublishOption) (*Message, error) {
|
|
return c.PublishReader(topic, strings.NewReader(message), options)
|
|
}
|
|
|
|
// PublishReader sends a message to a specific topic, optionally using options.
|
|
//
|
|
// A topic can be either a full URL (e.g. https://myhost.lan/mytopic), a short URL which is then prepended https://
|
|
// (e.g. myhost.lan -> https://myhost.lan), or a short name which is expanded using the default host in the
|
|
// config (e.g. mytopic -> https://ntfy.sh/mytopic).
|
|
//
|
|
// To pass title, priority and tags, check out WithTitle, WithPriority, WithTagsList, WithDelay, WithNoCache,
|
|
// WithNoFirebase, and the generic WithHeader.
|
|
func (c *Client) PublishReader(topic string, body io.Reader, options []PublishOption) (*Message, error) {
|
|
topicURL, err := c.expandTopicURL(topic)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
req, err := http.NewRequest("POST", topicURL, body)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
for _, option := range options {
|
|
if err := option(req); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
log.DebugF("%s Publishing message with headers %s", topicURL, req.Header)
|
|
resp, err := http.DefaultClient.Do(req)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer resp.Body.Close()
|
|
b, err := io.ReadAll(io.LimitReader(resp.Body, maxResponseBytes))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if resp.StatusCode != http.StatusOK {
|
|
return nil, errors.New(strings.TrimSpace(string(b)))
|
|
}
|
|
m, err := toMessage(string(b), topicURL, "")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return m, nil
|
|
}
|
|
|
|
func (c *Client) expandTopicURL(topic string) (string, error) {
|
|
if strings.HasPrefix(topic, "http://") || strings.HasPrefix(topic, "https://") {
|
|
return topic, nil
|
|
} else if strings.Contains(topic, "/") {
|
|
return fmt.Sprintf("https://%s", topic), nil
|
|
}
|
|
if !topicRegex.MatchString(topic) {
|
|
return "", fmt.Errorf("invalid topic name: %s", topic)
|
|
}
|
|
return fmt.Sprintf("%s/%s", c.config.Host, topic), nil
|
|
}
|
|
|
|
func (c *Client) parseConfigToOption(options []PublishOption) []PublishOption {
|
|
config := c.config
|
|
|
|
if config.Token != "" {
|
|
options = append(options, WithBearerAuth(config.Token))
|
|
} else if config.User != "" {
|
|
if *config.Password != "" {
|
|
options = append(options, WithBasicAuth(config.User, *config.Password))
|
|
} else {
|
|
log.ErrorF("[parseConfigToOption] - default password is empty!")
|
|
}
|
|
}
|
|
|
|
return options
|
|
}
|
|
|
|
func toMessage(s, topicURL, subscriptionID string) (*Message, error) {
|
|
var m *Message
|
|
if err := json.NewDecoder(strings.NewReader(s)).Decode(&m); err != nil {
|
|
return nil, err
|
|
}
|
|
m.TopicURL = topicURL
|
|
m.SubscriptionID = subscriptionID
|
|
m.Raw = s
|
|
return m, nil
|
|
}
|