// 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) }