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 the configured lobby voice channel, 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 } lobbyID, ok, err := svc.Meeting.GetLobbyChannel(ctx, guildID64) if err != nil || !ok || lobbyID == 0 { return } if vs.ChannelID != strconv.FormatInt(lobbyID, 10) { 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) }