Files
Bellhop/internal/config/config.go
prosolis 8f38d3d9f4 Switch config format from YAML to TOML
Replaces gopkg.in/yaml.v3 with github.com/BurntSushi/toml. Updates
struct tags, default config path, Dockerfile CMD, README, and ships
config.example.toml in place of the YAML example. ${ENV_VAR}
expansion still runs on the raw file before parsing, so the behavior
is unchanged.
2026-05-24 21:05:41 -07:00

133 lines
3.5 KiB
Go

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 = "!"
}
}