- /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]>
107 lines
3.1 KiB
Go
107 lines
3.1 KiB
Go
package public
|
||
|
||
import (
|
||
"context"
|
||
"fmt"
|
||
"strings"
|
||
"time"
|
||
|
||
"github.com/bwmarrin/discordgo"
|
||
)
|
||
|
||
// Looking at who is banned and who is timed out, so unbanning and ending a
|
||
// timeout never needs a dashboard to find the person first.
|
||
|
||
func listOptions() []*discordgo.ApplicationCommandOption {
|
||
return []*discordgo.ApplicationCommandOption{
|
||
{Type: discordgo.ApplicationCommandOptionSubCommand, Name: "bans", Description: "List banned users, to use with /moderation unban"},
|
||
{Type: discordgo.ApplicationCommandOptionSubCommand, Name: "timeouts", Description: "List members who are timed out right now"},
|
||
}
|
||
}
|
||
|
||
const listShown = 20
|
||
|
||
func handleBans(s *discordgo.Session, i *discordgo.InteractionCreate) {
|
||
if !hasPerm(i, discordgo.PermissionBanMembers) {
|
||
respondEphemeral(s, i, "You need **Ban Members** for this command.")
|
||
return
|
||
}
|
||
bans, err := s.GuildBans(i.GuildID, 1000, "", "")
|
||
if err != nil {
|
||
respondEphemeral(s, i, "I couldn't read the ban list. I need the **Ban Members** permission.")
|
||
return
|
||
}
|
||
if len(bans) == 0 {
|
||
respondEphemeral(s, i, "Nobody is banned.")
|
||
return
|
||
}
|
||
var b strings.Builder
|
||
fmt.Fprintf(&b, "**Banned users (%d)**\n", len(bans))
|
||
for n, ban := range bans {
|
||
if n == listShown {
|
||
fmt.Fprintf(&b, "…and %d more.\n", len(bans)-listShown)
|
||
break
|
||
}
|
||
name := "unknown"
|
||
id := ""
|
||
if ban.User != nil {
|
||
name, id = ban.User.Username, ban.User.ID
|
||
}
|
||
reason := strings.TrimSpace(ban.Reason)
|
||
if reason == "" {
|
||
reason = "no reason"
|
||
}
|
||
fmt.Fprintf(&b, "• %s (`%s`): %s\n", escape(name), id, escape(clip(reason, 80)))
|
||
}
|
||
b.WriteString("Unban with `/moderation unban user_id:<id>`.")
|
||
respondEphemeral(s, i, clip(b.String(), 1950))
|
||
}
|
||
|
||
func handleTimeouts(s *discordgo.Session, i *discordgo.InteractionCreate) {
|
||
if !hasPerm(i, discordgo.PermissionModerateMembers) {
|
||
respondEphemeral(s, i, "You need **Moderate Members** for this command.")
|
||
return
|
||
}
|
||
_, cancel := context.WithTimeout(context.Background(), 20*time.Second)
|
||
defer cancel()
|
||
members, err := s.GuildMembers(i.GuildID, "", 1000)
|
||
if err != nil {
|
||
respondEphemeral(s, i, "I couldn't read the member list.")
|
||
return
|
||
}
|
||
now := time.Now()
|
||
var out []*discordgo.Member
|
||
for _, m := range members {
|
||
if m.CommunicationDisabledUntil != nil && m.CommunicationDisabledUntil.After(now) {
|
||
out = append(out, m)
|
||
}
|
||
}
|
||
if len(out) == 0 {
|
||
respondEphemeral(s, i, "Nobody is timed out (among the first 1000 members).")
|
||
return
|
||
}
|
||
var b strings.Builder
|
||
fmt.Fprintf(&b, "**Timed out (%d)**\n", len(out))
|
||
for n, m := range out {
|
||
if n == listShown {
|
||
fmt.Fprintf(&b, "…and %d more.\n", len(out)-listShown)
|
||
break
|
||
}
|
||
fmt.Fprintf(&b, "• <@%s> until <t:%d:f>\n", m.User.ID, m.CommunicationDisabledUntil.Unix())
|
||
}
|
||
b.WriteString("End one with `/moderation untimeout`.")
|
||
respondEphemeral(s, i, clip(b.String(), 1950))
|
||
}
|
||
|
||
func clip(s string, n int) string {
|
||
if r := []rune(s); len(r) > n {
|
||
return string(r[:n-1]) + "…"
|
||
}
|
||
return s
|
||
}
|
||
|
||
// escape keeps names and reasons from becoming markdown or mentions.
|
||
func escape(s string) string {
|
||
return strings.NewReplacer("`", "'", "*", "", "_", " ", "~", "", "@", "@", "|", "/").Replace(s)
|
||
}
|