package config import ( "fmt" "os" "regexp" "github.com/BurntSushi/toml" ) var timeFormatRe = regexp.MustCompile(`^\d{2}:\d{2}$`) type Config struct { Memos MemosConfig `toml:"memos"` Database DatabaseConfig `toml:"database"` Search SearchConfig `toml:"search"` Telegram TelegramConfig `toml:"telegram"` Web WebConfig `toml:"web"` General GeneralConfig `toml:"general"` } type MemosConfig struct { URL string `toml:"url"` Token string `toml:"token"` PublicURL string `toml:"public_url"` } type DatabaseConfig struct { Path string `toml:"path"` } type SearchConfig struct { CooldownDays int `toml:"cooldown_days"` RelaxedCooldownDays int `toml:"relaxed_cooldown_days"` PageSize int `toml:"page_size"` MaxYearsBack int `toml:"max_years_back"` PreferOlder bool `toml:"prefer_older"` TierWeights TierWeights `toml:"tier_weights"` } type TierWeights struct { Tier1 int `toml:"tier1"` Tier2 int `toml:"tier2"` Tier3 int `toml:"tier3"` Tier4 int `toml:"tier4"` Tier5 int `toml:"tier5"` Tier6 int `toml:"tier6"` Tier7 int `toml:"tier7"` } func (tw TierWeights) Sum() int { return tw.Tier1 + tw.Tier2 + tw.Tier3 + tw.Tier4 + tw.Tier5 + tw.Tier6 + tw.Tier7 } func (tw TierWeights) AsSlice() [7]int { return [7]int{tw.Tier1, tw.Tier2, tw.Tier3, tw.Tier4, tw.Tier5, tw.Tier6, tw.Tier7} } type TelegramConfig struct { Enabled bool `toml:"enabled"` Token string `toml:"token"` ChatID int64 `toml:"chat_id"` SendAt string `toml:"send_at"` } type WebConfig struct { Listen string `toml:"listen"` } type GeneralConfig struct { Timezone string `toml:"timezone"` LogLevel string `toml:"log_level"` AllowLoadMore bool `toml:"allow_load_more"` } func Load(path string) (*Config, error) { data, err := os.ReadFile(path) if err != nil { return nil, fmt.Errorf("read config: %w", err) } var cfg Config if err := toml.Unmarshal(data, &cfg); err != nil { return nil, fmt.Errorf("parse config: %w", err) } setDefaults(&cfg) if err := validate(&cfg); err != nil { return nil, fmt.Errorf("validate config: %w", err) } return &cfg, nil } func setDefaults(cfg *Config) { if cfg.Database.Path == "" { cfg.Database.Path = "remembos.db" } if cfg.Search.CooldownDays == 0 { cfg.Search.CooldownDays = 90 } if cfg.Search.RelaxedCooldownDays == 0 { cfg.Search.RelaxedCooldownDays = 30 } if cfg.Search.PageSize == 0 { cfg.Search.PageSize = 50 } if cfg.Search.MaxYearsBack == 0 { cfg.Search.MaxYearsBack = 10 } if cfg.Search.TierWeights.Sum() == 0 { cfg.Search.TierWeights = TierWeights{ Tier1: 35, Tier2: 15, Tier3: 15, Tier4: 12, Tier5: 10, Tier6: 5, Tier7: 8, } } if cfg.Web.Listen == "" { cfg.Web.Listen = "127.0.0.1:8080" } if cfg.General.Timezone == "" { cfg.General.Timezone = "Europe/Moscow" } if cfg.General.LogLevel == "" { cfg.General.LogLevel = "info" } if cfg.Telegram.SendAt == "" { cfg.Telegram.SendAt = "09:00" } } func validate(cfg *Config) error { if cfg.Memos.URL == "" { return fmt.Errorf("memos.url is required") } if cfg.Memos.Token == "" { return fmt.Errorf("memos.token is required") } if sum := cfg.Search.TierWeights.Sum(); sum != 100 { return fmt.Errorf("search.tier_weights must sum to 100, got %d", sum) } if cfg.Telegram.Enabled { if cfg.Telegram.Token == "" { return fmt.Errorf("telegram.token is required when telegram.enabled = true") } if cfg.Telegram.ChatID == 0 { return fmt.Errorf("telegram.chat_id is required when telegram.enabled = true") } if !timeFormatRe.MatchString(cfg.Telegram.SendAt) { return fmt.Errorf("telegram.send_at must be in HH:MM format, got %q", cfg.Telegram.SendAt) } } return nil }