package events import ( "context" "fmt" "log" "strconv" "time" "velox-bot/internal/db/services" "github.com/bwmarrin/discordgo" ) // HandleVoiceStateUpdate implements the "meeting lobby" behavior: // - When a user joins one of the server's lobby voice channels, create a temporary voice channel // with the same UserLimit and move the user into it. // - When a temporary channel becomes empty, delete it. func HandleVoiceStateUpdate(s *discordgo.Session, vs *discordgo.VoiceStateUpdate, svc *services.Services) { if svc == nil || svc.Meeting == nil { return } if vs == nil || vs.VoiceState == nil { return } if vs.GuildID == "" || vs.UserID == "" { return } ctx := context.Background() guildID64, _ := strconv.ParseInt(vs.GuildID, 10, 64) // Clean up the channel the user left (if any) if vs.BeforeUpdate != nil && vs.BeforeUpdate.ChannelID != "" { tryCleanupTempChannel(s, ctx, svc, vs.GuildID, guildID64, vs.BeforeUpdate.ChannelID) } // If not joining a channel (disconnect), nothing else to do. if vs.ChannelID == "" { return } joinedID, err := strconv.ParseInt(vs.ChannelID, 10, 64) if err != nil { return } isLobby, err := svc.Meeting.IsLobby(ctx, guildID64, joinedID) if err != nil || !isLobby { return } lobbyCh, err := s.Channel(vs.ChannelID) if err != nil || lobbyCh == nil { return } // Create the new channel in the same category as the lobby, with same user limit. newName := fmt.Sprintf("%s • %s", lobbyCh.Name, time.Now().Format("15:04")) newCh, err := s.GuildChannelCreateComplex(vs.GuildID, discordgo.GuildChannelCreateData{ Name: newName, Type: discordgo.ChannelTypeGuildVoice, ParentID: lobbyCh.ParentID, UserLimit: lobbyCh.UserLimit, Bitrate: lobbyCh.Bitrate, }) if err != nil || newCh == nil { log.Printf("meeting: failed creating temp channel guild=%s user=%s err=%v", vs.GuildID, vs.UserID, err) return } newChID64, _ := strconv.ParseInt(newCh.ID, 10, 64) _ = svc.Meeting.AddTempChannel(ctx, guildID64, newChID64, mustParseInt64(vs.UserID)) // Move the user into the new channel. targetID := newCh.ID if err := s.GuildMemberMove(vs.GuildID, vs.UserID, &targetID); err != nil { log.Printf("meeting: failed moving member guild=%s user=%s channel=%s err=%v", vs.GuildID, vs.UserID, newCh.ID, err) // If move fails, delete the channel to avoid junk. _, _ = s.ChannelDelete(newCh.ID) _ = svc.Meeting.RemoveTempChannel(ctx, guildID64, newChID64) return } } func tryCleanupTempChannel(s *discordgo.Session, ctx context.Context, svc *services.Services, guildID string, guildID64 int64, channelID string) { chID64, err := strconv.ParseInt(channelID, 10, 64) if err != nil || chID64 == 0 { return } isTemp, err := svc.Meeting.IsTempChannel(ctx, guildID64, chID64) if err != nil || !isTemp { return } // Check if anyone is still in the channel. // IMPORTANT: don't use REST `s.Guild(...)` here; it doesn't include VoiceStates. // Use the session state cache (requires IntentsGuildVoiceStates). if s == nil || s.State == nil { return } g, err := s.State.Guild(guildID) if err != nil || g == nil { return } for _, st := range g.VoiceStates { if st != nil && st.ChannelID == channelID { return } } if _, err := s.ChannelDelete(channelID); err != nil { // If already deleted, still remove from DB. log.Printf("meeting: failed deleting empty temp channel guild=%s channel=%s err=%v", guildID, channelID, err) } _ = svc.Meeting.RemoveTempChannel(ctx, guildID64, chID64) } // HandleChannelDelete drops a deleted voice channel from the meeting lobbies, // so it doesn't keep using up one of the server's slots. func HandleChannelDelete(_ *discordgo.Session, c *discordgo.ChannelDelete, svc *services.Services) { if svc == nil || svc.Meeting == nil || c == nil || c.Channel == nil || c.GuildID == "" { return } if c.Type != discordgo.ChannelTypeGuildVoice { return } guildID, err1 := strconv.ParseInt(c.GuildID, 10, 64) channelID, err2 := strconv.ParseInt(c.ID, 10, 64) if err1 != nil || err2 != nil { return } if _, err := svc.Meeting.RemoveLobby(context.Background(), guildID, channelID); err != nil { log.Printf("meeting: removing deleted lobby guild=%s channel=%s err=%v", c.GuildID, c.ID, err) } }