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 }