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 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216
|
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 {
// Create a copy of the config to avoid modifying the original
config := *service.Config
var params types.Params
if paramsPtr == nil {
// Handle nil params by creating empty map
params = types.Params{}
} else {
params = *paramsPtr
}
if err := service.pkr.UpdateConfigFromParams(&config, ¶ms); err != nil {
// Update config with runtime parameters
service.Logf("Failed to update params: %v", err)
}
// Prepare parameters for sending
sendParams := createSendParams(&config, params, message)
if err := service.doSend(&config, sendParams); err != nil {
// Execute the HTTP request to send the notification
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 {
// Set the logger for the service
service.SetLogger(logger)
// Get default config and property key resolver
config, pkr := DefaultConfig()
// Assign config to service
service.Config = config
// Assign resolver
service.pkr = pkr
// Set URL and return any error
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) {
// Copy the URL to modify
webhookURL := *customURL
if strings.HasPrefix(webhookURL.Scheme, Scheme) {
// Remove the scheme prefix if present
webhookURL.Scheme = webhookURL.Scheme[len(Scheme)+1:]
}
// Parse config from webhook URL
config, pkr, err := ConfigFromWebhookURL(webhookURL)
if err != nil {
return nil, err
}
// Generate and return the service URL
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 {
// Get the webhook URL as string
postURL := config.WebhookURL().String()
// Prepare the request payload
payload, err := service.GetPayload(config, params)
if err != nil {
return err
}
// Create background context for the request
ctx := context.Background()
// Create HTTP request with context
req, err := http.NewRequestWithContext(ctx, config.RequestMethod, postURL, payload)
if err != nil {
return fmt.Errorf("creating HTTP request: %w", err)
}
// Set content type header
req.Header.Set("Content-Type", config.ContentType)
// Set accept header
req.Header.Set("Accept", config.ContentType)
for key, value := range config.headers {
// Add custom headers
req.Header.Set(key, value)
}
// Send the HTTP request
res, err := http.DefaultClient.Do(req)
if err != nil {
return fmt.Errorf("sending HTTP request: %w", err)
}
if res != nil && res.Body != nil {
// Read and log response body if available
defer func() {
_ = res.Body.Close()
}()
if body, err := io.ReadAll(res.Body); err == nil {
service.Log("Server response: ", string(body))
}
}
if res.StatusCode >= http.StatusMultipleChoices {
// Check for error status codes
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 "":
// No template, send message directly
return bytes.NewBufferString(params[config.MessageKey]), nil
case "json", JSONTemplate:
// JSON template, marshal params to JSON
for key, value := range config.extraData {
// Add extra data to params
params[key] = value
}
// Marshal to JSON
jsonBytes, err := json.Marshal(params)
if err != nil {
return nil, fmt.Errorf("marshaling params to JSON: %w", err)
}
return bytes.NewBuffer(jsonBytes), nil
}
// Get the template
tpl, found := service.GetTemplate(config.Template)
if !found {
return nil, fmt.Errorf("%w: %q", ErrTemplateNotLoaded, config.Template)
}
// Buffer for template execution
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 {
// Initialize new params map
sendParams := types.Params{}
for key, val := range params {
// Copy params, mapping title key if necessary
if key == types.TitleKey {
key = config.TitleKey
}
sendParams[key] = val
}
// Add the message
sendParams[config.MessageKey] = message
return sendParams
}
|