Files
velox-bot/internal/commands/config/public/extras.go
T
FernandoJVideiraandClaude Sonnet 5.5 132a2cd986 Commands for everything a dashboard can do
- /music skip, pause, resume, stop, repeat, remove, move (DJ role)
- /config view, clear, toggletwitch, streamers, beta-features (server owner)
- /slvl status, remove-reward; /moderation bans, timeouts; /security history
- /beta is now the bot owner's DM-only command: set, off, servers, remove-server, status
- betasettings service and repo methods, with integration tests

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-23 02:10:05 +01:00

215 lines
7.0 KiB
Go

package public
import (
"context"
"fmt"
"log"
"strconv"
"strings"
"time"
"velox-bot/internal/db/services"
"github.com/bwmarrin/discordgo"
)
// Everything else /config needs so that no setting depends on a dashboard:
// seeing the settings, clearing one, switching Twitch alerts on and off,
// listing streamers, and the server owner's beta features switch.
var clearChoices = []string{"welcome-channel", "welcome-message", "welcome-dm", "welcome-gif", "default-role", "log-channel"}
func extraOptions() []*discordgo.ApplicationCommandOption {
clear := &discordgo.ApplicationCommandOption{
Type: discordgo.ApplicationCommandOptionString, Name: "setting", Description: "What to clear", Required: true,
}
for _, c := range clearChoices {
clear.Choices = append(clear.Choices, &discordgo.ApplicationCommandOptionChoice{Name: c, Value: c})
}
return []*discordgo.ApplicationCommandOption{
{Type: discordgo.ApplicationCommandOptionSubCommand, Name: "view", Description: "Show the welcome, role, logging and Twitch settings"},
{Type: discordgo.ApplicationCommandOptionSubCommand, Name: "clear", Description: "Clear a welcome, role or log setting", Options: []*discordgo.ApplicationCommandOption{clear}},
{
Type: discordgo.ApplicationCommandOptionSubCommand, Name: "toggletwitch", Description: "Turn Twitch live notifications on or off",
Options: []*discordgo.ApplicationCommandOption{{Type: discordgo.ApplicationCommandOptionBoolean, Name: "enabled", Description: "On or off", Required: true}},
},
{Type: discordgo.ApplicationCommandOptionSubCommand, Name: "streamers", Description: "List the Twitch streamers being watched"},
{
Type: discordgo.ApplicationCommandOptionSubCommand, Name: "beta-features",
Description: "Let testers use beta features here. Only the server owner can change it",
Options: []*discordgo.ApplicationCommandOption{{Type: discordgo.ApplicationCommandOptionBoolean, Name: "enabled", Description: "On or off. Leave out to see the state"}},
},
}
}
func guildInt(i *discordgo.InteractionCreate) int64 {
n, _ := strconv.ParseInt(i.GuildID, 10, 64)
return n
}
func ownerOf(s *discordgo.Session, guildID string) (string, error) {
if g, err := s.State.Guild(guildID); err == nil && g != nil && g.OwnerID != "" {
return g.OwnerID, nil
}
g, err := s.Guild(guildID)
if err != nil {
return "", err
}
return g.OwnerID, nil
}
// handleExtra runs the subcommands above. It reports whether it handled name.
func handleExtra(s *discordgo.Session, i *discordgo.InteractionCreate, sub *discordgo.ApplicationCommandInteractionDataOption) bool {
ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
defer cancel()
g := guildInt(i)
svc := services.Global
args := map[string]*discordgo.ApplicationCommandInteractionDataOption{}
for _, o := range sub.Options {
args[o.Name] = o
}
fail := func(err error) {
respondEphemeral(s, i, "Something went wrong. Try again in a moment.")
log.Printf("config %s: %v", sub.Name, err)
}
switch sub.Name {
case "view":
var b strings.Builder
if w, err := svc.Welcome.GetSettings(ctx, g); err == nil {
fmt.Fprintf(&b, "**Welcome**\nChannel: %s\nMessage: %s\nDM: %s\nGIF: %s\n", mention("#", w.ChannelID), textOr(w.WelcomeMessage), textOr(w.WelcomeDM), textOr(w.WelcomeGIFURL))
}
if r, err := svc.DefaultRole.GetRole(ctx, g); err == nil {
fmt.Fprintf(&b, "**Default role:** %s\n", mention("@&", r))
}
if l, err := svc.LogSettings.GetSettings(ctx, g); err == nil && l != nil {
state := "off"
if l.IsEnabled {
state = "on"
}
fmt.Fprintf(&b, "**Logging:** %s, channel %s\n", state, mention("#", l.ChannelID))
} else {
b.WriteString("**Logging:** off\n")
}
if on, err := svc.Twitch.IsEnabled(ctx, g); err == nil {
ch, _, _ := svc.Twitch.GetNotificationChannel(ctx, g)
users, _ := svc.Twitch.ListUsersForGuild(ctx, g)
state := "off"
if on {
state = "on"
}
fmt.Fprintf(&b, "**Twitch alerts:** %s, channel %s, %d streamer(s)\n", state, mention("#", ch), len(users))
}
respondEphemeral(s, i, clipConfig(b.String(), 1900))
case "clear":
var err error
setting := args["setting"].StringValue()
switch setting {
case "welcome-channel":
err = svc.Welcome.SetChannel(ctx, g, 0)
case "welcome-message":
err = svc.Welcome.SetMessage(ctx, g, "")
case "welcome-dm":
err = svc.Welcome.SetDM(ctx, g, "")
case "welcome-gif":
err = svc.Welcome.SetGIF(ctx, g, "")
case "default-role":
err = svc.DefaultRole.SetRole(ctx, g, 0)
case "log-channel":
err = svc.LogSettings.SetChannel(ctx, g, 0)
default:
respondEphemeral(s, i, "Unknown setting.")
return true
}
if err != nil {
fail(err)
return true
}
respondEphemeral(s, i, "Cleared **"+setting+"**.")
case "toggletwitch":
on := args["enabled"].BoolValue()
if err := svc.Twitch.SetEnabled(ctx, g, on); err != nil {
fail(err)
return true
}
if on {
if ch, ok, _ := svc.Twitch.GetNotificationChannel(ctx, g); !ok || ch == 0 {
respondEphemeral(s, i, "Twitch alerts are **on**, but there is no channel yet. Set one with `/config settwitchnotificationchannel`.")
return true
}
respondEphemeral(s, i, "Twitch alerts are **on**.")
} else {
respondEphemeral(s, i, "Twitch alerts are **off**.")
}
case "streamers":
users, err := svc.Twitch.ListUsersForGuild(ctx, g)
if err != nil {
fail(err)
return true
}
if len(users) == 0 {
respondEphemeral(s, i, "No streamers yet. Add one with `/config addstreamer`.")
return true
}
respondEphemeral(s, i, clipConfig("Watching: "+strings.Join(users, ", "), 1900))
case "beta-features":
on, err := svc.BetaSettings.IsOptedIn(ctx, g)
if err != nil {
fail(err)
return true
}
state := "off"
if on {
state = "on"
}
want, ok := args["enabled"]
if !ok {
respondEphemeral(s, i, "Beta features are **"+state+"** in this server. Testers can use them here when this is on. Only the server owner can change it.")
return true
}
owner, err := ownerOf(s, i.GuildID)
if err != nil {
fail(err)
return true
}
if i.Member == nil || i.Member.User == nil || i.Member.User.ID != owner {
respondEphemeral(s, i, "Only the server owner can change this. Beta features can act on the whole server.")
return true
}
by, _ := strconv.ParseInt(owner, 10, 64)
if err := svc.BetaSettings.SetOptedIn(ctx, g, by, want.BoolValue()); err != nil {
fail(err)
return true
}
if want.BoolValue() {
respondEphemeral(s, i, "Beta features are **on**. Beta commands appear here within a minute, for testers only.")
} else {
respondEphemeral(s, i, "Beta features are **off**. Beta commands disappear from this server within a minute.")
}
default:
return false
}
return true
}
func mention(prefix string, id int64) string {
if id == 0 {
return "not set"
}
return fmt.Sprintf("<%s%d>", prefix, id)
}
func textOr(v string) string {
if strings.TrimSpace(v) == "" {
return "not set"
}
return "`" + clipConfig(v, 120) + "`"
}
func clipConfig(s string, n int) string {
if r := []rune(s); len(r) > n {
return string(r[:n-1]) + "…"
}
return s
}