discord-jukebox-bot/pkg/config/config.go
2025-05-13 11:52:37 +02:00

222 lines
No EOL
6.2 KiB
Go

package config
import (
"fmt"
"os"
"strconv"
"strings"
"time"
"github.com/joho/godotenv"
"log/slog"
)
const (
// Environment variable prefix for the application
envPrefix = "JUKEBOX_"
)
// Config holds all the configuration for the application
type Config struct {
// Discord configuration
DiscordToken string
DiscordGuildID string
DiscordChannelID string
// Subsonic configuration
SubsonicServer string
SubsonicUsername string
SubsonicPassword string
SubsonicVersion string
// Jukebox configuration
AudioVolume float64
TimeoutSec int
}
// Load loads the configuration from environment variables and .env file
func Load() (*Config, error) {
// Try to load .env file
if err := godotenv.Load(); err != nil {
slog.Info("No .env file found, using environment variables only")
} else {
slog.Info("Loaded environment variables from .env file")
}
// Load the raw values first
rawGuildID := getEnv(envPrefix+"DISCORD_GUILD_ID", "")
// Clean up Guild ID - ensure we have just the snowflake
cleanGuildID := cleanSnowflake(rawGuildID)
if cleanGuildID != rawGuildID && rawGuildID != "" {
slog.Warn("Cleaned Discord Guild ID", "original", rawGuildID, "cleaned", cleanGuildID)
}
config := &Config{
// Discord
DiscordToken: getEnv(envPrefix+"DISCORD_TOKEN", ""),
DiscordGuildID: cleanGuildID,
DiscordChannelID: getEnv(envPrefix+"DISCORD_CHANNEL_ID", ""),
// Subsonic
SubsonicServer: getEnv(envPrefix+"SUBSONIC_SERVER", ""),
SubsonicUsername: getEnv(envPrefix+"SUBSONIC_USERNAME", ""),
SubsonicPassword: getEnv(envPrefix+"SUBSONIC_PASSWORD", ""),
SubsonicVersion: getEnv(envPrefix+"SUBSONIC_VERSION", "1.16.1"),
// Jukebox
AudioVolume: getEnvFloat(envPrefix+"AUDIO_VOLUME", 0.5),
TimeoutSec: getEnvInt(envPrefix+"TIMEOUT_SEC", 30),
}
if err := config.validate(); err != nil {
// Print helpful debug information
PrintDebugInfo()
return nil, err
}
return config, nil
}
// validate checks if the required configuration values are set
func (c *Config) validate() error {
if c.DiscordToken == "" {
return errorMissingConfig("discord token")
}
if c.SubsonicServer == "" {
return errorMissingConfig("subsonic server")
}
if c.SubsonicUsername == "" {
return errorMissingConfig("subsonic username")
}
if c.SubsonicPassword == "" {
return errorMissingConfig("subsonic password")
}
return nil
}
// errorMissingConfig returns a formatted error for missing configuration
func errorMissingConfig(name string) error {
return fmt.Errorf("%s is required", name)
}
// GetTimeout returns the timeout duration
func (c *Config) GetTimeout() time.Duration {
return time.Duration(c.TimeoutSec) * time.Second
}
// getEnv gets an environment variable or returns a default value
func getEnv(key, defaultValue string) string {
if value, exists := os.LookupEnv(key); exists {
return value
}
return defaultValue
}
// getEnvInt gets an environment variable as an integer or returns a default value
func getEnvInt(key string, defaultValue int) int {
if value, exists := os.LookupEnv(key); exists {
if intValue, err := strconv.Atoi(value); err == nil {
return intValue
}
}
return defaultValue
}
// getEnvFloat gets an environment variable as a float or returns a default value
func getEnvFloat(key string, defaultValue float64) float64 {
if value, exists := os.LookupEnv(key); exists {
if floatValue, err := strconv.ParseFloat(value, 64); err == nil {
return floatValue
}
}
return defaultValue
}
// PrintDebugInfo prints helpful debugging information for environment variables
func PrintDebugInfo() {
slog.Info("=== Configuration Debug Information ===")
slog.Info("Required environment variables:")
checkEnvVar(envPrefix + "DISCORD_TOKEN")
checkEnvVar(envPrefix + "DISCORD_GUILD_ID")
checkEnvVar(envPrefix + "SUBSONIC_SERVER")
checkEnvVar(envPrefix + "SUBSONIC_USERNAME")
checkEnvVar(envPrefix + "SUBSONIC_PASSWORD")
slog.Info("Optional environment variables:")
checkEnvVar(envPrefix + "DISCORD_CHANNEL_ID")
checkEnvVar(envPrefix + "SUBSONIC_VERSION")
checkEnvVar(envPrefix + "AUDIO_VOLUME")
checkEnvVar(envPrefix + "TIMEOUT_SEC")
slog.Info("Troubleshooting tips:")
slog.Info("1. Your .env file is in the correct directory")
slog.Info("2. All variable names have the JUKEBOX_ prefix")
slog.Info("3. There are no spaces around the = sign in your .env file")
slog.Info(" Example: JUKEBOX_DISCORD_TOKEN=your_token_here")
slog.Info("4. Your .env file has been loaded (check for errors above)")
slog.Info("===================================")
}
// checkEnvVar checks if an environment variable is set and prints its status
func checkEnvVar(name string) {
if value, exists := os.LookupEnv(name); exists {
// Mask sensitive values
displayValue := value
if contains(name, "TOKEN", "PASSWORD") {
if len(displayValue) > 8 {
displayValue = displayValue[:4] + "..." + displayValue[len(displayValue)-4:]
} else {
displayValue = "****"
}
}
// Special handling for Guild ID to detect common issues
if strings.Contains(name, "GUILD_ID") && strings.Contains(value, "=") {
slog.Warn("Environment variable contains equals sign",
"name", name,
"raw_value", displayValue,
"cleaned_value", cleanSnowflake(value))
} else {
slog.Info("Environment variable check", "name", name, "value", displayValue, "status", "set")
}
} else {
slog.Warn("Environment variable not set", "name", name)
}
}
// contains checks if the string contains any of the substrings
func contains(str string, substrings ...string) bool {
for _, substring := range substrings {
if strings.Contains(str, substring) {
return true
}
}
return false
}
// cleanSnowflake cleans a Discord snowflake (ID) from potential contamination
// This handles cases where the ID might include the variable name or other text
func cleanSnowflake(input string) string {
// If the input contains an equals sign, it might be contaminated
if strings.Contains(input, "=") {
parts := strings.SplitN(input, "=", 2)
if len(parts) == 2 {
input = parts[1] // Take the part after the equals sign
}
}
// Extract only digits
var result strings.Builder
for _, char := range input {
if char >= '0' && char <= '9' {
result.WriteRune(char)
}
}
return result.String()
}