Multiple meeting lobbies and beta-gated features (3.5.2)

- /meeting add-lobby, remove-lobby, lobbies: up to 10 lobbies per server, capped in the database
- Deleted voice channels are dropped from the lobbies
- Beta features: registered only in the beta server and usable only with the Beta Tester role (BETA_GUILD_ID, BETA_ROLE_ID); fails closed
- /beta (bot owner only) shows how beta access is set up

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
This commit is contained in:
FernandoJVideiraandClaude Sonnet 5.5 committed 2026-10-23 00:38:48 +01:00
1 parent 136377d81a
commit a364435bab
15 files changed
+789 -52

No files matched your search

+102 -16
View File
@@ -2,11 +2,14 @@ package public
import (
"context"
"errors"
"fmt"
"strconv"
"strings"
"velox-bot/internal/commands/meeting/shared"
"velox-bot/internal/db/services"
meetingsvc "velox-bot/internal/db/services/meeting"
"github.com/bwmarrin/discordgo"
)
@@ -17,18 +20,37 @@ var Meeting = &discordgo.ApplicationCommand{
Options: []*discordgo.ApplicationCommandOption{
{
Type: discordgo.ApplicationCommandOptionSubCommand,
Name: "set-lobby",
Description: "Set the voice lobby channel used to create meetings",
Name: "add-lobby",
Description: "Add a voice lobby: joining it creates a meeting room",
Options: []*discordgo.ApplicationCommandOption{
{
Type: discordgo.ApplicationCommandOptionChannel,
Name: "channel",
Description: "Voice channel lobby",
Description: "Voice channel to use as a lobby",
Required: true,
ChannelTypes: []discordgo.ChannelType{discordgo.ChannelTypeGuildVoice},
},
},
},
{
Type: discordgo.ApplicationCommandOptionSubCommand,
Name: "remove-lobby",
Description: "Stop using a voice channel as a lobby",
Options: []*discordgo.ApplicationCommandOption{
{
Type: discordgo.ApplicationCommandOptionChannel,
Name: "channel",
Description: "The lobby to remove",
Required: true,
ChannelTypes: []discordgo.ChannelType{discordgo.ChannelTypeGuildVoice},
},
},
},
{
Type: discordgo.ApplicationCommandOptionSubCommand,
Name: "lobbies",
Description: "List this server's meeting lobbies",
},
{
Type: discordgo.ApplicationCommandOptionSubCommand,
Name: "lock",
@@ -85,8 +107,12 @@ func MeetingHandler(s *discordgo.Session, i *discordgo.InteractionCreate) {
}
switch data.Options[0].Name {
case "set-lobby":
handleSetLobby(s, i)
case "add-lobby":
handleAddLobby(s, i)
case "remove-lobby":
handleRemoveLobby(s, i)
case "lobbies":
handleListLobbies(s, i)
case "lock":
handleLock(s, i)
case "lock-private":
@@ -102,37 +128,97 @@ func MeetingHandler(s *discordgo.Session, i *discordgo.InteractionCreate) {
}
}
func handleSetLobby(s *discordgo.Session, i *discordgo.InteractionCreate) {
guildID, ok := shared.ParseGuildID(s, i)
if !ok {
return
}
// lobbyChannelOption reads the "channel" option of add-lobby and
// remove-lobby, answering the user itself if it's missing or not a voice
// channel.
func lobbyChannelOption(s *discordgo.Session, i *discordgo.InteractionCreate) (*discordgo.Channel, int64, bool) {
optMap := shared.SubcommandOptionMap(i)
channelOpt, ok := optMap["channel"]
if !ok {
shared.RespondEphemeral(s, i, "Missing channel option.")
return
return nil, 0, false
}
ch := channelOpt.ChannelValue(s)
if ch == nil || ch.Type != discordgo.ChannelTypeGuildVoice {
shared.RespondEphemeral(s, i, "Invalid voice channel.")
return
return nil, 0, false
}
channelID, err := strconv.ParseInt(ch.ID, 10, 64)
if err != nil {
shared.RespondEphemeral(s, i, "Invalid channel ID.")
return nil, 0, false
}
return ch, channelID, true
}
func handleAddLobby(s *discordgo.Session, i *discordgo.InteractionCreate) {
guildID, ok := shared.ParseGuildID(s, i)
if !ok {
return
}
ch, channelID, ok := lobbyChannelOption(s, i)
if !ok {
return
}
if err := services.Global.Meeting.SetLobbyChannel(context.Background(), guildID, channelID); err != nil {
shared.RespondEphemeral(s, i, "Failed to set meeting lobby.")
added, err := services.Global.Meeting.AddLobby(context.Background(), guildID, channelID)
switch {
case errors.Is(err, meetingsvc.ErrLobbyLimit):
shared.RespondEphemeral(s, i, fmt.Sprintf("This server already has %d lobbies. Remove one with `/meeting remove-lobby` first.", meetingsvc.MaxLobbies))
case err != nil:
shared.RespondEphemeral(s, i, "Failed to add the meeting lobby.")
case !added:
shared.RespondEphemeral(s, i, ch.Mention()+" is already a meeting lobby.")
default:
shared.RespondEphemeral(s, i, ch.Mention()+" is now a meeting lobby.")
}
}
func handleRemoveLobby(s *discordgo.Session, i *discordgo.InteractionCreate) {
guildID, ok := shared.ParseGuildID(s, i)
if !ok {
return
}
ch, channelID, ok := lobbyChannelOption(s, i)
if !ok {
return
}
shared.RespondEphemeral(s, i, "Meeting lobby set to "+ch.Mention()+".")
removed, err := services.Global.Meeting.RemoveLobby(context.Background(), guildID, channelID)
switch {
case err != nil:
shared.RespondEphemeral(s, i, "Failed to remove the meeting lobby.")
case !removed:
shared.RespondEphemeral(s, i, ch.Mention()+" isn't a meeting lobby.")
default:
shared.RespondEphemeral(s, i, ch.Mention()+" is no longer a meeting lobby.")
}
}
func handleListLobbies(s *discordgo.Session, i *discordgo.InteractionCreate) {
guildID, ok := shared.ParseGuildID(s, i)
if !ok {
return
}
ids, err := services.Global.Meeting.ListLobbies(context.Background(), guildID)
if err != nil {
shared.RespondEphemeral(s, i, "Failed to list the meeting lobbies.")
return
}
if len(ids) == 0 {
shared.RespondEphemeral(s, i, "No meeting lobbies yet. Add one with `/meeting add-lobby`.")
return
}
var b strings.Builder
fmt.Fprintf(&b, "Meeting lobbies (%d of %d):\n", len(ids), meetingsvc.MaxLobbies)
for _, id := range ids {
fmt.Fprintf(&b, "<#%d>\n", id)
}
shared.RespondEphemeral(s, i, b.String())
}
func handleLock(s *discordgo.Session, i *discordgo.InteractionCreate) {