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
|
package gotify
import (
"encoding/json"
"fmt"
"log"
"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"
)
// Scheme identifies this service in configuration URLs.
const (
Scheme = "gotify"
)
// Config holds settings for the Gotify notification service.
// This struct contains all configuration parameters needed to connect to and authenticate
// with a Gotify server, including connection details, authentication credentials,
// notification defaults, and additional metadata.
type Config struct {
standard.EnumlessConfig // Embeds standard configuration functionality without enum handling
Token string `desc:"Application token" required:"" url:"path2"` // Gotify application token for authentication (must be 15 chars starting with 'A')
Host string `desc:"Server hostname (and optionally port)" required:"" url:"host,port"` // Gotify server hostname and optional port number
Path string `desc:"Server subpath" url:"path1" optional:""` // Optional subpath for Gotify installation (e.g., "/gotify")
Priority int ` default:"0" key:"priority"` // Notification priority level (-2 to 10, where higher numbers are more important; negative values have special meanings in some clients)
Title string ` default:"Shoutrrr notification" key:"title"` // Default notification title when none provided
DisableTLS bool ` default:"No" key:"disabletls"` // Disable TLS in URL scheme only (use HTTP instead of HTTPS)
InsecureSkipVerify bool ` default:"No" key:"insecureskipverify"` // Skip TLS certificate verification (insecure, use with caution)
UseHeader bool `desc:"Enable header-based authentication" default:"No" key:"useheader"` // Send token in X-Gotify-Key header instead of URL query parameter
Date string ` default:"" key:"date"` // Optional custom timestamp in ISO 8601 format for the notification
Extras map[string]any // Additional extras parsed from JSON - custom key-value pairs sent with notifications
}
// SetURL updates the configuration from a URL representation.
func (config *Config) SetURL(url *url.URL) error {
resolver := format.NewPropKeyResolver(config)
return config.setURL(&resolver, url)
}
// GetURL returns a URL representation of the current configuration.
func (config *Config) GetURL() *url.URL {
resolver := format.NewPropKeyResolver(config)
return config.getURL(&resolver)
}
// getURL generates a URL from the current configuration values.
// This internal method constructs a URL representation of the configuration,
// including all settings as query parameters and extras as JSON in the query string.
// Used for serialization and URL reconstruction.
// Parameters:
// - resolver: Configuration resolver for building query parameters from config fields
//
// Returns: *url.URL containing the complete configuration as a URL.
func (config *Config) getURL(resolver types.ConfigQueryResolver) *url.URL {
// Build base query string from configuration fields using the resolver
query := format.BuildQuery(resolver)
// Handle extras serialization if present
if config.Extras != nil {
// Marshal extras map to JSON string
extrasJSON, err := json.Marshal(config.Extras)
if err != nil {
// Skip adding extras when Extras cannot be serialized
log.Printf("Failed to marshal Extras %v: %v, skipping extras", config.Extras, err)
} else {
// Append extras to query string with proper URL encoding
if query != "" {
query += "&"
}
query += "extras=" + url.QueryEscape(string(extrasJSON))
}
}
// Construct and return the complete URL
return &url.URL{
Host: config.Host, // Server hostname and port
Scheme: Scheme, // URL scheme (gotify)
ForceQuery: false, // Don't force query string presence
Path: config.Path + config.Token, // Path with token appended
RawQuery: query, // Query parameters including extras
}
}
// setURL updates the configuration from a URL representation.
// This internal method parses a URL and extracts configuration values including
// host, path, token, and query parameters, populating the config struct fields.
// Used for deserialization from URL format.
// Parameters:
// - resolver: Configuration resolver for setting config fields from query parameters
// - url: The URL to parse configuration values from
//
// Returns: error if URL parsing or parameter processing fails.
func (config *Config) setURL(resolver types.ConfigQueryResolver, url *url.URL) error {
// Extract and clean the path from the URL
path := url.Path
if len(path) > 0 && path[len(path)-1] == '/' {
path = path[:len(path)-1] // Remove trailing slash if present
}
// Find the last slash to separate path from token
tokenIndex := strings.LastIndex(path, "/") + 1
// Extract path component (everything before the token)
config.Path = path[:tokenIndex]
if config.Path == "/" {
config.Path = config.Path[1:] // Remove leading slash to normalize empty path
}
// Set host and token from URL components
config.Host = url.Host
config.Token = path[tokenIndex:]
// Process query parameters to set remaining configuration fields
if err := config.processQueryParameters(resolver, url.Query()); err != nil {
return fmt.Errorf("failed to process query parameters: %w", err)
}
return nil
}
// processQueryParameters processes query parameters from URL.
// This function handles both standard configuration parameters and the special 'extras'
// JSON parameter, setting appropriate config fields through the resolver or direct assignment.
// Parameters:
// - resolver: Configuration resolver for setting standard config properties
// - query: URL query parameters to process
//
// Returns: error if parameter parsing or setting fails.
func (config *Config) processQueryParameters(
resolver types.ConfigQueryResolver,
query url.Values,
) error {
// Iterate through all query parameters
for key := range query {
if key == "extras" {
// Special handling for extras JSON parameter
if query.Get(key) != "" {
// Initialize extras map
config.Extras = make(map[string]any)
// Parse JSON string into map
if err := json.Unmarshal([]byte(query.Get(key)), &config.Extras); err != nil {
return fmt.Errorf("%w", ErrExtrasParseFailed)
}
}
} else {
// Standard parameter handling through resolver
if err := resolver.Set(key, query.Get(key)); err != nil {
return fmt.Errorf("%w", ErrConfigPropertyFailed)
}
}
}
return nil
}
|