main.go aktualisiert
All checks were successful
release-tag / release-image (push) Successful in 1m41s

This commit is contained in:
2025-08-10 08:44:03 +00:00
parent 2a3638cc46
commit 9b2c984244

229
main.go
View File

@@ -14,9 +14,9 @@ import (
// ===== Defaults / Names (pro Guild per Commands konfigurierbar) ===== // ===== Defaults / Names (pro Guild per Commands konfigurierbar) =====
const ( const (
pollInterval = 15 * time.Second pollInterval = 15 * time.Second
defaultLobbyName = " Erstelle privaten Raum" defaultLobbyName = " Erstelle privaten Raum"
defaultCategory = "Private Räume" defaultCategory = "Private Räume"
) )
// ===== Per-Guild Config (in-memory) ===== // ===== Per-Guild Config (in-memory) =====
@@ -27,9 +27,9 @@ type GuildConfig struct {
} }
var ( var (
cfgMu sync.RWMutex cfgMu sync.RWMutex
guildCfgs = map[string]*GuildConfig{} guildCfgs = map[string]*GuildConfig{}
createdCmds = map[string][]*discordgo.ApplicationCommand{} // guildID -> cmds createdCmds = map[string][]*discordgo.ApplicationCommand{} // guildID -> cmds
registerOnce sync.Once registerOnce sync.Once
) )
@@ -286,10 +286,14 @@ func onVoiceStateUpdate(s *discordgo.Session, e *discordgo.VoiceStateUpdate) {
// ===== Permissions Helper ===== // ===== Permissions Helper =====
func isGuildAdmin(s *discordgo.Session, guildID string, user *discordgo.User, member *discordgo.Member) bool { 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 { if member == nil {
m, err := s.GuildMember(guildID, user.ID) m, err := s.GuildMember(guildID, user.ID)
if err != nil { return false } if err != nil {
return false
}
member = m member = m
} }
g, err := s.State.Guild(guildID) g, err := s.State.Guild(guildID)
@@ -297,7 +301,9 @@ func isGuildAdmin(s *discordgo.Session, guildID string, user *discordgo.User, me
return true return true
} }
roles, err := s.GuildRoles(guildID) roles, err := s.GuildRoles(guildID)
if err != nil { return false } if err != nil {
return false
}
rolePerm := int64(0) rolePerm := int64(0)
for _, r := range roles { for _, r := range roles {
for _, mr := range member.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) // Slash-Commands: makevc / setlobby / setcategory / settimeout (Admin-geschützt)
func onInteractionCreate(_ 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) { return func(s *discordgo.Session, i *discordgo.InteractionCreate) {
if i.Type != discordgo.InteractionApplicationCommand { return } if i.Type != discordgo.InteractionApplicationCommand {
return
}
data := i.ApplicationCommandData() data := i.ApplicationCommandData()
guildID := i.GuildID guildID := i.GuildID
switch data.Name { switch data.Name {
case "makevc": case "makevc":
cfg := getCfg(guildID) cfg := getCfg(guildID)
user := i.User user := i.User
if user == nil && i.Member != nil { user = i.Member.User } if user == nil && i.Member != nil {
if user == nil { return } user = i.Member.User
}
if user == nil {
return
}
userLimit := 0 userLimit := 0
timeoutMin := cfg.TimeoutMin timeoutMin := cfg.TimeoutMin
var nameOpt string var nameOpt string
for _, o := range data.Options { for _, o := range data.Options {
switch o.Name { switch o.Name {
case "name": nameOpt = o.StringValue() case "name":
case "user_limit": userLimit = int(o.IntValue()) nameOpt = o.StringValue()
case "timeout_min": timeoutMin = int(o.IntValue()) case "user_limit":
userLimit = int(o.IntValue())
case "timeout_min":
timeoutMin = int(o.IntValue())
} }
} }
display := displayName(i) display := displayName(i)
if nameOpt != "" { display = nameOpt } if nameOpt != "" {
display = nameOpt
}
catID, err := findOrCreateCategoryID(s, guildID, cfg.CategoryName) catID, err := findOrCreateCategoryID(s, guildID, cfg.CategoryName)
if err != nil { if err != nil {
_ = s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{Type: discordgo.InteractionResponseChannelMessageWithSource, Data: &discordgo.InteractionResponseData{Content: "Konnte Kategorie nicht finden/erstellen.", Flags: discordgo.MessageFlagsEphemeral}}) _ = 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 return
} }
msg := fmt.Sprintf("✅ Voice-Channel **%s** erstellt.", newChan.Name) 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}}) _ = s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{Type: discordgo.InteractionResponseChannelMessageWithSource, Data: &discordgo.InteractionResponseData{Content: msg, Flags: discordgo.MessageFlagsEphemeral}})
case "setlobby": case "setlobby":
@@ -360,9 +381,18 @@ func onInteractionCreate(_ string) func(s *discordgo.Session, i *discordgo.Inter
return return
} }
var name string var name string
for _, o := range data.Options { if o.Name == "name" { name = o.StringValue() } } for _, o := range data.Options {
if name == "" { _ = s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{Type: discordgo.InteractionResponseChannelMessageWithSource, Data: &discordgo.InteractionResponseData{Content: "Bitte gib einen Namen an.", Flags: discordgo.MessageFlagsEphemeral}}); return } if o.Name == "name" {
cfgMu.Lock(); getCfg(guildID).LobbyName = name; cfgMu.Unlock() 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}}) _ = s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{Type: discordgo.InteractionResponseChannelMessageWithSource, Data: &discordgo.InteractionResponseData{Content: "✅ Lobby-Name aktualisiert auf: " + name, Flags: discordgo.MessageFlagsEphemeral}})
case "setcategory": case "setcategory":
@@ -371,9 +401,18 @@ func onInteractionCreate(_ string) func(s *discordgo.Session, i *discordgo.Inter
return return
} }
var name string var name string
for _, o := range data.Options { if o.Name == "name" { name = o.StringValue() } } for _, o := range data.Options {
if name == "" { _ = s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{Type: discordgo.InteractionResponseChannelMessageWithSource, Data: &discordgo.InteractionResponseData{Content: "Bitte gib einen Namen an.", Flags: discordgo.MessageFlagsEphemeral}}); return } if o.Name == "name" {
cfgMu.Lock(); getCfg(guildID).CategoryName = name; cfgMu.Unlock() 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}}) _ = s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{Type: discordgo.InteractionResponseChannelMessageWithSource, Data: &discordgo.InteractionResponseData{Content: "✅ Kategorie-Name aktualisiert auf: " + name, Flags: discordgo.MessageFlagsEphemeral}})
case "settimeout": case "settimeout":
@@ -382,17 +421,117 @@ func onInteractionCreate(_ string) func(s *discordgo.Session, i *discordgo.Inter
return return
} }
var minutes int64 = int64(getCfg(guildID).TimeoutMin) var minutes int64 = int64(getCfg(guildID).TimeoutMin)
for _, o := range data.Options { if o.Name == "minutes" { minutes = o.IntValue() } } for _, o := range data.Options {
if minutes < 1 { minutes = 1 } if o.Name == "minutes" {
cfgMu.Lock(); getCfg(guildID).TimeoutMin = int(minutes); cfgMu.Unlock() 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}}) _ = 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 ===== // ===== Commands Definition =====
var ( var (
adminPerm = int64(discordgo.PermissionAdministrator) adminPerm = int64(discordgo.PermissionAdministrator)
slashCommands = []*discordgo.ApplicationCommand{ slashCommands = []*discordgo.ApplicationCommand{
{ {
Name: "makevc", Name: "makevc",
@@ -404,39 +543,55 @@ var (
}, },
}, },
{ {
Name: "setlobby", Name: "setlobby",
Description: "Setzt den Namen des Lobby-Voice-Channels", Description: "Setzt den Namen des Lobby-Voice-Channels",
DefaultMemberPermissions: &adminPerm, DefaultMemberPermissions: &adminPerm,
Options: []*discordgo.ApplicationCommandOption{ Options: []*discordgo.ApplicationCommandOption{
{Type: discordgo.ApplicationCommandOptionString, Name: "name", Description: "Neuer Lobby-Name", Required: true}, {Type: discordgo.ApplicationCommandOptionString, Name: "name", Description: "Neuer Lobby-Name", Required: true},
}, },
}, },
{ {
Name: "setcategory", Name: "setcategory",
Description: "Setzt den Namen der Kategorie für private Räume", Description: "Setzt den Namen der Kategorie für private Räume",
DefaultMemberPermissions: &adminPerm, DefaultMemberPermissions: &adminPerm,
Options: []*discordgo.ApplicationCommandOption{ Options: []*discordgo.ApplicationCommandOption{
{Type: discordgo.ApplicationCommandOptionString, Name: "name", Description: "Neue Kategorie-Bezeichnung", Required: true}, {Type: discordgo.ApplicationCommandOptionString, Name: "name", Description: "Neue Kategorie-Bezeichnung", Required: true},
}, },
}, },
{ {
Name: "settimeout", Name: "settimeout",
Description: "Setzt das Auto-Lösch-Timeout (Minuten)", Description: "Setzt das Auto-Lösch-Timeout (Minuten)",
DefaultMemberPermissions: &adminPerm, DefaultMemberPermissions: &adminPerm,
Options: []*discordgo.ApplicationCommandOption{ Options: []*discordgo.ApplicationCommandOption{
{Type: discordgo.ApplicationCommandOptionInteger, Name: "minutes", Description: "Minuten (>=1)", Required: true, MaxValue: 480}, {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 ===== // ===== main: Multi-Guild, pro Guild registrieren =====
func main() { func main() {
token := os.Getenv("DISCORD_TOKEN") 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) 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 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.") log.Println("Bot online. Ctrl+C zum Beenden.")
stop := make(chan os.Signal, 1) stop := make(chan os.Signal, 1)