Files
velox-bot/internal/commands/meeting/public/meeting.go
T

333 lines
9.0 KiB
Go

package public
import (
"context"
"fmt"
"strconv"
"velox-bot/internal/commands/meeting/shared"
"velox-bot/internal/db/services"
"github.com/bwmarrin/discordgo"
)
var Meeting = &discordgo.ApplicationCommand{
Name: "meeting",
Description: "Meeting room settings",
Options: []*discordgo.ApplicationCommandOption{
{
Type: discordgo.ApplicationCommandOptionSubCommand,
Name: "set-lobby",
Description: "Set the voice lobby channel used to create meetings",
Options: []*discordgo.ApplicationCommandOption{
{
Type: discordgo.ApplicationCommandOptionChannel,
Name: "channel",
Description: "Voice channel lobby",
Required: true,
ChannelTypes: []discordgo.ChannelType{discordgo.ChannelTypeGuildVoice},
},
},
},
{
Type: discordgo.ApplicationCommandOptionSubCommand,
Name: "lock",
Description: "Lock your current meeting room (block others from joining)",
},
{
Type: discordgo.ApplicationCommandOptionSubCommand,
Name: "lock-private",
Description: "Lock and hide your meeting room (block joining and visibility)",
},
{
Type: discordgo.ApplicationCommandOptionSubCommand,
Name: "unlock",
Description: "Unlock your current meeting room (allow others to join)",
},
{
Type: discordgo.ApplicationCommandOptionSubCommand,
Name: "invite",
Description: "Allow a user to join your locked meeting room",
Options: []*discordgo.ApplicationCommandOption{
{
Type: discordgo.ApplicationCommandOptionUser,
Name: "user",
Description: "User to allow",
Required: true,
},
},
},
{
Type: discordgo.ApplicationCommandOptionSubCommand,
Name: "uninvite",
Description: "Remove a previously invited user's permission to join",
Options: []*discordgo.ApplicationCommandOption{
{
Type: discordgo.ApplicationCommandOptionUser,
Name: "user",
Description: "User to remove",
Required: true,
},
},
},
},
}
func MeetingHandler(s *discordgo.Session, i *discordgo.InteractionCreate) {
if !shared.RequireGuild(s, i) || !shared.RequireManageGuild(s, i) || !shared.RequireMeetingService(s, i) {
return
}
data := i.ApplicationCommandData()
if len(data.Options) == 0 {
shared.RespondEphemeral(s, i, "Missing subcommand.")
return
}
switch data.Options[0].Name {
case "set-lobby":
handleSetLobby(s, i)
case "lock":
handleLock(s, i)
case "lock-private":
handleLockPrivate(s, i)
case "unlock":
handleUnlock(s, i)
case "invite":
handleInvite(s, i)
case "uninvite":
handleUninvite(s, i)
default:
shared.RespondEphemeral(s, i, "Unknown subcommand.")
}
}
func handleSetLobby(s *discordgo.Session, i *discordgo.InteractionCreate) {
guildID, ok := shared.ParseGuildID(s, i)
if !ok {
return
}
optMap := shared.SubcommandOptionMap(i)
channelOpt, ok := optMap["channel"]
if !ok {
shared.RespondEphemeral(s, i, "Missing channel option.")
return
}
ch := channelOpt.ChannelValue(s)
if ch == nil || ch.Type != discordgo.ChannelTypeGuildVoice {
shared.RespondEphemeral(s, i, "Invalid voice channel.")
return
}
channelID, err := strconv.ParseInt(ch.ID, 10, 64)
if err != nil {
shared.RespondEphemeral(s, i, "Invalid channel ID.")
return
}
if err := services.Global.Meeting.SetLobbyChannel(context.Background(), guildID, channelID); err != nil {
shared.RespondEphemeral(s, i, "Failed to set meeting lobby.")
return
}
shared.RespondEphemeral(s, i, "Meeting lobby set to "+ch.Mention()+".")
}
func handleLock(s *discordgo.Session, i *discordgo.InteractionCreate) {
ch, chID64, guildID64, ok := requireOwnTempVoiceChannel(s, i)
if !ok {
return
}
// Deny CONNECT for @everyone (guild ID).
if err := s.ChannelPermissionSet(
ch.ID,
i.GuildID,
discordgo.PermissionOverwriteTypeRole,
0,
discordgo.PermissionVoiceConnect,
); err != nil {
shared.RespondEphemeral(s, i, "Failed to lock the meeting room.")
return
}
shared.RespondEphemeral(s, i, "Locked "+ch.Mention()+". Use `/meeting invite user:@someone` to allow specific people.")
_ = chID64
_ = guildID64
}
func handleLockPrivate(s *discordgo.Session, i *discordgo.InteractionCreate) {
ch, _, _, ok := requireOwnTempVoiceChannel(s, i)
if !ok {
return
}
// Deny CONNECT and VIEW_CHANNEL for @everyone (guild ID).
deny := int64(discordgo.PermissionVoiceConnect | discordgo.PermissionViewChannel)
if err := s.ChannelPermissionSet(
ch.ID,
i.GuildID,
discordgo.PermissionOverwriteTypeRole,
0,
deny,
); err != nil {
shared.RespondEphemeral(s, i, "Failed to lock the meeting room.")
return
}
shared.RespondEphemeral(s, i, "Locked (private) "+ch.Mention()+". Use `/meeting invite user:@someone` to allow specific people.")
}
func handleUnlock(s *discordgo.Session, i *discordgo.InteractionCreate) {
ch, _, _, ok := requireOwnTempVoiceChannel(s, i)
if !ok {
return
}
// Remove the @everyone overwrite so defaults apply again.
if err := s.ChannelPermissionDelete(ch.ID, i.GuildID); err != nil {
shared.RespondEphemeral(s, i, "Failed to unlock the meeting room.")
return
}
shared.RespondEphemeral(s, i, "Unlocked "+ch.Mention()+".")
}
func handleInvite(s *discordgo.Session, i *discordgo.InteractionCreate) {
ch, _, _, ok := requireOwnTempVoiceChannel(s, i)
if !ok {
return
}
optMap := shared.SubcommandOptionMap(i)
userOpt, ok := optMap["user"]
if !ok {
shared.RespondEphemeral(s, i, "Missing user option.")
return
}
u := userOpt.UserValue(s)
if u == nil {
shared.RespondEphemeral(s, i, "Invalid user.")
return
}
// Allow CONNECT for the invited user.
if err := s.ChannelPermissionSet(
ch.ID,
u.ID,
discordgo.PermissionOverwriteTypeMember,
discordgo.PermissionVoiceConnect,
0,
); err != nil {
shared.RespondEphemeral(s, i, "Failed to invite user.")
return
}
shared.RespondEphemeral(s, i, "Invited "+u.Mention()+" to "+ch.Mention()+".")
}
func handleUninvite(s *discordgo.Session, i *discordgo.InteractionCreate) {
ch, _, _, ok := requireOwnTempVoiceChannel(s, i)
if !ok {
return
}
optMap := shared.SubcommandOptionMap(i)
userOpt, ok := optMap["user"]
if !ok {
shared.RespondEphemeral(s, i, "Missing user option.")
return
}
u := userOpt.UserValue(s)
if u == nil {
shared.RespondEphemeral(s, i, "Invalid user.")
return
}
// Remove any member-specific overwrite for this user.
if err := s.ChannelPermissionDelete(ch.ID, u.ID); err != nil {
shared.RespondEphemeral(s, i, "Failed to remove invite.")
return
}
shared.RespondEphemeral(s, i, "Removed "+u.Mention()+" from "+ch.Mention()+".")
}
func requireOwnTempVoiceChannel(s *discordgo.Session, i *discordgo.InteractionCreate) (channel *discordgo.Channel, channelID64 int64, guildID64 int64, ok bool) {
guildID64, ok = shared.ParseGuildID(s, i)
if !ok {
return nil, 0, 0, false
}
if i.Member == nil || i.Member.User == nil {
shared.RespondEphemeral(s, i, "Missing member info.")
return nil, 0, 0, false
}
voiceChannelID, found := findMemberVoiceChannelID(s, i.GuildID, i.Member.User.ID)
if !found || voiceChannelID == "" {
shared.RespondEphemeral(s, i, "You must be in your meeting voice channel to use this.")
return nil, 0, 0, false
}
ch, err := s.Channel(voiceChannelID)
if err != nil || ch == nil {
shared.RespondEphemeral(s, i, "Failed to load your voice channel.")
return nil, 0, 0, false
}
if ch.Type != discordgo.ChannelTypeGuildVoice {
shared.RespondEphemeral(s, i, "This only works in a voice channel.")
return nil, 0, 0, false
}
channelID64, err = strconv.ParseInt(ch.ID, 10, 64)
if err != nil {
shared.RespondEphemeral(s, i, "Invalid channel ID.")
return nil, 0, 0, false
}
isTemp, err := services.Global.Meeting.IsTempChannel(context.Background(), guildID64, channelID64)
if err != nil || !isTemp {
shared.RespondEphemeral(s, i, "This is not a bot-created meeting room.")
return nil, 0, 0, false
}
ownerID, ownerOK, err := services.Global.Meeting.GetTempChannelOwner(context.Background(), guildID64, channelID64)
if err != nil || !ownerOK {
shared.RespondEphemeral(s, i, "Failed to verify meeting room owner.")
return nil, 0, 0, false
}
invokerID64, _ := strconv.ParseInt(i.Member.User.ID, 10, 64)
if invokerID64 == 0 || ownerID != invokerID64 {
shared.RespondEphemeral(s, i, fmt.Sprintf("Only the room creator can do this. (creator: <@%d>)", ownerID))
return nil, 0, 0, false
}
return ch, channelID64, guildID64, true
}
func findMemberVoiceChannelID(s *discordgo.Session, guildID, userID string) (string, bool) {
// Prefer session state cache (discordgo tracks voice states there when IntentsGuildVoiceStates is enabled).
if s != nil && s.State != nil {
if vs, err := s.State.VoiceState(guildID, userID); err == nil && vs != nil {
if vs.ChannelID != "" {
return vs.ChannelID, true
}
return "", false
}
// Fallback to cached guild object (State, not REST).
if g, err := s.State.Guild(guildID); err == nil && g != nil {
for _, st := range g.VoiceStates {
if st != nil && st.UserID == userID {
return st.ChannelID, st.ChannelID != ""
}
}
}
}
return "", false
}