package config import ( "fmt" "os" "sync/atomic" "gopkg.in/yaml.v3" ) type ButtonColor string const ( ColorPrimary ButtonColor = "primary" ColorSecondary ButtonColor = "secondary" ColorSuccess ButtonColor = "success" ColorDanger ButtonColor = "danger" ) type TicketType struct { ButtonLabel string `yaml:"button_label"` ButtonColor ButtonColor `yaml:"button_color"` ButtonEmoji string `yaml:"button_emoji"` EmbedColor string `yaml:"embed_color"` EmbedTitle string `yaml:"embed_title"` EmbedText string `yaml:"embed_text"` StaffRole string `yaml:"staff_role"` Category string `yaml:"category"` } type Panel struct { EmbedTitle string `yaml:"embed_title"` EmbedDescription string `yaml:"embed_description"` EmbedColor string `yaml:"embed_color"` Types map[string]TicketType `yaml:"types"` } type Bot struct { AdminRole string `yaml:"admin_role"` LogsChannel string `yaml:"logs_channel"` ClaimChannel string `yaml:"claim_channel"` ConvocationCategory string `yaml:"convocation_category"` ClaimReupMinutes int `yaml:"claim_reup_minutes"` AllowedGuilds []string `yaml:"allowed_guilds"` // whitelist; empty = use GUILD_ID env only } type PanelWebConfig struct { Port int `yaml:"port"` BaseURL string `yaml:"base_url"` OAuthClientID string `yaml:"oauth_client_id"` OAuthClientSecret string `yaml:"oauth_client_secret"` SessionTimeoutMinutes int `yaml:"session_timeout_minutes"` } type Config struct { Bot Bot `yaml:"bot"` Panel PanelWebConfig `yaml:"panel"` Panels map[string]Panel `yaml:"panels"` } func (c *Config) Validate() error { if c.Bot.AdminRole == "" { return fmt.Errorf("bot.admin_role is required") } if c.Bot.ClaimReupMinutes <= 0 { c.Bot.ClaimReupMinutes = 30 } if c.Panel.Port <= 0 { c.Panel.Port = 8080 } if c.Panel.SessionTimeoutMinutes <= 0 { c.Panel.SessionTimeoutMinutes = 30 } return nil } 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 := yaml.Unmarshal(data, &cfg); err != nil { return nil, fmt.Errorf("parse config: %w", err) } if err := cfg.Validate(); err != nil { return nil, err } return &cfg, nil } // Provider holds the current config and allows atomic swaps. type Provider struct { ptr atomic.Pointer[Config] } func NewProvider(cfg *Config) *Provider { p := &Provider{} p.ptr.Store(cfg) return p } func (p *Provider) Get() *Config { return p.ptr.Load() } func (p *Provider) Swap(cfg *Config) { p.ptr.Store(cfg) }