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 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238
|
package gotify
import (
"fmt"
"net/http"
"net/url"
"sync"
"github.com/nicholas-fedor/shoutrrr/pkg/format"
"github.com/nicholas-fedor/shoutrrr/pkg/services/standard"
"github.com/nicholas-fedor/shoutrrr/pkg/types"
"github.com/nicholas-fedor/shoutrrr/pkg/util/jsonclient"
)
// Service implements a Gotify notification service that handles sending push notifications
// to Gotify servers. It manages HTTP client configuration, TLS settings, authentication,
// and payload construction for reliable message delivery.
type Service struct {
standard.Standard // Embeds the standard service functionality including logging
Config *Config // Holds the configuration settings for the Gotify service, including host, token, and other parameters
pkr format.PropKeyResolver // Property key resolver used to update configuration from URL parameters dynamically
mu sync.Mutex // Protects HTTP client initialization for thread safety
httpClient *http.Client // HTTP client instance configured with appropriate timeout and transport settings for API calls
client jsonclient.Client // JSON client wrapper that handles JSON request/response marshaling and HTTP communication
// Interface dependencies (injected during initialization)
httpClientManager HTTPClientManager
urlBuilder URLBuilder
payloadBuilder PayloadBuilder
validator Validator
sender Sender
}
// Initialize configures the service with a URL and logger.
// This method sets up the entire service infrastructure including configuration parsing,
// HTTP client creation with appropriate TLS settings, and logging capabilities.
// Parameters:
// - configURL: The URL containing Gotify server configuration (host, token, path, etc.)
// - logger: Logger instance for recording service operations and warnings
//
// Returns: error if configuration parsing or setup fails, nil on success.
func (service *Service) Initialize(configURL *url.URL, logger types.StdLogger) error {
// Set the logger for this service instance to enable logging throughout the service lifecycle
service.SetLogger(logger)
// Initialize the configuration with default values
service.Config = &Config{
Title: "Shoutrrr notification", // Default notification title used when none specified
}
// Create a property key resolver to handle dynamic configuration updates from parameters
service.pkr = format.NewPropKeyResolver(service.Config)
// Parse the configuration URL to extract host, token, path, and other settings
err := service.Config.SetURL(configURL)
if err != nil {
return fmt.Errorf("failed to set URL: %w", err)
}
// Inject default implementations for interfaces
service.httpClientManager = &DefaultHTTPClientManager{}
service.urlBuilder = &DefaultURLBuilder{}
service.payloadBuilder = &DefaultPayloadBuilder{}
service.validator = &DefaultValidator{}
service.sender = &DefaultSender{}
// Initialize HTTP client and related components in a thread-safe manner
service.initClient()
return nil // Return success
}
// GetID returns the identifier for this service.
func (service *Service) GetID() string {
return Scheme
}
// GetHTTPClient returns the HTTP client used by this service.
// This method implements the MockClientService interface for testing.
func (service *Service) GetHTTPClient() *http.Client {
return service.httpClient
}
// Send delivers a notification message to Gotify.
// This is the main entry point for sending notifications. It handles message validation,
// parameter processing, configuration updates, URL construction, authentication setup,
// and HTTP request execution.
// Parameters:
// - message: The notification message content to send (cannot be empty)
// - params: Optional parameters that can override configuration settings or provide extras
//
// Returns: error if sending fails or validation fails, nil on successful delivery.
func (service *Service) Send(message string, params *types.Params) error {
if err := service.validateInputs(message, params); err != nil {
return fmt.Errorf("input validation failed: %w", err)
}
service.initClient()
config, extras, err := service.processConfig(params)
if err != nil {
return fmt.Errorf("failed to process config: %w", err)
}
postURL, request, headers, err := service.buildRequest(message, &config, extras)
if err != nil {
return fmt.Errorf("failed to build request: %w", err)
}
return service.sendRequest(postURL, request, headers)
}
// validateInputs performs initial validation checks for the Send method.
func (service *Service) validateInputs(message string, _ *types.Params) error {
if err := service.validator.ValidateMessage(message); err != nil {
return fmt.Errorf("message validation failed: %w", err)
}
if err := service.validator.ValidateServiceInitialized(service.Config); err != nil {
return fmt.Errorf("service initialization validation failed: %w", err)
}
return nil
}
// processConfig handles configuration processing including parameter updates, validation, and extras parsing.
func (service *Service) processConfig(params *types.Params) (Config, map[string]any, error) {
// Get reference to current configuration
config := *service.Config
// Filter out 'extras' parameter as it's handled separately from other config updates
filteredParams := filterParams(params)
// Update configuration with filtered parameters (title, priority, etc.)
if err := service.pkr.UpdateConfigFromParams(&config, &filteredParams); err != nil {
return config, nil, fmt.Errorf("failed to update config from params: %w", err)
}
// Validate priority is within valid range (-2 to 10)
if err := service.validator.ValidatePriority(config.Priority); err != nil {
return config, nil, fmt.Errorf("priority validation failed: %w", err)
}
// Validate and convert date format
validatedDate, err := service.validator.ValidateDate(config.Date)
if err != nil {
service.Logf("Invalid date format: %v", err)
config.Date = ""
} else {
config.Date = validatedDate
}
// Parse extras from parameters or fall back to config extras
extras, err := service.payloadBuilder.ParseExtras(params, &config)
if err != nil {
service.Logf("Failed to parse extras from params: %v", err)
extras = config.Extras
}
return config, extras, nil
}
// buildRequest constructs the URL, payload, and headers for the HTTP request.
func (service *Service) buildRequest(
message string,
config *Config,
extras map[string]any,
) (string, *MessageRequest, http.Header, error) {
// Validate token format before constructing URL
if !service.validator.ValidateToken(config.Token) {
return "", nil, nil, fmt.Errorf("%w: %q", ErrInvalidToken, config.Token)
}
// Construct the complete API endpoint URL
postURL, err := service.urlBuilder.BuildURL(config)
if err != nil {
return "", nil, nil, fmt.Errorf("failed to build URL: %w", err)
}
// Prepare the JSON request payload
request := service.payloadBuilder.PrepareRequest(message, config, extras, config.Date)
// Prepare headers for header-based authentication
var headers http.Header
if config.UseHeader {
headers = make(http.Header)
headers.Set("X-Gotify-Key", config.Token)
}
return postURL, request, headers, nil
}
// filterParams filters out 'extras' parameters from the given params.
func filterParams(params *types.Params) types.Params {
if params == nil {
return types.Params{}
}
filtered := make(types.Params)
for k, v := range *params {
if k != "extras" {
filtered[k] = v
}
}
return filtered
}
// initClient initializes the HTTP client and related components.
// This method ensures that the transport, HTTP client, JSON client,
// and TLS warning logging are performed when needed, allowing re-initialization if the client becomes nil.
func (service *Service) initClient() {
initClient(service, service.httpClientManager)
}
// sendRequest handles the HTTP request.
// This function executes the actual HTTP POST request to the Gotify API endpoint,
// handling both successful responses and error conditions with appropriate error wrapping.
// Parameters:
// - postURL: The complete API endpoint URL to send the request to
// - request: The JSON payload to send in the request body
// - headers: Optional headers to set on the request
//
// Returns: error if the request fails or server returns an error, nil on success.
func (service *Service) sendRequest(
postURL string,
request *MessageRequest,
headers http.Header,
) error {
if err := service.sender.SendRequest(service.httpClient, postURL, request, headers); err != nil {
return fmt.Errorf("%s: %w", ErrSendFailed.Error(), err)
}
return nil
}
|