package config import ( "fmt" "log/slog" "os" "regexp" "github.com/BurntSushi/toml" ) var envBracketRe = regexp.MustCompile(`\$\{([^}]+)\}`) type Config struct { Matrix MatrixConfig `toml:"matrix"` Services ServicesConfig `toml:"services"` } type MatrixConfig struct { Homeserver string `toml:"homeserver"` UserID string `toml:"user_id"` Password string `toml:"password"` PickleKey string `toml:"pickle_key"` DisplayName string `toml:"display_name"` DataDir string `toml:"data_dir"` CommandPrefix string `toml:"command_prefix"` // AllowedRooms is the set of room IDs the bot listens for commands in. // Messages in any other room are ignored. AllowedRooms []string `toml:"allowed_rooms"` } type ServicesConfig struct { Radarr *ArrConfig `toml:"radarr"` Sonarr *ArrConfig `toml:"sonarr"` Lidarr *ArrConfig `toml:"lidarr"` } type ArrConfig struct { URL string `toml:"url"` APIKey string `toml:"api_key"` QualityProfileID int `toml:"quality_profile_id"` RootFolder string `toml:"root_folder"` // MetadataProfileID is required by Lidarr; ignored by Radarr/Sonarr. MetadataProfileID int `toml:"metadata_profile_id"` } func Load(path string) (*Config, error) { data, err := os.ReadFile(path) if err != nil { return nil, fmt.Errorf("read config: %w", err) } expanded := envBracketRe.ReplaceAllStringFunc(string(data), func(match string) string { varName := match[2 : len(match)-1] val := os.Getenv(varName) if val == "" { slog.Warn("config: env var referenced but not set", "var", varName) } return val }) var cfg Config if _, err := toml.Decode(expanded, &cfg); err != nil { return nil, fmt.Errorf("parse config: %w", err) } cfg.applyDefaults() if err := cfg.validate(); err != nil { return nil, fmt.Errorf("validate config: %w", err) } return &cfg, nil } func (c *Config) validate() error { if c.Matrix.Homeserver == "" { return fmt.Errorf("matrix.homeserver is required") } if c.Matrix.UserID == "" { return fmt.Errorf("matrix.user_id is required") } if c.Matrix.Password == "" { return fmt.Errorf("matrix.password is required") } if len(c.Matrix.AllowedRooms) == 0 { return fmt.Errorf("matrix.allowed_rooms must have at least one entry") } if c.Services.Radarr == nil && c.Services.Sonarr == nil && c.Services.Lidarr == nil { return fmt.Errorf("at least one of services.radarr/sonarr/lidarr must be configured") } for name, svc := range map[string]*ArrConfig{ "radarr": c.Services.Radarr, "sonarr": c.Services.Sonarr, "lidarr": c.Services.Lidarr, } { if svc == nil { continue } if svc.URL == "" { return fmt.Errorf("services.%s.url is required when service is set", name) } if svc.APIKey == "" { return fmt.Errorf("services.%s.api_key is required when service is set", name) } if svc.RootFolder == "" { return fmt.Errorf("services.%s.root_folder is required when service is set", name) } if svc.QualityProfileID == 0 { return fmt.Errorf("services.%s.quality_profile_id is required when service is set", name) } } if c.Services.Lidarr != nil && c.Services.Lidarr.MetadataProfileID == 0 { return fmt.Errorf("services.lidarr.metadata_profile_id is required when lidarr is set") } return nil } func (c *Config) applyDefaults() { if c.Matrix.DataDir == "" { c.Matrix.DataDir = "./data" } if c.Matrix.DisplayName == "" { c.Matrix.DisplayName = "Bellhop" } if c.Matrix.PickleKey == "" { c.Matrix.PickleKey = "bellhop_pickle_key" } if c.Matrix.CommandPrefix == "" { c.Matrix.CommandPrefix = "!" } }