musiclink/pkg/config/config.go

87 lines
2.5 KiB
Go

// Package config handles configuration loading and validation.
package config
import (
"fmt"
"net"
"os"
"strings"
"github.com/BurntSushi/toml"
)
// Config holds the complete application configuration.
type Config struct {
Matrix MatrixConfig `toml:"matrix"`
}
// MatrixConfig holds Matrix-native bot settings.
type MatrixConfig struct {
Shadow bool `toml:"shadow"` // Shadow mode (log only)
HealthAddr string `toml:"healthAddr"` // Health server listen address
Server string `toml:"server"` // Homeserver base URL
AccessToken string `toml:"accessToken"` // Access token for bot user
UserID string `toml:"userId"` // Full Matrix user ID
Rooms []string `toml:"rooms"` // Allowlisted room IDs
StateStorePath string `toml:"stateStorePath"` // Path to state store (sync token/dedupe)
}
// Load reads and parses a TOML configuration file.
func Load(path string) (*Config, error) {
data, err := os.ReadFile(path)
if err != nil {
return nil, fmt.Errorf("reading config file: %w", err)
}
var cfg Config
if err := toml.Unmarshal(data, &cfg); err != nil {
return nil, fmt.Errorf("parsing config file: %w", err)
}
if err := cfg.Validate(); err != nil {
return nil, fmt.Errorf("validating config: %w", err)
}
return &cfg, nil
}
// Validate checks that required configuration fields are set.
func (c *Config) Validate() error {
if c.Matrix.Server == "" {
return fmt.Errorf("matrix.server is required")
}
if c.Matrix.AccessToken == "" {
return fmt.Errorf("matrix.accessToken is required")
}
if c.Matrix.UserID == "" {
return fmt.Errorf("matrix.userId is required")
}
if len(c.Matrix.Rooms) == 0 {
return fmt.Errorf("matrix.rooms must include at least one room")
}
if c.Matrix.StateStorePath == "" {
c.Matrix.StateStorePath = "data/matrix-state.db"
}
if c.Matrix.HealthAddr != "" {
if err := validateLocalHealthAddr(c.Matrix.HealthAddr); err != nil {
return err
}
}
return nil
}
func validateLocalHealthAddr(addr string) error {
host, _, err := net.SplitHostPort(addr)
if err != nil {
return fmt.Errorf("matrix.healthAddr must be host:port: %w", err)
}
if host == "" {
return fmt.Errorf("matrix.healthAddr must bind to localhost (use 127.0.0.1:PORT)")
}
normalized := strings.ToLower(host)
if normalized == "localhost" || normalized == "127.0.0.1" || normalized == "::1" {
return nil
}
return fmt.Errorf("matrix.healthAddr must bind to localhost (got %s)", host)
}