diff --git a/main.go b/main.go index b001e41..e57bf5a 100644 --- a/main.go +++ b/main.go @@ -5,6 +5,7 @@ import ( "log" "os" "os/signal" + "sync" "syscall" "time" @@ -13,14 +14,12 @@ import ( const ( // optional: Voice-Channels unter dieser Kategorie anlegen (leer lassen = keine Kategorie) - envCategoryID = "CATEGORY_ID" // wie oft wir prüfen, ob der Channel leer ist pollInterval = 15 * time.Second + lobbyName = "➕ Erstelle privaten Raum" + categoryName = "Private Räume" ) -var lobbyID = os.Getenv("LOBBY_CHANNEL_ID") -var discordToken = os.Getenv("DISCORD_TOKEN") - // ===== Neu: gemeinsame Helper ===== func createPrivateVCAndMove( @@ -160,18 +159,29 @@ var slashCommands = []*discordgo.ApplicationCommand{ // Triggert beim Joinen in die Lobby und erstellt den VC + Move via Helper func onVoiceStateUpdate(s *discordgo.Session, e *discordgo.VoiceStateUpdate) { - // nur Join-Events in die Lobby interessieren - if e.UserID == "" || e.ChannelID != lobbyID || (e.BeforeUpdate != nil && e.BeforeUpdate.ChannelID == lobbyID) { + if e.UserID == "" { return } + // Nur Join in die definierte Lobby + lobby := findVoiceChannelIDByName(s, e.GuildID, lobbyName) + if lobby == "" || e.ChannelID != lobby || (e.BeforeUpdate != nil && e.BeforeUpdate.ChannelID == lobby) { + return + } + // Bots ignorieren m, err := s.GuildMember(e.GuildID, e.UserID) if err == nil && m.User.Bot { return } - // Parameter laden - categoryID := os.Getenv(envCategoryID) + // Kategorie per Name holen/erstellen + catID, err := findOrCreateCategoryID(s, e.GuildID, categoryName) + if err != nil { + log.Printf("Kategorie-Auflösung fehlgeschlagen: %v", err) + return + } + + // Parameter userLimit := 0 timeoutMin := 60 if v := os.Getenv("TIMEOUT_MIN"); v != "" { @@ -184,10 +194,10 @@ func onVoiceStateUpdate(s *discordgo.Session, e *discordgo.VoiceStateUpdate) { e.GuildID, e.UserID, safeDisplayName(m), - categoryID, + catID, userLimit, timeoutMin, - e.ChannelID, // Quell-Channel: Lobby + lobby, ) if err != nil { log.Printf("VC-Erstellung/Move fehlgeschlagen: %v", err) @@ -207,58 +217,111 @@ func safeDisplayName(m *discordgo.Member) string { return m.User.Username } +// Holt oder erstellt eine Kategorie nach Namen und gibt die ID zurück. +func findOrCreateCategoryID(s *discordgo.Session, guildID, name string) (string, error) { + chans, err := s.GuildChannels(guildID) + if err != nil { + return "", err + } + for _, ch := range chans { + if ch.Type == discordgo.ChannelTypeGuildCategory && ch.Name == name { + return ch.ID, nil + } + } + cat, err := s.GuildChannelCreateComplex(guildID, discordgo.GuildChannelCreateData{ + Name: name, + Type: discordgo.ChannelTypeGuildCategory, + }) + if err != nil { + return "", err + } + return cat.ID, nil +} + +// Sucht die ID eines Voice-Channels nach Name. Leerer String, wenn nicht gefunden. +func findVoiceChannelIDByName(s *discordgo.Session, guildID, name string) string { + chans, err := s.GuildChannels(guildID) + if err != nil { + return "" + } + for _, ch := range chans { + if ch.Type == discordgo.ChannelTypeGuildVoice && ch.Name == name { + return ch.ID + } + } + return "" +} + +var ( + createdCmds = map[string][]*discordgo.ApplicationCommand{} // guildID -> cmds + registerOnce sync.Once +) + func main() { - token := discordToken + token := os.Getenv("DISCORD_TOKEN") if token == "" { log.Fatal("Bitte setze DISCORD_TOKEN") } - guildID := os.Getenv("GUILD_ID") // optional: für schnelle Command-Registrierung - categoryID := os.Getenv(envCategoryID) s, err := discordgo.New("Bot " + token) if err != nil { log.Fatalf("Session fehlgeschlagen: %v", err) } - // Intents für Slash + Voice-State-Events + // Nur was wir brauchen s.Identify.Intents = discordgo.IntentsGuilds | discordgo.IntentsGuildVoiceStates - s.AddHandler(onVoiceStateUpdate) // ← neu // Handlers - s.AddHandler(func(_ *discordgo.Session, r *discordgo.Ready) { + s.AddHandler(onVoiceStateUpdate) + s.AddHandler(onInteractionCreate("")) // parameter wird nicht mehr genutzt + s.AddHandler(func(s *discordgo.Session, r *discordgo.Ready) { log.Printf("Eingeloggt als %s", r.User.Username) - }) - s.AddHandler(onInteractionCreate(categoryID)) + // Ready kann bei Reconnect mehrfach feuern → nur einmal registrieren + registerOnce.Do(func() { + appID := s.State.User.ID + for _, g := range s.State.Guilds { + log.Printf("Registriere Commands in Guild: %s (%s)", g.Name, g.ID) + for _, cmd := range slashCommands { + c, err := s.ApplicationCommandCreate(appID, g.ID, cmd) + if err != nil { + log.Printf("Command-Registrierung in %s fehlgeschlagen: %v", g.Name, err) + continue + } + createdCmds[g.ID] = append(createdCmds[g.ID], c) + } + } + log.Printf("Lobby-Name: %q | Kategorie-Name: %q", lobbyName, categoryName) + }) + }) + + // Start if err := s.Open(); err != nil { log.Fatalf("Gateway-Start fehlgeschlagen: %v", err) } log.Println("Bot online. Ctrl+C zum Beenden.") - // Slash-Commands registrieren (Guild-spezifisch = sofort sichtbar) - appID := s.State.User.ID - created := make([]*discordgo.ApplicationCommand, 0, len(slashCommands)) - for _, cmd := range slashCommands { - c, err := s.ApplicationCommandCreate(appID, guildID, cmd) - if err != nil { - log.Fatalf("Command-Registrierung fehlgeschlagen: %v", err) - } - created = append(created, c) - } - - // Shutdown + // Shutdown warten stop := make(chan os.Signal, 1) signal.Notify(stop, os.Interrupt, syscall.SIGTERM) <-stop log.Println("Fahre herunter…") - for _, c := range created { - _ = s.ApplicationCommandDelete(appID, guildID, c.ID) + + // Commands wieder entfernen (best effort) + appID := s.State.User.ID + for guildID, cmds := range createdCmds { + for _, c := range cmds { + if delErr := s.ApplicationCommandDelete(appID, guildID, c.ID); delErr != nil { + log.Printf("Cmd-Delete (%s/%s) fehlgeschlagen: %v", guildID, c.Name, delErr) + } + } } + _ = s.Close() } // Slash-Command-Variante, nutzt dieselbe Helper-Logik und moved falls der User bereits in einem VC ist -func onInteractionCreate(categoryID string) func(s *discordgo.Session, i *discordgo.InteractionCreate) { +func onInteractionCreate(_ string) func(s *discordgo.Session, i *discordgo.InteractionCreate) { return func(s *discordgo.Session, i *discordgo.InteractionCreate) { if i.Type != discordgo.InteractionApplicationCommand { return @@ -268,7 +331,7 @@ func onInteractionCreate(categoryID string) func(s *discordgo.Session, i *discor } // Optionen lesen - var name string // (wird nur für die Anzeige verwendet – der Helper baut "🔒 " selbst) + var nameOpt string userLimit := 0 timeoutMin := 60 if v := os.Getenv("TIMEOUT_MIN"); v != "" { @@ -277,7 +340,7 @@ func onInteractionCreate(categoryID string) func(s *discordgo.Session, i *discor for _, o := range i.ApplicationCommandData().Options { switch o.Name { case "name": - name = o.StringValue() + nameOpt = o.StringValue() case "user_limit": userLimit = int(o.IntValue()) case "timeout_min": @@ -285,18 +348,35 @@ func onInteractionCreate(categoryID string) func(s *discordgo.Session, i *discor } } - // Aufrufer bestimmen + // Aufrufer user := i.User if user == nil && i.Member != nil { user = i.Member.User } - display := displayName(i) - if name != "" { - // Falls Name explizit gesetzt wurde, nimm ihn als Display-Basis - display = name + if user == nil { + return } - // Wenn der User bereits in einem VC ist, verschieben wir ihn automatisch + // Kategorie per Name holen/erstellen + catID, err := findOrCreateCategoryID(s, i.GuildID, categoryName) + if err != nil { + _ = s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{ + Type: discordgo.InteractionResponseChannelMessageWithSource, + Data: &discordgo.InteractionResponseData{ + Content: "Konnte Kategorie nicht finden/erstellen.", + Flags: discordgo.MessageFlagsEphemeral, + }, + }) + return + } + + // Display-Name bestimmen (Option > Fallback auf displayName()) + display := displayName(i) + if nameOpt != "" { + display = nameOpt + } + + // Falls User bereits in einem Voice-Channel ist: automatisch verschieben srcVC := findUserVoiceChannelID(s, i.GuildID, user.ID) // "" wenn nicht in VC newChan, err := createPrivateVCAndMove( @@ -304,7 +384,7 @@ func onInteractionCreate(categoryID string) func(s *discordgo.Session, i *discor i.GuildID, user.ID, display, - categoryID, + catID, userLimit, timeoutMin, srcVC, // nur wenn nicht leer wird moved @@ -326,7 +406,6 @@ func onInteractionCreate(categoryID string) func(s *discordgo.Session, i *discor } else { msg += " Ich verschiebe dich jetzt dort hinein." } - _ = s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{ Type: discordgo.InteractionResponseChannelMessageWithSource, Data: &discordgo.InteractionResponseData{