diff --git a/main.go b/main.go index e01a641..eca86e3 100644 --- a/main.go +++ b/main.go @@ -14,9 +14,9 @@ import ( // ===== Defaults / Names (pro Guild per Commands konfigurierbar) ===== const ( - pollInterval = 15 * time.Second - defaultLobbyName = "➕ Erstelle privaten Raum" - defaultCategory = "Private Räume" + pollInterval = 15 * time.Second + defaultLobbyName = "➕ Erstelle privaten Raum" + defaultCategory = "Private Räume" ) // ===== Per-Guild Config (in-memory) ===== @@ -27,9 +27,9 @@ type GuildConfig struct { } var ( - cfgMu sync.RWMutex - guildCfgs = map[string]*GuildConfig{} - createdCmds = map[string][]*discordgo.ApplicationCommand{} // guildID -> cmds + cfgMu sync.RWMutex + guildCfgs = map[string]*GuildConfig{} + createdCmds = map[string][]*discordgo.ApplicationCommand{} // guildID -> cmds registerOnce sync.Once ) @@ -286,10 +286,14 @@ func onVoiceStateUpdate(s *discordgo.Session, e *discordgo.VoiceStateUpdate) { // ===== Permissions Helper ===== func isGuildAdmin(s *discordgo.Session, guildID string, user *discordgo.User, member *discordgo.Member) bool { - if user == nil && member == nil { return false } + if user == nil && member == nil { + return false + } if member == nil { m, err := s.GuildMember(guildID, user.ID) - if err != nil { return false } + if err != nil { + return false + } member = m } g, err := s.State.Guild(guildID) @@ -297,7 +301,9 @@ func isGuildAdmin(s *discordgo.Session, guildID string, user *discordgo.User, me return true } roles, err := s.GuildRoles(guildID) - if err != nil { return false } + if err != nil { + return false + } rolePerm := int64(0) for _, r := range roles { for _, mr := range member.Roles { @@ -318,27 +324,38 @@ func isGuildAdmin(s *discordgo.Session, guildID string, user *discordgo.User, me // Slash-Commands: makevc / setlobby / setcategory / settimeout (Admin-geschützt) func onInteractionCreate(_ string) func(s *discordgo.Session, i *discordgo.InteractionCreate) { return func(s *discordgo.Session, i *discordgo.InteractionCreate) { - if i.Type != discordgo.InteractionApplicationCommand { return } + if i.Type != discordgo.InteractionApplicationCommand { + return + } data := i.ApplicationCommandData() guildID := i.GuildID switch data.Name { case "makevc": cfg := getCfg(guildID) user := i.User - if user == nil && i.Member != nil { user = i.Member.User } - if user == nil { return } + if user == nil && i.Member != nil { + user = i.Member.User + } + if user == nil { + return + } userLimit := 0 timeoutMin := cfg.TimeoutMin var nameOpt string for _, o := range data.Options { switch o.Name { - case "name": nameOpt = o.StringValue() - case "user_limit": userLimit = int(o.IntValue()) - case "timeout_min": timeoutMin = int(o.IntValue()) + case "name": + nameOpt = o.StringValue() + case "user_limit": + userLimit = int(o.IntValue()) + case "timeout_min": + timeoutMin = int(o.IntValue()) } } display := displayName(i) - if nameOpt != "" { display = nameOpt } + if nameOpt != "" { + display = nameOpt + } catID, err := findOrCreateCategoryID(s, guildID, cfg.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}}) @@ -351,7 +368,11 @@ func onInteractionCreate(_ string) func(s *discordgo.Session, i *discordgo.Inter return } msg := fmt.Sprintf("✅ Voice-Channel **%s** erstellt.", newChan.Name) - if srcVC == "" { msg += " Du bist aktuell in keinem Voice-Channel; join bitte manuell in deinen neuen Raum." } else { msg += " Ich verschiebe dich jetzt dort hinein." } + if srcVC == "" { + msg += " Du bist aktuell in keinem Voice-Channel; join bitte manuell in deinen neuen Raum." + } else { + msg += " Ich verschiebe dich jetzt dort hinein." + } _ = s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{Type: discordgo.InteractionResponseChannelMessageWithSource, Data: &discordgo.InteractionResponseData{Content: msg, Flags: discordgo.MessageFlagsEphemeral}}) case "setlobby": @@ -360,9 +381,18 @@ func onInteractionCreate(_ string) func(s *discordgo.Session, i *discordgo.Inter return } var name string - for _, o := range data.Options { if o.Name == "name" { name = o.StringValue() } } - if name == "" { _ = s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{Type: discordgo.InteractionResponseChannelMessageWithSource, Data: &discordgo.InteractionResponseData{Content: "Bitte gib einen Namen an.", Flags: discordgo.MessageFlagsEphemeral}}); return } - cfgMu.Lock(); getCfg(guildID).LobbyName = name; cfgMu.Unlock() + for _, o := range data.Options { + if o.Name == "name" { + name = o.StringValue() + } + } + if name == "" { + _ = s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{Type: discordgo.InteractionResponseChannelMessageWithSource, Data: &discordgo.InteractionResponseData{Content: "Bitte gib einen Namen an.", Flags: discordgo.MessageFlagsEphemeral}}) + return + } + cfgMu.Lock() + getCfg(guildID).LobbyName = name + cfgMu.Unlock() _ = s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{Type: discordgo.InteractionResponseChannelMessageWithSource, Data: &discordgo.InteractionResponseData{Content: "✅ Lobby-Name aktualisiert auf: " + name, Flags: discordgo.MessageFlagsEphemeral}}) case "setcategory": @@ -371,9 +401,18 @@ func onInteractionCreate(_ string) func(s *discordgo.Session, i *discordgo.Inter return } var name string - for _, o := range data.Options { if o.Name == "name" { name = o.StringValue() } } - if name == "" { _ = s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{Type: discordgo.InteractionResponseChannelMessageWithSource, Data: &discordgo.InteractionResponseData{Content: "Bitte gib einen Namen an.", Flags: discordgo.MessageFlagsEphemeral}}); return } - cfgMu.Lock(); getCfg(guildID).CategoryName = name; cfgMu.Unlock() + for _, o := range data.Options { + if o.Name == "name" { + name = o.StringValue() + } + } + if name == "" { + _ = s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{Type: discordgo.InteractionResponseChannelMessageWithSource, Data: &discordgo.InteractionResponseData{Content: "Bitte gib einen Namen an.", Flags: discordgo.MessageFlagsEphemeral}}) + return + } + cfgMu.Lock() + getCfg(guildID).CategoryName = name + cfgMu.Unlock() _ = s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{Type: discordgo.InteractionResponseChannelMessageWithSource, Data: &discordgo.InteractionResponseData{Content: "✅ Kategorie-Name aktualisiert auf: " + name, Flags: discordgo.MessageFlagsEphemeral}}) case "settimeout": @@ -382,17 +421,117 @@ func onInteractionCreate(_ string) func(s *discordgo.Session, i *discordgo.Inter return } var minutes int64 = int64(getCfg(guildID).TimeoutMin) - for _, o := range data.Options { if o.Name == "minutes" { minutes = o.IntValue() } } - if minutes < 1 { minutes = 1 } - cfgMu.Lock(); getCfg(guildID).TimeoutMin = int(minutes); cfgMu.Unlock() + for _, o := range data.Options { + if o.Name == "minutes" { + minutes = o.IntValue() + } + } + if minutes < 1 { + minutes = 1 + } + cfgMu.Lock() + getCfg(guildID).TimeoutMin = int(minutes) + cfgMu.Unlock() _ = s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{Type: discordgo.InteractionResponseChannelMessageWithSource, Data: &discordgo.InteractionResponseData{Content: fmt.Sprintf("✅ Timeout auf %d Minuten gesetzt.", minutes), Flags: discordgo.MessageFlagsEphemeral}}) + case "adduser": + // wer ruft auf? + requester := i.User + if requester == nil && i.Member != nil { + requester = i.Member.User + } + if requester == nil { + return + } + + // Ziel-User aus den Optionen holen + var target *discordgo.User + for _, o := range data.Options { + if o.Name == "user" { + // UserValue braucht die Session + u := o.UserValue(s) + if u != nil { + target = u + } + } + } + if target == nil { + _ = s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{ + Type: discordgo.InteractionResponseChannelMessageWithSource, + Data: &discordgo.InteractionResponseData{ + Content: "Bitte gib ein gültiges Mitglied an.", + Flags: discordgo.MessageFlagsEphemeral, + }, + }) + return + } + + // In welchem VC ist der Aufrufer gerade? + srcVC := findUserVoiceChannelID(s, guildID, requester.ID) + if srcVC == "" { + _ = s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{ + Type: discordgo.InteractionResponseChannelMessageWithSource, + Data: &discordgo.InteractionResponseData{ + Content: "Du bist aktuell in keinem Voice-Channel. Betritt zuerst deinen privaten Channel.", + Flags: discordgo.MessageFlagsEphemeral, + }, + }) + return + } + + // Sicherheits-Check: darf der Aufrufer diesen Channel verwalten? + // (Der Owner bekommt in deinem Code ManageChannels → das ist unser Indikator) + perms, err := s.UserChannelPermissions(requester.ID, srcVC) + if err != nil || perms&int64(discordgo.PermissionManageChannels) == 0 { + _ = s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{ + Type: discordgo.InteractionResponseChannelMessageWithSource, + Data: &discordgo.InteractionResponseData{ + Content: "❌ Du bist nicht der Besitzer dieses Channels oder dir fehlen Rechte (Manage Channel).", + Flags: discordgo.MessageFlagsEphemeral, + }, + }) + return + } + + // Overwrite setzen: Ziel-User darf sehen & beitreten (& sprechen/Stream/VAD) + allow := int64( + discordgo.PermissionViewChannel | + discordgo.PermissionVoiceConnect | + discordgo.PermissionVoiceSpeak | + discordgo.PermissionVoiceUseVAD | + discordgo.PermissionVoiceStreamVideo, + ) + if err := s.ChannelPermissionSet( + srcVC, + target.ID, + discordgo.PermissionOverwriteTypeMember, + allow, + 0, + ); err != nil { + _ = s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{ + Type: discordgo.InteractionResponseChannelMessageWithSource, + Data: &discordgo.InteractionResponseData{ + Content: fmt.Sprintf("Konnte Berechtigung nicht setzen: %v", err), + Flags: discordgo.MessageFlagsEphemeral, + }, + }) + return + } + + _ = s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{ + Type: discordgo.InteractionResponseChannelMessageWithSource, + Data: &discordgo.InteractionResponseData{ + Content: fmt.Sprintf("✅ %s hat jetzt Zugriff auf deinen Voice-Channel.", target.Username), + Flags: discordgo.MessageFlagsEphemeral, + }, + }) + } } } // ===== Commands Definition ===== var ( - adminPerm = int64(discordgo.PermissionAdministrator) + adminPerm = int64(discordgo.PermissionAdministrator) slashCommands = []*discordgo.ApplicationCommand{ { Name: "makevc", @@ -404,39 +543,55 @@ var ( }, }, { - Name: "setlobby", - Description: "Setzt den Namen des Lobby-Voice-Channels", + Name: "setlobby", + Description: "Setzt den Namen des Lobby-Voice-Channels", DefaultMemberPermissions: &adminPerm, Options: []*discordgo.ApplicationCommandOption{ {Type: discordgo.ApplicationCommandOptionString, Name: "name", Description: "Neuer Lobby-Name", Required: true}, }, }, { - Name: "setcategory", - Description: "Setzt den Namen der Kategorie für private Räume", + Name: "setcategory", + Description: "Setzt den Namen der Kategorie für private Räume", DefaultMemberPermissions: &adminPerm, Options: []*discordgo.ApplicationCommandOption{ {Type: discordgo.ApplicationCommandOptionString, Name: "name", Description: "Neue Kategorie-Bezeichnung", Required: true}, }, }, { - Name: "settimeout", - Description: "Setzt das Auto-Lösch-Timeout (Minuten)", + Name: "settimeout", + Description: "Setzt das Auto-Lösch-Timeout (Minuten)", DefaultMemberPermissions: &adminPerm, Options: []*discordgo.ApplicationCommandOption{ {Type: discordgo.ApplicationCommandOptionInteger, Name: "minutes", Description: "Minuten (>=1)", Required: true, MaxValue: 480}, }, }, + { + Name: "adduser", + Description: "Gibt einem Mitglied Zugriff auf deinen aktuellen privaten Voice-Channel", + Options: []*discordgo.ApplicationCommandOption{ + { + Type: discordgo.ApplicationCommandOptionUser, + Name: "user", + Description: "Mitglied, das Zugriff bekommen soll", + Required: true, + }, + }, + }, } ) // ===== main: Multi-Guild, pro Guild registrieren ===== func main() { token := os.Getenv("DISCORD_TOKEN") - if token == "" { log.Fatal("Bitte setze DISCORD_TOKEN") } + if token == "" { + log.Fatal("Bitte setze DISCORD_TOKEN") + } s, err := discordgo.New("Bot " + token) - if err != nil { log.Fatalf("Session fehlgeschlagen: %v", err) } + if err != nil { + log.Fatalf("Session fehlgeschlagen: %v", err) + } s.Identify.Intents = discordgo.IntentsGuilds | discordgo.IntentsGuildVoiceStates @@ -461,7 +616,9 @@ func main() { }) }) - if err := s.Open(); err != nil { log.Fatalf("Gateway-Start fehlgeschlagen: %v", err) } + if err := s.Open(); err != nil { + log.Fatalf("Gateway-Start fehlgeschlagen: %v", err) + } log.Println("Bot online. Ctrl+C zum Beenden.") stop := make(chan os.Signal, 1)