117 lines
3.4 KiB
Go
117 lines
3.4 KiB
Go
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)
|
|
}
|
|
|