package main import ( "flag" "log" "log/slog" "net/http" "os" "releasewatcher/internal/auth" "releasewatcher/internal/discordbot" "releasewatcher/internal/notify" "releasewatcher/internal/store" "releasewatcher/internal/web" ) func main() { addr := flag.String("addr", env("RW_ADDR", ":8080"), "HTTP listen address") dataFile := flag.String("data", env("RW_DATA", "/data/releasewatcher.json"), "JSON data file") adminEmail := flag.String("admin-email", env("RW_ADMIN_EMAIL", "admin@example.local"), "initial admin email") adminPass := flag.String("admin-pass", env("RW_ADMIN_PASSWORD", "admin12345"), "initial admin password") secret := flag.String("secret", env("RW_SECRET", "change-me-in-production"), "session secret") discordToken := flag.String("discord-token", env("RW_DISCORD_TOKEN", ""), "Discord bot token; empty disables Discord") discordAppID := flag.String("discord-app-id", env("RW_DISCORD_APP_ID", ""), "Discord application ID for command registration") discordGuildID := flag.String("discord-guild-id", env("RW_DISCORD_GUILD_ID", ""), "Discord guild/server ID for release channels") discordCategoryID := flag.String("discord-category-id", env("RW_DISCORD_CATEGORY_ID", ""), "Discord category ID for software release channels; empty creates/uses category name") discordCategoryName := flag.String("discord-category-name", env("RW_DISCORD_CATEGORY_NAME", "ReleaseWatcher"), "Discord category name for software release channels") discordReleaseMention := flag.String("discord-release-mention", env("RW_DISCORD_RELEASE_MENTION", "@here"), "Mention text for release channel pings, e.g. @here or <@&ROLE_ID>; empty disables pings") discordSendDMs := flag.Bool("discord-send-dms", envBool("RW_DISCORD_SEND_DMS", true), "also send release notifications as DMs to subscribers") flag.Parse() logger := slog.New(slog.NewTextHandler(os.Stdout, nil)) st, err := store.Open(*dataFile) if err != nil { log.Fatal(err) } if err := st.EnsureUser(store.User{Email: *adminEmail, DisplayName: "Administrator", Role: store.RoleAdmin, PasswordHash: auth.HashPassword(*adminPass)}); err != nil { log.Fatal(err) } am := auth.New(*secret, st) var notifier notify.ReleaseNotifier = notify.LoggingNotifier{Log: logger} if *discordToken != "" { dg, err := discordbot.NewDiscordSession(*discordToken) if err != nil { log.Fatal(err) } discordServices := discordbot.NewServices(st, dg, logger, discordbot.Config{ GuildID: *discordGuildID, CategoryID: *discordCategoryID, CategoryName: *discordCategoryName, ReleaseMention: *discordReleaseMention, SendDMs: *discordSendDMs, }) discordbot.AttachDiscordHandlers(dg, discordServices) if err := dg.Open(); err != nil { log.Fatal(err) } defer dg.Close() if *discordAppID != "" { if err := discordbot.UpsertCommands(dg, *discordAppID); err != nil { logger.Warn("Discord commands konnten nicht registriert werden", "error", err) } } else { logger.Warn("RW_DISCORD_APP_ID fehlt; Slash-Commands werden nicht registriert") } notifier = discordServices logger.Info("Discord-Bot gestartet") } srv, err := web.New(st, am, notifier, logger) if err != nil { log.Fatal(err) } logger.Info("ReleaseWatcher gestartet", "addr", *addr, "admin", *adminEmail) log.Fatal(http.ListenAndServe(*addr, srv.Routes())) } func env(key, fallback string) string { if v := os.Getenv(key); v != "" { return v } return fallback } func envBool(key string, fallback bool) bool { switch os.Getenv(key) { case "1", "true", "TRUE", "yes", "YES", "on", "ON": return true case "0", "false", "FALSE", "no", "NO", "off", "OFF": return false default: return fallback } }