github.com/safing/portbase@v0.19.5/apprise/notify.go (about)

     1  package apprise
     2  
     3  import (
     4  	"bytes"
     5  	"context"
     6  	"encoding/json"
     7  	"errors"
     8  	"fmt"
     9  	"io"
    10  	"net/http"
    11  	"sync"
    12  
    13  	"github.com/safing/portbase/utils"
    14  )
    15  
    16  // Notifier sends messsages to an Apprise API.
    17  type Notifier struct {
    18  	// URL defines the Apprise API endpoint.
    19  	URL string
    20  
    21  	// DefaultType defines the default message type.
    22  	DefaultType MsgType
    23  
    24  	// DefaultTag defines the default message tag.
    25  	DefaultTag string
    26  
    27  	// DefaultFormat defines the default message format.
    28  	DefaultFormat MsgFormat
    29  
    30  	// AllowUntagged defines if untagged messages are allowed,
    31  	// which are sent to all configured apprise endpoints.
    32  	AllowUntagged bool
    33  
    34  	client     *http.Client
    35  	clientLock sync.Mutex
    36  }
    37  
    38  // Message represents the message to be sent to the Apprise API.
    39  type Message struct {
    40  	// Title is an optional title to go along with the body.
    41  	Title string `json:"title,omitempty"`
    42  
    43  	// Body is the main message content. This is the only required field.
    44  	Body string `json:"body"`
    45  
    46  	// Type defines the message type you want to send as.
    47  	// The valid options are info, success, warning, and failure.
    48  	// If no type is specified then info is the default value used.
    49  	Type MsgType `json:"type,omitempty"`
    50  
    51  	// Tag is used to notify only those tagged accordingly.
    52  	// Use a comma (,) to OR your tags and a space ( ) to AND them.
    53  	Tag string `json:"tag,omitempty"`
    54  
    55  	// Format optionally identifies the text format of the data you're feeding Apprise.
    56  	// The valid options are text, markdown, html.
    57  	// The default value if nothing is specified is text.
    58  	Format MsgFormat `json:"format,omitempty"`
    59  }
    60  
    61  // MsgType defines the message type.
    62  type MsgType string
    63  
    64  // Message Types.
    65  const (
    66  	TypeInfo    MsgType = "info"
    67  	TypeSuccess MsgType = "success"
    68  	TypeWarning MsgType = "warning"
    69  	TypeFailure MsgType = "failure"
    70  )
    71  
    72  // MsgFormat defines the message format.
    73  type MsgFormat string
    74  
    75  // Message Formats.
    76  const (
    77  	FormatText     MsgFormat = "text"
    78  	FormatMarkdown MsgFormat = "markdown"
    79  	FormatHTML     MsgFormat = "html"
    80  )
    81  
    82  type errorResponse struct {
    83  	Error string `json:"error"`
    84  }
    85  
    86  // Send sends a message to the Apprise API.
    87  func (n *Notifier) Send(ctx context.Context, m *Message) error {
    88  	// Check if the message has a body.
    89  	if m.Body == "" {
    90  		return errors.New("the message must have a body")
    91  	}
    92  
    93  	// Apply notifier defaults.
    94  	n.applyDefaults(m)
    95  
    96  	// Check if the message is tagged.
    97  	if m.Tag == "" && !n.AllowUntagged {
    98  		return errors.New("the message must have a tag")
    99  	}
   100  
   101  	// Marshal the message to JSON.
   102  	payload, err := json.Marshal(m)
   103  	if err != nil {
   104  		return fmt.Errorf("failed to marshal message: %w", err)
   105  	}
   106  
   107  	// Create request.
   108  	request, err := http.NewRequestWithContext(ctx, http.MethodPost, n.URL, bytes.NewReader(payload))
   109  	if err != nil {
   110  		return fmt.Errorf("failed to create request: %w", err)
   111  	}
   112  	request.Header.Set("Content-Type", "application/json")
   113  
   114  	// Send message to API.
   115  	resp, err := n.getClient().Do(request)
   116  	if err != nil {
   117  		return fmt.Errorf("failed to send message: %w", err)
   118  	}
   119  	defer resp.Body.Close() //nolint:errcheck,gosec
   120  	switch resp.StatusCode {
   121  	case http.StatusOK, http.StatusCreated, http.StatusNoContent, http.StatusAccepted:
   122  		return nil
   123  	default:
   124  		// Try to tease body contents.
   125  		if body, err := io.ReadAll(resp.Body); err == nil && len(body) > 0 {
   126  			// Try to parse json response.
   127  			errorResponse := &errorResponse{}
   128  			if err := json.Unmarshal(body, errorResponse); err == nil && errorResponse.Error != "" {
   129  				return fmt.Errorf("failed to send message: apprise returned %q with an error message: %s", resp.Status, errorResponse.Error)
   130  			}
   131  			return fmt.Errorf("failed to send message: %s (body teaser: %s)", resp.Status, utils.SafeFirst16Bytes(body))
   132  		}
   133  		return fmt.Errorf("failed to send message: %s", resp.Status)
   134  	}
   135  }
   136  
   137  func (n *Notifier) applyDefaults(m *Message) {
   138  	if m.Type == "" {
   139  		m.Type = n.DefaultType
   140  	}
   141  	if m.Tag == "" {
   142  		m.Tag = n.DefaultTag
   143  	}
   144  	if m.Format == "" {
   145  		m.Format = n.DefaultFormat
   146  	}
   147  }
   148  
   149  // SetClient sets a custom http client for accessing the Apprise API.
   150  func (n *Notifier) SetClient(client *http.Client) {
   151  	n.clientLock.Lock()
   152  	defer n.clientLock.Unlock()
   153  
   154  	n.client = client
   155  }
   156  
   157  func (n *Notifier) getClient() *http.Client {
   158  	n.clientLock.Lock()
   159  	defer n.clientLock.Unlock()
   160  
   161  	// Create client if needed.
   162  	if n.client == nil {
   163  		n.client = &http.Client{}
   164  	}
   165  
   166  	return n.client
   167  }