safing-portbase/apprise/notify.go
2023-09-12 16:43:47 +02:00

167 lines
4.3 KiB
Go

package apprise
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"sync"
"github.com/safing/portbase/utils"
)
// Notifier sends messsages to an Apprise API.
type Notifier struct {
// URL defines the Apprise API endpoint.
URL string
// DefaultType defines the default message type.
DefaultType MsgType
// DefaultTag defines the default message tag.
DefaultTag string
// DefaultFormat defines the default message format.
DefaultFormat MsgFormat
// AllowUntagged defines if untagged messages are allowed,
// which are sent to all configured apprise endpoints.
AllowUntagged bool
client *http.Client
clientLock sync.Mutex
}
// Message represents the message to be sent to the Apprise API.
type Message struct {
// Title is an optional title to go along with the body.
Title string `json:"title,omitempty"`
// Body is the main message content. This is the only required field.
Body string `json:"body"`
// Type defines the message type you want to send as.
// The valid options are info, success, warning, and failure.
// If no type is specified then info is the default value used.
Type MsgType `json:"type,omitempty"`
// Tag is used to notify only those tagged accordingly.
// Use a comma (,) to OR your tags and a space ( ) to AND them.
Tag string `json:"tag,omitempty"`
// Format optionally identifies the text format of the data you're feeding Apprise.
// The valid options are text, markdown, html.
// The default value if nothing is specified is text.
Format MsgFormat `json:"format,omitempty"`
}
// MsgType defines the message type.
type MsgType string
// Message Types.
const (
TypeInfo MsgType = "info"
TypeSuccess MsgType = "success"
TypeWarning MsgType = "warning"
TypeFailure MsgType = "failure"
)
// MsgFormat defines the message format.
type MsgFormat string
// Message Formats.
const (
FormatText MsgFormat = "text"
FormatMarkdown MsgFormat = "markdown"
FormatHTML MsgFormat = "html"
)
type errorResponse struct {
Error string `json:"error"`
}
// Send sends a message to the Apprise API.
func (n *Notifier) Send(ctx context.Context, m *Message) error {
// Check if the message has a body.
if m.Body == "" {
return errors.New("the message must have a body")
}
// Apply notifier defaults.
n.applyDefaults(m)
// Check if the message is tagged.
if m.Tag == "" && !n.AllowUntagged {
return errors.New("the message must have a tag")
}
// Marshal the message to JSON.
payload, err := json.Marshal(m)
if err != nil {
return fmt.Errorf("failed to marshal message: %w", err)
}
// Create request.
request, err := http.NewRequestWithContext(ctx, http.MethodPost, n.URL, bytes.NewReader(payload))
if err != nil {
return fmt.Errorf("failed to create request: %w", err)
}
request.Header.Set("Content-Type", "application/json")
// Send message to API.
resp, err := n.getClient().Do(request)
if err != nil {
return fmt.Errorf("failed to send message: %w", err)
}
defer resp.Body.Close() //nolint:errcheck,gosec
switch resp.StatusCode {
case http.StatusOK, http.StatusCreated, http.StatusNoContent, http.StatusAccepted:
return nil
default:
// Try to tease body contents.
if body, err := io.ReadAll(resp.Body); err == nil && len(body) > 0 {
// Try to parse json response.
errorResponse := &errorResponse{}
if err := json.Unmarshal(body, errorResponse); err == nil && errorResponse.Error != "" {
return fmt.Errorf("failed to send message: apprise returned %q with an error message: %s", resp.Status, errorResponse.Error)
}
return fmt.Errorf("failed to send message: %s (body teaser: %s)", resp.Status, utils.SafeFirst16Bytes(body))
}
return fmt.Errorf("failed to send message: %s", resp.Status)
}
}
func (n *Notifier) applyDefaults(m *Message) {
if m.Type == "" {
m.Type = n.DefaultType
}
if m.Tag == "" {
m.Tag = n.DefaultTag
}
if m.Format == "" {
m.Format = n.DefaultFormat
}
}
// SetClient sets a custom http client for accessing the Apprise API.
func (n *Notifier) SetClient(client *http.Client) {
n.clientLock.Lock()
defer n.clientLock.Unlock()
n.client = client
}
func (n *Notifier) getClient() *http.Client {
n.clientLock.Lock()
defer n.clientLock.Unlock()
// Create client if needed.
if n.client == nil {
n.client = &http.Client{}
}
return n.client
}