Pivot from FastAPI web portal to a Matrix command bot (modeled on Pete).
Users will issue !movie / !tv / !music commands in allowlisted rooms; the
bot performs a top-hit search against Radarr/Sonarr/Lidarr and adds it.
This commit is session 1 of a multi-session rewrite (see SESSION_PLAN.md):
- Delete app/, requirements.txt, old Dockerfile, .env.example
- Add go.mod (mautrix-go + yaml.v3)
- Add internal/config: YAML loader with ${ENV} expansion, validates
matrix creds, allowed_rooms, and per-service *arr config
- Reset .gitignore / .dockerignore for the Go layout
128 lines
3.2 KiB
Go
128 lines
3.2 KiB
Go
package config
|
|
|
|
import (
|
|
"fmt"
|
|
"log/slog"
|
|
"os"
|
|
"regexp"
|
|
|
|
"gopkg.in/yaml.v3"
|
|
)
|
|
|
|
var envBracketRe = regexp.MustCompile(`\$\{([^}]+)\}`)
|
|
|
|
type Config struct {
|
|
Matrix MatrixConfig `yaml:"matrix"`
|
|
Services ServicesConfig `yaml:"services"`
|
|
}
|
|
|
|
type MatrixConfig struct {
|
|
Homeserver string `yaml:"homeserver"`
|
|
UserID string `yaml:"user_id"`
|
|
Password string `yaml:"password"`
|
|
PickleKey string `yaml:"pickle_key"`
|
|
DisplayName string `yaml:"display_name"`
|
|
DataDir string `yaml:"data_dir"`
|
|
CommandPrefix string `yaml:"command_prefix"`
|
|
// AllowedRooms is the set of room IDs the bot listens for commands in.
|
|
// Messages in any other room are ignored.
|
|
AllowedRooms []string `yaml:"allowed_rooms"`
|
|
}
|
|
|
|
type ServicesConfig struct {
|
|
Radarr *ArrConfig `yaml:"radarr"`
|
|
Sonarr *ArrConfig `yaml:"sonarr"`
|
|
Lidarr *ArrConfig `yaml:"lidarr"`
|
|
}
|
|
|
|
type ArrConfig struct {
|
|
URL string `yaml:"url"`
|
|
APIKey string `yaml:"api_key"`
|
|
QualityProfileID int `yaml:"quality_profile_id"`
|
|
RootFolder string `yaml:"root_folder"`
|
|
}
|
|
|
|
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 := yaml.Unmarshal([]byte(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)
|
|
}
|
|
}
|
|
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 = "!"
|
|
}
|
|
}
|