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

This commit is contained in:
2025-08-09 22:59:03 +00:00
parent 1536934306
commit 4a1c76baca

165
main.go
View File

@@ -5,6 +5,7 @@ import (
"log" "log"
"os" "os"
"os/signal" "os/signal"
"sync"
"syscall" "syscall"
"time" "time"
@@ -13,14 +14,12 @@ import (
const ( const (
// optional: Voice-Channels unter dieser Kategorie anlegen (leer lassen = keine Kategorie) // optional: Voice-Channels unter dieser Kategorie anlegen (leer lassen = keine Kategorie)
envCategoryID = "CATEGORY_ID"
// wie oft wir prüfen, ob der Channel leer ist // wie oft wir prüfen, ob der Channel leer ist
pollInterval = 15 * time.Second 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 ===== // ===== Neu: gemeinsame Helper =====
func createPrivateVCAndMove( func createPrivateVCAndMove(
@@ -160,18 +159,29 @@ var slashCommands = []*discordgo.ApplicationCommand{
// Triggert beim Joinen in die Lobby und erstellt den VC + Move via Helper // Triggert beim Joinen in die Lobby und erstellt den VC + Move via Helper
func onVoiceStateUpdate(s *discordgo.Session, e *discordgo.VoiceStateUpdate) { func onVoiceStateUpdate(s *discordgo.Session, e *discordgo.VoiceStateUpdate) {
// nur Join-Events in die Lobby interessieren if e.UserID == "" {
if e.UserID == "" || e.ChannelID != lobbyID || (e.BeforeUpdate != nil && e.BeforeUpdate.ChannelID == lobbyID) {
return 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 // Bots ignorieren
m, err := s.GuildMember(e.GuildID, e.UserID) m, err := s.GuildMember(e.GuildID, e.UserID)
if err == nil && m.User.Bot { if err == nil && m.User.Bot {
return return
} }
// Parameter laden // Kategorie per Name holen/erstellen
categoryID := os.Getenv(envCategoryID) catID, err := findOrCreateCategoryID(s, e.GuildID, categoryName)
if err != nil {
log.Printf("Kategorie-Auflösung fehlgeschlagen: %v", err)
return
}
// Parameter
userLimit := 0 userLimit := 0
timeoutMin := 60 timeoutMin := 60
if v := os.Getenv("TIMEOUT_MIN"); v != "" { if v := os.Getenv("TIMEOUT_MIN"); v != "" {
@@ -184,10 +194,10 @@ func onVoiceStateUpdate(s *discordgo.Session, e *discordgo.VoiceStateUpdate) {
e.GuildID, e.GuildID,
e.UserID, e.UserID,
safeDisplayName(m), safeDisplayName(m),
categoryID, catID,
userLimit, userLimit,
timeoutMin, timeoutMin,
e.ChannelID, // Quell-Channel: Lobby lobby,
) )
if err != nil { if err != nil {
log.Printf("VC-Erstellung/Move fehlgeschlagen: %v", err) log.Printf("VC-Erstellung/Move fehlgeschlagen: %v", err)
@@ -207,58 +217,111 @@ func safeDisplayName(m *discordgo.Member) string {
return m.User.Username 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() { func main() {
token := discordToken token := os.Getenv("DISCORD_TOKEN")
if token == "" { if token == "" {
log.Fatal("Bitte setze DISCORD_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) s, err := discordgo.New("Bot " + token)
if err != nil { if err != nil {
log.Fatalf("Session fehlgeschlagen: %v", err) 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.Identify.Intents = discordgo.IntentsGuilds | discordgo.IntentsGuildVoiceStates
s.AddHandler(onVoiceStateUpdate) // ← neu
// Handlers // 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) 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 { if err := s.Open(); err != nil {
log.Fatalf("Gateway-Start fehlgeschlagen: %v", err) log.Fatalf("Gateway-Start fehlgeschlagen: %v", err)
} }
log.Println("Bot online. Ctrl+C zum Beenden.") log.Println("Bot online. Ctrl+C zum Beenden.")
// Slash-Commands registrieren (Guild-spezifisch = sofort sichtbar) // Shutdown warten
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
stop := make(chan os.Signal, 1) stop := make(chan os.Signal, 1)
signal.Notify(stop, os.Interrupt, syscall.SIGTERM) signal.Notify(stop, os.Interrupt, syscall.SIGTERM)
<-stop <-stop
log.Println("Fahre herunter…") 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() _ = s.Close()
} }
// Slash-Command-Variante, nutzt dieselbe Helper-Logik und moved falls der User bereits in einem VC ist // 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) { return func(s *discordgo.Session, i *discordgo.InteractionCreate) {
if i.Type != discordgo.InteractionApplicationCommand { if i.Type != discordgo.InteractionApplicationCommand {
return return
@@ -268,7 +331,7 @@ func onInteractionCreate(categoryID string) func(s *discordgo.Session, i *discor
} }
// Optionen lesen // Optionen lesen
var name string // (wird nur für die Anzeige verwendet der Helper baut "🔒 <display>" selbst) var nameOpt string
userLimit := 0 userLimit := 0
timeoutMin := 60 timeoutMin := 60
if v := os.Getenv("TIMEOUT_MIN"); v != "" { 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 { for _, o := range i.ApplicationCommandData().Options {
switch o.Name { switch o.Name {
case "name": case "name":
name = o.StringValue() nameOpt = o.StringValue()
case "user_limit": case "user_limit":
userLimit = int(o.IntValue()) userLimit = int(o.IntValue())
case "timeout_min": case "timeout_min":
@@ -285,18 +348,35 @@ func onInteractionCreate(categoryID string) func(s *discordgo.Session, i *discor
} }
} }
// Aufrufer bestimmen // Aufrufer
user := i.User user := i.User
if user == nil && i.Member != nil { if user == nil && i.Member != nil {
user = i.Member.User user = i.Member.User
} }
display := displayName(i) if user == nil {
if name != "" { return
// Falls Name explizit gesetzt wurde, nimm ihn als Display-Basis
display = name
} }
// 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 srcVC := findUserVoiceChannelID(s, i.GuildID, user.ID) // "" wenn nicht in VC
newChan, err := createPrivateVCAndMove( newChan, err := createPrivateVCAndMove(
@@ -304,7 +384,7 @@ func onInteractionCreate(categoryID string) func(s *discordgo.Session, i *discor
i.GuildID, i.GuildID,
user.ID, user.ID,
display, display,
categoryID, catID,
userLimit, userLimit,
timeoutMin, timeoutMin,
srcVC, // nur wenn nicht leer wird moved srcVC, // nur wenn nicht leer wird moved
@@ -326,7 +406,6 @@ func onInteractionCreate(categoryID string) func(s *discordgo.Session, i *discor
} else { } else {
msg += " Ich verschiebe dich jetzt dort hinein." msg += " Ich verschiebe dich jetzt dort hinein."
} }
_ = s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{ _ = s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
Type: discordgo.InteractionResponseChannelMessageWithSource, Type: discordgo.InteractionResponseChannelMessageWithSource,
Data: &discordgo.InteractionResponseData{ Data: &discordgo.InteractionResponseData{