package main import ( "context" "os" "os/signal" "syscall" "time" "github.com/bwmarrin/discordgo" "github.com/joho/godotenv" discordadapter "gitea.nik4nao.com/nik/home-services/discord-bot/internal/adapters/primary/discord" "gitea.nik4nao.com/nik/home-services/discord-bot/internal/adapters/secondary/gateway" "gitea.nik4nao.com/nik/home-services/discord-bot/internal/app" "gitea.nik4nao.com/nik/home-services/discord-bot/internal/config" "gitea.nik4nao.com/nik/home-services/discord-bot/internal/logger" "gitea.nik4nao.com/nik/home-services/discord-bot/internal/telemetry" ) var version = "dev" func main() { _ = godotenv.Load() // Config is loaded before logger setup so fatal startup errors still stop early. cfg, err := config.Load() if err != nil { os.Stderr.WriteString("config error: " + err.Error() + "\n") os.Exit(1) } log := logger.New(cfg.LogFormat, cfg.LogLevel) log.Info("starting discord-bot", "version", version, "ha_gateway_addr", cfg.HAGatewayAddr, "discord_token", redactToken(cfg.DiscordToken), "log_level", cfg.LogLevel, "log_format", cfg.LogFormat, ) ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM) defer stop() ctx = logger.WithLogger(ctx, log) // Telemetry is optional; an empty OTEL endpoint installs no-op providers. shutdown, err := telemetry.Setup(ctx, "discord-bot", version, cfg) if err != nil { log.Error("telemetry setup failed", "err", err) os.Exit(1) } if cfg.OTELEndpoint != "" { log.Info("telemetry enabled", "endpoint", cfg.OTELEndpoint) } else { log.Debug("telemetry disabled") } haClient, err := gateway.New(ctx, cfg.HAGatewayAddr, log) if err != nil { log.Error("ha-gateway client setup failed", "err", err) os.Exit(1) } defer func() { if err := haClient.Close(); err != nil { log.Error("ha-gateway client close failed", "err", err) } }() commandApp := app.NewCommandApp(haClient) // Discord-specific wiring stays at the edge so the app layer remains transport-agnostic. session, err := discordgo.New("Bot " + cfg.DiscordToken) if err != nil { log.Error("create discord session failed", "err", err) os.Exit(1) } session.Identify.Intents = discordgo.IntentsGuilds handler := discordadapter.NewHandler(commandApp) handler.Register(session) if err := session.Open(); err != nil { log.Error("open discord session failed", "err", err) os.Exit(1) } log.Info("discord session ready", "bot_user", session.State.User.Username) if err := discordadapter.RegisterCommands(session, cfg.GuildID); err != nil { log.Error("register discord commands failed", "err", err) os.Exit(1) } scope := "global" if cfg.GuildID != "" { scope = cfg.GuildID } log.Info("discord bot started", "command_scope", scope, "ha_gateway_addr", cfg.HAGatewayAddr, "version", version) <-ctx.Done() log.Info("shutdown signal received, closing session") if err := session.Close(); err != nil { log.Error("close discord session failed", "err", err) } log.Info("shutdown complete") shutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() if err := shutdown(shutdownCtx); err != nil { log.Error("telemetry shutdown error", "err", err) } } // redactToken logs only a short prefix so startup logs remain useful without // leaking the full Discord bot token. func redactToken(token string) string { if token == "" { return "[not set]" } if len(token) <= 8 { return "[set]" } return token[:8] + "..." }