Update-Test
Some checks failed
release-tag / release-image (push) Has been cancelled

This commit is contained in:
2025-08-10 00:17:03 +02:00
parent 70133bf396
commit 941cd1d854

248
main.go
View File

@@ -21,6 +21,102 @@ const (
var lobbyID = os.Getenv("LOBBY_CHANNEL_ID") var lobbyID = os.Getenv("LOBBY_CHANNEL_ID")
var discordToken = os.Getenv("DISCORD_TOKEN") var discordToken = os.Getenv("DISCORD_TOKEN")
// ===== Neu: gemeinsame Helper =====
func createPrivateVCAndMove(
s *discordgo.Session,
guildID, requesterID, displayName, categoryID string,
userLimit, timeoutMin int,
srcChannelID string, // aus welchem Channel wir verschieben (Lobby oder aktueller VC). Leer = kein Move.
) (*discordgo.Channel, error) {
everyoneID := guildID
allowOwner := int64(
discordgo.PermissionViewChannel |
discordgo.PermissionVoiceConnect |
discordgo.PermissionVoiceSpeak |
discordgo.PermissionVoiceUseVAD |
discordgo.PermissionVoiceStreamVideo,
)
denyEveryone := int64(
discordgo.PermissionViewChannel |
discordgo.PermissionVoiceConnect,
)
name := "🔒 " + displayName
newChan, err := s.GuildChannelCreateComplex(guildID, discordgo.GuildChannelCreateData{
Name: name,
Type: discordgo.ChannelTypeGuildVoice,
ParentID: categoryID,
UserLimit: userLimit,
Bitrate: 64000,
PermissionOverwrites: []*discordgo.PermissionOverwrite{
{ID: everyoneID, Type: discordgo.PermissionOverwriteTypeRole, Deny: denyEveryone},
{ID: requesterID, Type: discordgo.PermissionOverwriteTypeMember, Allow: allowOwner},
},
})
if err != nil {
return nil, fmt.Errorf("VC-Anlage fehlgeschlagen: %w", err)
}
// optionaler Move (nur wenn wir eine Quelle haben)
if srcChannelID != "" {
go func() {
time.Sleep(200 * time.Millisecond) // kurze Wartezeit nach Channel-Create
// (best-effort) Permissions prüfen
if perms, err := s.UserChannelPermissions(s.State.User.ID, srcChannelID); err == nil {
need := int64(discordgo.PermissionVoiceMoveMembers)
if perms&need == 0 {
log.Printf("WARN: Bot hat im Quell-Channel keine MoveMembers-Permission")
}
}
if perms, err := s.UserChannelPermissions(s.State.User.ID, newChan.ID); err == nil {
need := int64(discordgo.PermissionVoiceMoveMembers)
if perms&need == 0 {
log.Printf("WARN: Bot hat im Ziel-Channel keine MoveMembers-Permission")
}
}
// Retry mit Backoff
var moveErr error
for attempt := 0; attempt < 5; attempt++ {
moveErr = s.GuildMemberMove(guildID, requesterID, &newChan.ID)
if moveErr == nil {
break
}
wait := time.Duration(150*(attempt+1)) * time.Millisecond
log.Printf("Move fehlgeschlagen (Versuch %d): %v - retry in %s", attempt+1, moveErr, wait)
time.Sleep(wait)
}
if moveErr != nil {
log.Printf("Move endgültig fehlgeschlagen: %v", moveErr)
}
}()
}
// Auto-Cleanup
go watchAndCleanup(s, guildID, newChan.ID, time.Duration(timeoutMin)*time.Minute)
return newChan, nil
}
// Aktuellen Voice-Channel des Users finden (für Slash-Command)
func findUserVoiceChannelID(s *discordgo.Session, guildID, userID string) string {
g, err := s.State.Guild(guildID)
if err != nil {
g, err = s.Guild(guildID)
if err != nil {
return ""
}
}
for _, vs := range g.VoiceStates {
if vs.UserID == userID && vs.ChannelID != "" {
return vs.ChannelID
}
}
return ""
}
// /makevc Optionen: // /makevc Optionen:
// - name (string, optional) → Name des Voice-Channels // - name (string, optional) → Name des Voice-Channels
// - user_limit (int, optional) → Max. User (0 = unbegrenzt) // - user_limit (int, optional) → Max. User (0 = unbegrenzt)
@@ -54,9 +150,10 @@ 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) { func onVoiceStateUpdate(s *discordgo.Session, e *discordgo.VoiceStateUpdate) {
// nur Join-Events in die Lobby interessieren // nur Join-Events in die Lobby interessieren
if e.UserID == "" || e.ChannelID != lobbyID || e.BeforeUpdate != nil && e.BeforeUpdate.ChannelID == lobbyID { if e.UserID == "" || e.ChannelID != lobbyID || (e.BeforeUpdate != nil && e.BeforeUpdate.ChannelID == lobbyID) {
return return
} }
// Bots ignorieren // Bots ignorieren
@@ -65,77 +162,28 @@ func onVoiceStateUpdate(s *discordgo.Session, e *discordgo.VoiceStateUpdate) {
return return
} }
// Channel anlegen wie im /makevc-Handler: // Parameter laden
categoryID := os.Getenv(envCategoryID) categoryID := os.Getenv(envCategoryID)
name := "🔒 " + safeDisplayName(m)
userLimit := 0 userLimit := 0
timeoutMin := 60 timeoutMin := 60
if v := os.Getenv("TIMEOUT_MIN"); v != "" { if v := os.Getenv("TIMEOUT_MIN"); v != "" {
fmt.Sscanf(v, "%d", &timeoutMin) fmt.Sscanf(v, "%d", &timeoutMin)
} }
everyoneID := e.GuildID // Einheitliche Erstellung + Move (Quelle = Lobby)
allowOwner := int64(discordgo.PermissionViewChannel | _, err = createPrivateVCAndMove(
discordgo.PermissionVoiceConnect | s,
discordgo.PermissionVoiceSpeak | e.GuildID,
discordgo.PermissionVoiceStreamVideo | e.UserID,
discordgo.PermissionVoiceUseVAD) safeDisplayName(m),
denyEveryone := int64(discordgo.PermissionViewChannel | categoryID,
discordgo.PermissionVoiceConnect) userLimit,
timeoutMin,
newChan, err := s.GuildChannelCreateComplex(e.GuildID, discordgo.GuildChannelCreateData{ e.ChannelID, // Quell-Channel: Lobby
Name: name, )
Type: discordgo.ChannelTypeGuildVoice,
ParentID: categoryID,
UserLimit: userLimit,
Bitrate: 64000,
PermissionOverwrites: []*discordgo.PermissionOverwrite{
{ID: everyoneID, Type: discordgo.PermissionOverwriteTypeRole, Deny: denyEveryone},
{ID: e.UserID, Type: discordgo.PermissionOverwriteTypeMember, Allow: allowOwner},
},
})
if err != nil { if err != nil {
log.Printf("VC-Anlage fehlgeschlagen: %v", err) log.Printf("VC-Erstellung/Move fehlgeschlagen: %v", err)
return
} }
// User rüber bewegen (mit Perm-Check & Retry)
go func() {
// kurz warten, bis Discord den neuen Channel „fertig“ hat
time.Sleep(200 * time.Millisecond)
// Check: hat der Bot Move Members in Lobby & Ziel?
if perms, err := s.UserChannelPermissions(s.State.User.ID, e.ChannelID); err == nil {
need := int64(discordgo.PermissionVoiceMoveMembers)
if perms&need == 0 {
log.Printf("WARN: Bot hat im Lobby-Channel keine MoveMembers-Permission")
}
}
if perms, err := s.UserChannelPermissions(s.State.User.ID, newChan.ID); err == nil {
need := int64(discordgo.PermissionVoiceMoveMembers)
if perms&need == 0 {
log.Printf("WARN: Bot hat im Ziel-Channel keine MoveMembers-Permission")
}
}
// bis zu 5 Versuche mit steigendem Backoff
var moveErr error
for attempt := 0; attempt < 5; attempt++ {
moveErr = s.GuildMemberMove(e.GuildID, e.UserID, &newChan.ID)
if moveErr == nil {
break
}
wait := time.Duration(150*(attempt+1)) * time.Millisecond
log.Printf("Move fehlgeschlagen (Versuch %d): %v - retry in %s", attempt+1, moveErr, wait)
time.Sleep(wait)
}
if moveErr != nil {
log.Printf("Move endgültig fehlgeschlagen: %v", moveErr)
}
}()
// Auto-Cleanup starten
go watchAndCleanup(s, e.GuildID, newChan.ID, time.Duration(timeoutMin)*time.Minute)
} }
func safeDisplayName(m *discordgo.Member) string { func safeDisplayName(m *discordgo.Member) string {
@@ -201,6 +249,7 @@ func main() {
_ = s.Close() _ = 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(categoryID 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 {
@@ -210,8 +259,8 @@ func onInteractionCreate(categoryID string) func(s *discordgo.Session, i *discor
return return
} }
// Options lesen // Optionen lesen
var name string var name string // (wird nur für die Anzeige verwendet der Helper baut "🔒 <display>" selbst)
userLimit := 0 userLimit := 0
timeoutMin := 60 timeoutMin := 60
if v := os.Getenv("TIMEOUT_MIN"); v != "" { if v := os.Getenv("TIMEOUT_MIN"); v != "" {
@@ -227,63 +276,49 @@ func onInteractionCreate(categoryID string) func(s *discordgo.Session, i *discor
timeoutMin = int(o.IntValue()) timeoutMin = int(o.IntValue())
} }
} }
// Aufrufer bestimmen
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
} }
if name == "" { display := displayName(i)
name = fmt.Sprintf("🔒 %s", displayName(i)) if name != "" {
// Falls Name explizit gesetzt wurde, nimm ihn als Display-Basis
display = name
} }
// Permission Overwrites: @everyone deny, Owner allow // Wenn der User bereits in einem VC ist, verschieben wir ihn automatisch
guildID := i.GuildID srcVC := findUserVoiceChannelID(s, i.GuildID, user.ID) // "" wenn nicht in VC
everyoneID := guildID // @everyone Rolle hat die ID der Guild
allowOwner := int64(discordgo.PermissionViewChannel |
discordgo.PermissionVoiceConnect |
discordgo.PermissionVoiceSpeak |
discordgo.PermissionVoiceStreamVideo |
discordgo.PermissionVoiceUseVAD)
denyEveryone := int64(discordgo.PermissionViewChannel |
discordgo.PermissionVoiceConnect)
newChan, err := s.GuildChannelCreateComplex(guildID, discordgo.GuildChannelCreateData{ newChan, err := createPrivateVCAndMove(
Name: name, s,
Type: discordgo.ChannelTypeGuildVoice, i.GuildID,
// optional unter Kategorie user.ID,
ParentID: categoryID, display,
// Userlimit (0 = unlimited) categoryID,
UserLimit: userLimit, userLimit,
// Bitrate (optional): 64kbps ist sicher für die meisten Server timeoutMin,
Bitrate: 64000, srcVC, // nur wenn nicht leer wird moved
PermissionOverwrites: []*discordgo.PermissionOverwrite{ )
{
ID: everyoneID,
Type: discordgo.PermissionOverwriteTypeRole,
Deny: denyEveryone,
Allow: 0,
},
{
ID: user.ID,
Type: discordgo.PermissionOverwriteTypeMember,
Allow: allowOwner,
Deny: 0,
},
},
})
if err != nil { if err != nil {
errMsg := "Konnte Voice-Channel nicht erstellen. Fehlen dem Bot die Rechte **Manage Channels**?"
_ = s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{ _ = s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
Type: discordgo.InteractionResponseChannelMessageWithSource, Type: discordgo.InteractionResponseChannelMessageWithSource,
Data: &discordgo.InteractionResponseData{ Data: &discordgo.InteractionResponseData{
Content: errMsg, Content: "Konnte Voice-Channel nicht erstellen. Fehlen dem Bot Rechte (Manage Channels/Move Members)?",
Flags: discordgo.MessageFlagsEphemeral, Flags: discordgo.MessageFlagsEphemeral,
}, },
}) })
return return
} }
// Sofort antworten (ephemeral) msg := fmt.Sprintf("✅ Voice-Channel **%s** erstellt.", newChan.Name)
msg := fmt.Sprintf("✅ Voice-Channel **%s** erstellt. Ich lösche ihn, wenn er leer ist (Timeout: %d min).", newChan.Name, timeoutMin) 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{ _ = s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
Type: discordgo.InteractionResponseChannelMessageWithSource, Type: discordgo.InteractionResponseChannelMessageWithSource,
Data: &discordgo.InteractionResponseData{ Data: &discordgo.InteractionResponseData{
@@ -291,9 +326,6 @@ func onInteractionCreate(categoryID string) func(s *discordgo.Session, i *discor
Flags: discordgo.MessageFlagsEphemeral, Flags: discordgo.MessageFlagsEphemeral,
}, },
}) })
// Auto-Cleanup starten
go watchAndCleanup(s, i.GuildID, newChan.ID, time.Duration(timeoutMin)*time.Minute)
} }
} }