// internal/config/config.go package config import ( "fmt" "os" "path/filepath" "github.com/spf13/viper" ) // Config holds all configuration for the application type Config struct { // General configuration AppName string `mapstructure:"app_name"` ServerPort int `mapstructure:"server_port"` LogLevel string `mapstructure:"log_level"` // Bot configuration Bot struct { KeysFile string `mapstructure:"keys_file"` ContentDir string `mapstructure:"content_dir"` ArchiveDir string `mapstructure:"archive_dir"` DefaultInterval int `mapstructure:"default_interval"` // in minutes } `mapstructure:"bot"` // Database configuration DB struct { Path string `mapstructure:"path"` } `mapstructure:"db"` // Media services configuration Media struct { DefaultService string `mapstructure:"default_service"` // "nip94" or "blossom" // NIP-94 configuration NIP94 struct { ServerURL string `mapstructure:"server_url"` RequireAuth bool `mapstructure:"require_auth"` } `mapstructure:"nip94"` // Blossom configuration Blossom struct { ServerURL string `mapstructure:"server_url"` } `mapstructure:"blossom"` } `mapstructure:"media"` // Default relays Relays []struct { URL string `mapstructure:"url"` Read bool `mapstructure:"read"` Write bool `mapstructure:"write"` } `mapstructure:"relays"` AllowedNpub string `mapstructure:"allowed_npub"` // NEW } // LoadConfig loads the configuration from file or environment variables func LoadConfig(configPath string) (*Config, error) { v := viper.New() // Set defaults v.SetDefault("app_name", "Nostr Poster") v.SetDefault("server_port", 8765) v.SetDefault("log_level", "info") v.SetDefault("bot.keys_file", "keys.json") v.SetDefault("bot.content_dir", "./content") v.SetDefault("bot.archive_dir", "./archive") v.SetDefault("bot.default_interval", 60) // 1 hour v.SetDefault("db.path", "./nostr-poster.db") v.SetDefault("media.default_service", "nip94") v.SetDefault("media.nip94.require_auth", true) // Default relays v.SetDefault("relays", []map[string]interface{}{ {"url": "wss://freelay.sovbit.host", "read": true, "write": true}, }) v.SetDefault("allowed_npub", "") // Setup config file search if configPath != "" { // Use config file from the flag v.SetConfigFile(configPath) } else { // Try to find config in default locations v.AddConfigPath(".") v.AddConfigPath("./config") v.AddConfigPath("$HOME/.nostr-poster") v.SetConfigName("config") } // Read the config file v.SetConfigType("yaml") if err := v.ReadInConfig(); err != nil { if _, ok := err.(viper.ConfigFileNotFoundError); ok { // Config file not found, we'll create a default one fmt.Println("Config file not found, creating default config") defaultConfig := &Config{} v.Unmarshal(defaultConfig) // Ensure directories exist os.MkdirAll(filepath.Dir(v.GetString("db.path")), 0755) os.MkdirAll(v.GetString("bot.content_dir"), 0755) os.MkdirAll(v.GetString("bot.archive_dir"), 0755) // Create default config file if err := v.SafeWriteConfig(); err != nil { return nil, fmt.Errorf("failed to write default config: %w", err) } } else { // Config file was found but another error was produced return nil, fmt.Errorf("error reading config file: %w", err) } } // Environment variables can override config values v.SetEnvPrefix("NOSTR_POSTER") v.AutomaticEnv() // Parse config into struct config := &Config{} if err := v.Unmarshal(config); err != nil { return nil, fmt.Errorf("failed to parse config: %w", err) } return config, nil } // Save writes the current configuration to disk func (c *Config) Save(configPath string) error { v := viper.New() // Convert config struct to map err := v.MergeConfigMap(map[string]interface{}{ "app_name": c.AppName, "server_port": c.ServerPort, "log_level": c.LogLevel, "bot": map[string]interface{}{ "keys_file": c.Bot.KeysFile, "content_dir": c.Bot.ContentDir, "archive_dir": c.Bot.ArchiveDir, "default_interval": c.Bot.DefaultInterval, }, "db": map[string]interface{}{ "path": c.DB.Path, }, "media": map[string]interface{}{ "default_service": c.Media.DefaultService, "nip94": map[string]interface{}{ "server_url": c.Media.NIP94.ServerURL, "require_auth": c.Media.NIP94.RequireAuth, }, "blossom": map[string]interface{}{ "server_url": c.Media.Blossom.ServerURL, }, }, "relays": c.Relays, "allowed_npub": c.AllowedNpub, }) if err != nil { return fmt.Errorf("failed to merge config: %w", err) } // Set the config path v.SetConfigFile(configPath) // Write the config file return v.WriteConfig() }