This commit is contained in:
248
main.go
248
main.go
@@ -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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user