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:
1 parent
136377d81a
commit
a364435bab
15 files changed
+789
-52
No files matched your search
@@ -0,0 +1,86 @@
|
||||
// Package betacmd is the /beta command: it shows how beta access is set up.
|
||||
// It exists only in the beta server (see commands.BetaCommands), is hidden
|
||||
// from everyone but administrators in Discord's command picker, and only the
|
||||
// bot's owner can actually run it (the owner is looked up from the Discord
|
||||
// application, so there's nothing to configure).
|
||||
package betacmd
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"velox-bot/internal/beta"
|
||||
|
||||
"github.com/bwmarrin/discordgo"
|
||||
)
|
||||
|
||||
var (
|
||||
noDM = false
|
||||
noPerms = int64(0) // hides the command from everyone but administrators
|
||||
ownerIDs sync.Map // user ID -> struct{}, filled once from the application
|
||||
loadOnce sync.Mutex
|
||||
loaded bool
|
||||
)
|
||||
|
||||
var Beta = &discordgo.ApplicationCommand{
|
||||
Name: "beta",
|
||||
Description: "Show how beta access is set up (bot owner only)",
|
||||
DMPermission: &noDM,
|
||||
DefaultMemberPermissions: &noPerms,
|
||||
}
|
||||
|
||||
// isOwner reports whether the user owns the bot's Discord application (or is
|
||||
// on its team). If the application can't be read, nobody is the owner.
|
||||
func isOwner(s *discordgo.Session, userID string) bool {
|
||||
if userID == "" {
|
||||
return false
|
||||
}
|
||||
loadOnce.Lock()
|
||||
if !loaded {
|
||||
app, err := s.Application("@me")
|
||||
if err == nil && app != nil {
|
||||
if app.Owner != nil {
|
||||
ownerIDs.Store(app.Owner.ID, struct{}{})
|
||||
}
|
||||
if app.Team != nil {
|
||||
ownerIDs.Store(app.Team.OwnerID, struct{}{})
|
||||
for _, m := range app.Team.Members {
|
||||
if m != nil && m.User != nil {
|
||||
ownerIDs.Store(m.User.ID, struct{}{})
|
||||
}
|
||||
}
|
||||
}
|
||||
loaded = true
|
||||
}
|
||||
}
|
||||
loadOnce.Unlock()
|
||||
_, ok := ownerIDs.Load(userID)
|
||||
return ok
|
||||
}
|
||||
|
||||
func Handler(s *discordgo.Session, i *discordgo.InteractionCreate) {
|
||||
var userID string
|
||||
if i.Member != nil && i.Member.User != nil {
|
||||
userID = i.Member.User.ID
|
||||
}
|
||||
var msg string
|
||||
st := beta.StatusOf(i.GuildID, i.Member)
|
||||
switch {
|
||||
case !isOwner(s, userID):
|
||||
msg = "Only the bot's owner can use this command."
|
||||
case !st.InBetaGuild:
|
||||
msg = "Beta features aren't available here."
|
||||
case !st.RoleConfigured:
|
||||
msg = "No Beta Tester role is set up, so beta features are locked for everyone."
|
||||
case st.HasRole:
|
||||
msg = "You're a beta tester: beta features are unlocked for you in this server."
|
||||
default:
|
||||
msg = "You don't have the Beta Tester role, so beta features are locked for you."
|
||||
}
|
||||
_ = s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
|
||||
Type: discordgo.InteractionResponseChannelMessageWithSource,
|
||||
Data: &discordgo.InteractionResponseData{
|
||||
Content: msg,
|
||||
Flags: discordgo.MessageFlagsEphemeral,
|
||||
AllowedMentions: &discordgo.MessageAllowedMentions{},
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -2,6 +2,8 @@ package commands
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"velox-bot/internal/beta"
|
||||
"velox-bot/internal/commands/betacmd"
|
||||
"velox-bot/internal/commands/config"
|
||||
"velox-bot/internal/commands/fun"
|
||||
"velox-bot/internal/commands/help"
|
||||
@@ -11,8 +13,8 @@ import (
|
||||
cmdmusic "velox-bot/internal/commands/music"
|
||||
"velox-bot/internal/commands/projects"
|
||||
"velox-bot/internal/commands/schedule"
|
||||
cmdtickets "velox-bot/internal/commands/tickets"
|
||||
cmdsecurity "velox-bot/internal/commands/security"
|
||||
cmdtickets "velox-bot/internal/commands/tickets"
|
||||
"velox-bot/internal/commands/timezone"
|
||||
"velox-bot/internal/music"
|
||||
|
||||
@@ -79,6 +81,24 @@ var handlers = map[string]func(s *discordgo.Session, i *discordgo.InteractionCre
|
||||
"unlock": cmdsecurity.UnlockHandler,
|
||||
}
|
||||
|
||||
// BetaCommands are the beta features' slash commands. They're registered in
|
||||
// the beta server only, so nobody else even sees them, and each handler is
|
||||
// wrapped so it runs only for members with the Beta Tester role (see the beta
|
||||
// package). To put a feature in beta, add it with RegisterBeta instead of to
|
||||
// AllCommands and handlers; to release it, move it there.
|
||||
var BetaCommands = []*discordgo.ApplicationCommand{betacmd.Beta}
|
||||
|
||||
func init() {
|
||||
// /beta itself is open to the whole beta server (see betacmd).
|
||||
handlers[betacmd.Beta.Name] = betacmd.Handler
|
||||
}
|
||||
|
||||
// RegisterBeta adds a slash command that only beta testers can use.
|
||||
func RegisterBeta(cmd *discordgo.ApplicationCommand, h func(s *discordgo.Session, i *discordgo.InteractionCreate)) {
|
||||
BetaCommands = append(BetaCommands, cmd)
|
||||
handlers[cmd.Name] = beta.Gate(h)
|
||||
}
|
||||
|
||||
func HandleInteraction(s *discordgo.Session, i *discordgo.InteractionCreate) {
|
||||
switch i.Type {
|
||||
case discordgo.InteractionApplicationCommand:
|
||||
|
||||
@@ -106,7 +106,9 @@ func HelpHandler(s *discordgo.Session, i *discordgo.InteractionCreate) {
|
||||
{
|
||||
Name: "Meetings (**Manage Server**)",
|
||||
Value: "" +
|
||||
"`/meeting set-lobby channel:#voice` — Set voice lobby used to create meetings\n" +
|
||||
"`/meeting add-lobby channel:#voice` — Add a voice lobby that creates meetings (up to 10)\n" +
|
||||
"`/meeting remove-lobby channel:#voice` — Stop using a voice lobby\n" +
|
||||
"`/meeting lobbies` — List the voice lobbies\n" +
|
||||
"`/meeting lock` — Lock your meeting room\n" +
|
||||
"`/meeting lock-private` — Lock + hide your meeting room\n" +
|
||||
"`/meeting unlock` — Unlock your meeting room\n" +
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in new issue
Block a user