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 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181
|
package generic
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"net/url"
"strings"
"github.com/nicholas-fedor/shoutrrr/pkg/format"
"github.com/nicholas-fedor/shoutrrr/pkg/services/standard"
"github.com/nicholas-fedor/shoutrrr/pkg/types"
)
// JSONTemplate identifies the JSON format for webhook payloads.
const (
JSONTemplate = "JSON"
)
// ErrSendFailed indicates a failure to send a notification to the generic webhook.
var (
ErrSendFailed = errors.New("failed to send notification to generic webhook")
ErrUnexpectedStatus = errors.New("server returned unexpected response status code")
ErrTemplateNotLoaded = errors.New("template has not been loaded")
)
// Service implements a generic notification service for custom webhooks.
type Service struct {
standard.Standard
Config *Config
pkr format.PropKeyResolver
}
// Send delivers a notification message to a generic webhook endpoint.
func (service *Service) Send(message string, paramsPtr *types.Params) error {
config := *service.Config
var params types.Params
if paramsPtr == nil {
params = types.Params{}
} else {
params = *paramsPtr
}
if err := service.pkr.UpdateConfigFromParams(&config, ¶ms); err != nil {
service.Logf("Failed to update params: %v", err)
}
sendParams := createSendParams(&config, params, message)
if err := service.doSend(&config, sendParams); err != nil {
return fmt.Errorf("%w: %s", ErrSendFailed, err.Error())
}
return nil
}
// Initialize configures the service with a URL and logger.
func (service *Service) Initialize(configURL *url.URL, logger types.StdLogger) error {
service.SetLogger(logger)
config, pkr := DefaultConfig()
service.Config = config
service.pkr = pkr
return service.Config.setURL(&service.pkr, configURL)
}
// GetID returns the identifier for this service.
func (service *Service) GetID() string {
return Scheme
}
// GetConfigURLFromCustom converts a custom webhook URL into a standard service URL.
func (*Service) GetConfigURLFromCustom(customURL *url.URL) (*url.URL, error) {
webhookURL := *customURL
if strings.HasPrefix(webhookURL.Scheme, Scheme) {
webhookURL.Scheme = webhookURL.Scheme[len(Scheme)+1:]
}
config, pkr, err := ConfigFromWebhookURL(webhookURL)
if err != nil {
return nil, err
}
return config.getURL(&pkr), nil
}
// doSend executes the HTTP request to send a notification to the webhook.
func (service *Service) doSend(config *Config, params types.Params) error {
postURL := config.WebhookURL().String()
payload, err := service.GetPayload(config, params)
if err != nil {
return err
}
ctx := context.Background()
req, err := http.NewRequestWithContext(ctx, config.RequestMethod, postURL, payload)
if err != nil {
return fmt.Errorf("creating HTTP request: %w", err)
}
req.Header.Set("Content-Type", config.ContentType)
req.Header.Set("Accept", config.ContentType)
for key, value := range config.headers {
req.Header.Set(key, value)
}
res, err := http.DefaultClient.Do(req)
if err != nil {
return fmt.Errorf("sending HTTP request: %w", err)
}
if res != nil && res.Body != nil {
defer res.Body.Close()
if body, err := io.ReadAll(res.Body); err == nil {
service.Log("Server response: ", string(body))
}
}
if res.StatusCode >= http.StatusMultipleChoices {
return fmt.Errorf("%w: %s", ErrUnexpectedStatus, res.Status)
}
return nil
}
// GetPayload prepares the request payload based on the configured template.
func (service *Service) GetPayload(config *Config, params types.Params) (io.Reader, error) {
switch config.Template {
case "":
return bytes.NewBufferString(params[config.MessageKey]), nil
case "json", JSONTemplate:
for key, value := range config.extraData {
params[key] = value
}
jsonBytes, err := json.Marshal(params)
if err != nil {
return nil, fmt.Errorf("marshaling params to JSON: %w", err)
}
return bytes.NewBuffer(jsonBytes), nil
}
tpl, found := service.GetTemplate(config.Template)
if !found {
return nil, fmt.Errorf("%w: %q", ErrTemplateNotLoaded, config.Template)
}
bb := &bytes.Buffer{}
if err := tpl.Execute(bb, params); err != nil {
return nil, fmt.Errorf("executing template %q: %w", config.Template, err)
}
return bb, nil
}
// createSendParams constructs parameters for sending a notification.
func createSendParams(config *Config, params types.Params, message string) types.Params {
sendParams := types.Params{}
for key, val := range params {
if key == types.TitleKey {
key = config.TitleKey
}
sendParams[key] = val
}
sendParams[config.MessageKey] = message
return sendParams
}
|