- /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]>
215 lines
7.0 KiB
Go
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
|
|
}
|