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]>
This commit is contained in:
1 parent
acdb5ead35
commit
132a2cd986
26 files changed
+1062
-65
No files matched your search
@@ -14,7 +14,7 @@ import (
|
||||
var Config = &discordgo.ApplicationCommand{
|
||||
Name: "config",
|
||||
Description: "Server configuration",
|
||||
Options: []*discordgo.ApplicationCommandOption{
|
||||
Options: append([]*discordgo.ApplicationCommandOption{
|
||||
{
|
||||
Type: discordgo.ApplicationCommandOptionSubCommand,
|
||||
Name: "addstreamer",
|
||||
@@ -145,7 +145,7 @@ var Config = &discordgo.ApplicationCommand{
|
||||
Name: "disablelogging",
|
||||
Description: "Disable server logging events",
|
||||
},
|
||||
},
|
||||
}, extraOptions()...),
|
||||
}
|
||||
|
||||
func ConfigHandler(s *discordgo.Session, i *discordgo.InteractionCreate) {
|
||||
@@ -168,6 +168,10 @@ func ConfigHandler(s *discordgo.Session, i *discordgo.InteractionCreate) {
|
||||
return
|
||||
}
|
||||
|
||||
if handleExtra(s, i, data.Options[0]) {
|
||||
return
|
||||
}
|
||||
|
||||
switch data.Options[0].Name {
|
||||
case "addstreamer":
|
||||
log.Printf("twitch: /config addstreamer invoked by %s in guild %s", i.Member.User.ID, i.GuildID)
|
||||
|
||||
@@ -0,0 +1,214 @@
|
||||
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
|
||||
}
|
||||
Reference in new issue
Block a user