package main import ( "fmt" "log" "os" "os/signal" "syscall" "time" "github.com/bwmarrin/discordgo" "github.com/joho/godotenv" "heydeman/vc-stat-bot-discord/db_management" ) var sessions = make(map[string]*Session) type Session struct { Start time.Time PausedAt *time.Time Paused bool Accum time.Duration } func main() { _ = godotenv.Load() db_management.InitDB() token := os.Getenv("DISCORD_TOKEN") if token == "" { log.Fatal("DISCORD_TOKEN nicht gefunden!") } discord, err := discordgo.New("Bot " + token) if err != nil { log.Fatal(err) } // Handler discord.AddHandler(interactionCreate) discord.AddHandler(voiceUpdate) // Intents discord.Identify.Intents = discordgo.IntentsGuilds | discordgo.IntentsGuildVoiceStates err = discord.Open() if err != nil { log.Fatal("Error opening connection:", err) } // Slash Commands registrieren registerCommands(discord) go autoSaveSessions() fmt.Println("Bot läuft stabil. CTRL+C zum Beenden.") // Shutdown Handling stop := make(chan os.Signal, 1) signal.Notify(stop, syscall.SIGINT, syscall.SIGTERM) <-stop fmt.Println("Speichere aktive Sessions...") // 🔥 NEUE Shutdown-Logik (wichtig!) for uid, sess := range sessions { var total time.Duration // bereits gesammelte Zeit total += sess.Accum // wenn NICHT pausiert → aktuelle Zeit dazu if !sess.Paused { total += time.Since(sess.Start) } db_management.SaveSession(uid, int(total.Seconds())) } discord.Close() } func registerCommands(s *discordgo.Session) { // /stats _, err := s.ApplicationCommandCreate(s.State.User.ID, "", &discordgo.ApplicationCommand{ Name: "stats", Description: "Zeigt die 30-Tage Statistik eines Users", Options: []*discordgo.ApplicationCommandOption{ { Type: discordgo.ApplicationCommandOptionUser, Name: "user", Description: "Optional: anderer User", Required: false, }, }, }) if err != nil { log.Println("Fehler bei /stats:", err) } // /top _, err = s.ApplicationCommandCreate(s.State.User.ID, "", &discordgo.ApplicationCommand{ Name: "top", Description: "Top 10 User nach Zeit (30 Tage)", }) if err != nil { log.Println("Fehler bei /top:", err) } } func interactionCreate(s *discordgo.Session, i *discordgo.InteractionCreate) { if i.Type != discordgo.InteractionApplicationCommand { return } switch i.ApplicationCommandData().Name { // ===================== // /stats // ===================== case "stats": user := i.Member.User if len(i.ApplicationCommandData().Options) > 0 { opt := i.ApplicationCommandData().Options[0] if opt.Name == "user" { user = opt.UserValue(s) } } totalSeconds := db_management.GetUserStats(user.ID) duration := time.Duration(totalSeconds) * time.Second name := user.GlobalName if name == "" { name = user.Username } msg := fmt.Sprintf( "📊 **30-Tage Statistik für %s**\nZeit: `%s`", name, formatDuration(duration), ) s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{ Type: discordgo.InteractionResponseChannelMessageWithSource, Data: &discordgo.InteractionResponseData{ Content: msg, }, }) // ===================== // /top // ===================== case "top": topUsers := db_management.GetTopUsers(10) if len(topUsers) == 0 { s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{ Type: discordgo.InteractionResponseChannelMessageWithSource, Data: &discordgo.InteractionResponseData{ Content: "Keine Daten vorhanden.", }, }) return } msg := "🏆 **Top 10 (30 Tage)**\n\n" for i, user := range topUsers { duration := time.Duration(user.Seconds) * time.Second msg += fmt.Sprintf( "**%d.** <@%s> — `%s`\n", i+1, user.UserID, formatDuration(duration), ) } s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{ Type: discordgo.InteractionResponseChannelMessageWithSource, Data: &discordgo.InteractionResponseData{ Content: msg, }, }) } } const excludedChannelID = "1232031563391172649" func voiceUpdate(s *discordgo.Session, v *discordgo.VoiceStateUpdate) { uid := v.UserID // ========================= // ❌ Pause Bedingungen // ========================= if v.ChannelID == excludedChannelID || v.SelfMute || v.Mute || v.SelfDeaf || v.Deaf { if sess, ok := sessions[uid]; ok && !sess.Paused { now := time.Now() elapsed := now.Sub(sess.Start) sess.Accum += elapsed sess.PausedAt = &now sess.Paused = true } return } // ========================= // ▶ Resume (User wieder aktiv) // ========================= if v.ChannelID != "" { sess, ok := sessions[uid] if !ok { // neue Session sessions[uid] = &Session{ Start: time.Now(), Paused: false, } return } // wenn pausiert → weiterlaufen lassen if sess.Paused { sess.Start = time.Now() sess.Paused = false sess.PausedAt = nil } return } // ========================= // ❌ User verlässt Voice // ========================= if sess, ok := sessions[uid]; ok { total := sess.Accum if !sess.Paused { total += time.Since(sess.Start) } db_management.SaveSession(uid, int(total.Seconds())) delete(sessions, uid) } } func formatDuration(d time.Duration) string { h := int(d.Hours()) m := int(d.Minutes()) % 60 s := int(d.Seconds()) % 60 return fmt.Sprintf("%dh %dm %ds", h, m, s) } func autoSaveSessions() { ticker := time.NewTicker(5 * time.Minute) // ⏱ Intervall anpassen defer ticker.Stop() for range ticker.C { fmt.Println("🔄 Auto-Save: speichere Sessions...") for uid, sess := range sessions { var total time.Duration total += sess.Accum if !sess.Paused { total += time.Since(sess.Start) } // Zwischenspeichern (ADD statt replace!) db_management.SaveSession(uid, int(total.Seconds())) // ⚠️ Wichtig: // NICHT löschen → Session läuft weiter } } }