- Introduced a new set of moderation commands including `/moderation purge`, `/moderation timeout`, `/moderation kick`, `/moderation ban`, `/moderation unban`, `/moderation untimeout`, and `/moderation slowmode`. - Updated README to include details about the new moderation features and commands. - Enhanced help command to display moderation tools and their usage.
541 lines
14 KiB
Go
541 lines
14 KiB
Go
package public
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/bwmarrin/discordgo"
|
|
)
|
|
|
|
var Moderation = &discordgo.ApplicationCommand{
|
|
Name: "moderation",
|
|
Description: "High-value moderation tools",
|
|
Options: []*discordgo.ApplicationCommandOption{
|
|
{
|
|
Type: discordgo.ApplicationCommandOptionSubCommand,
|
|
Name: "purge",
|
|
Description: "Delete recent messages in this channel (max 100)",
|
|
Options: []*discordgo.ApplicationCommandOption{
|
|
{
|
|
Type: discordgo.ApplicationCommandOptionInteger,
|
|
Name: "amount",
|
|
Description: "How many recent messages to delete (1-100)",
|
|
Required: true,
|
|
},
|
|
},
|
|
},
|
|
{
|
|
Type: discordgo.ApplicationCommandOptionSubCommand,
|
|
Name: "timeout",
|
|
Description: "Temporarily timeout a member",
|
|
Options: []*discordgo.ApplicationCommandOption{
|
|
{
|
|
Type: discordgo.ApplicationCommandOptionUser,
|
|
Name: "user",
|
|
Description: "User to timeout",
|
|
Required: true,
|
|
},
|
|
{
|
|
Type: discordgo.ApplicationCommandOptionString,
|
|
Name: "duration",
|
|
Description: "Duration like 10m, 1h, 2h30m (max 28d)",
|
|
Required: true,
|
|
},
|
|
{
|
|
Type: discordgo.ApplicationCommandOptionString,
|
|
Name: "reason",
|
|
Description: "Optional reason",
|
|
Required: false,
|
|
},
|
|
},
|
|
},
|
|
{
|
|
Type: discordgo.ApplicationCommandOptionSubCommand,
|
|
Name: "kick",
|
|
Description: "Kick a member from the server",
|
|
Options: []*discordgo.ApplicationCommandOption{
|
|
{
|
|
Type: discordgo.ApplicationCommandOptionUser,
|
|
Name: "user",
|
|
Description: "User to kick",
|
|
Required: true,
|
|
},
|
|
{
|
|
Type: discordgo.ApplicationCommandOptionString,
|
|
Name: "reason",
|
|
Description: "Optional reason",
|
|
Required: false,
|
|
},
|
|
},
|
|
},
|
|
{
|
|
Type: discordgo.ApplicationCommandOptionSubCommand,
|
|
Name: "ban",
|
|
Description: "Ban a member from the server",
|
|
Options: []*discordgo.ApplicationCommandOption{
|
|
{
|
|
Type: discordgo.ApplicationCommandOptionUser,
|
|
Name: "user",
|
|
Description: "User to ban",
|
|
Required: true,
|
|
},
|
|
{
|
|
Type: discordgo.ApplicationCommandOptionInteger,
|
|
Name: "delete_days",
|
|
Description: "Delete message history (0-7 days)",
|
|
Required: false,
|
|
},
|
|
{
|
|
Type: discordgo.ApplicationCommandOptionString,
|
|
Name: "reason",
|
|
Description: "Optional reason",
|
|
Required: false,
|
|
},
|
|
},
|
|
},
|
|
{
|
|
Type: discordgo.ApplicationCommandOptionSubCommand,
|
|
Name: "unban",
|
|
Description: "Unban a user by ID",
|
|
Options: []*discordgo.ApplicationCommandOption{
|
|
{
|
|
Type: discordgo.ApplicationCommandOptionString,
|
|
Name: "user_id",
|
|
Description: "User ID to unban",
|
|
Required: true,
|
|
},
|
|
{
|
|
Type: discordgo.ApplicationCommandOptionString,
|
|
Name: "reason",
|
|
Description: "Optional reason",
|
|
Required: false,
|
|
},
|
|
},
|
|
},
|
|
{
|
|
Type: discordgo.ApplicationCommandOptionSubCommand,
|
|
Name: "untimeout",
|
|
Description: "Remove timeout from a member",
|
|
Options: []*discordgo.ApplicationCommandOption{
|
|
{
|
|
Type: discordgo.ApplicationCommandOptionUser,
|
|
Name: "user",
|
|
Description: "User to untimeout",
|
|
Required: true,
|
|
},
|
|
{
|
|
Type: discordgo.ApplicationCommandOptionString,
|
|
Name: "reason",
|
|
Description: "Optional reason",
|
|
Required: false,
|
|
},
|
|
},
|
|
},
|
|
{
|
|
Type: discordgo.ApplicationCommandOptionSubCommand,
|
|
Name: "slowmode",
|
|
Description: "Set channel slowmode (0 disables)",
|
|
Options: []*discordgo.ApplicationCommandOption{
|
|
{
|
|
Type: discordgo.ApplicationCommandOptionChannel,
|
|
Name: "channel",
|
|
Description: "Text channel to update",
|
|
Required: true,
|
|
ChannelTypes: []discordgo.ChannelType{discordgo.ChannelTypeGuildText, discordgo.ChannelTypeGuildNews},
|
|
},
|
|
{
|
|
Type: discordgo.ApplicationCommandOptionInteger,
|
|
Name: "seconds",
|
|
Description: "Slowmode delay in seconds (0-21600)",
|
|
Required: true,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
func ModerationHandler(s *discordgo.Session, i *discordgo.InteractionCreate) {
|
|
if i.GuildID == "" {
|
|
respondEphemeral(s, i, "This command can only be used in a server.")
|
|
return
|
|
}
|
|
|
|
data := i.ApplicationCommandData()
|
|
if len(data.Options) == 0 {
|
|
respondEphemeral(s, i, "Missing subcommand.")
|
|
return
|
|
}
|
|
|
|
switch data.Options[0].Name {
|
|
case "purge":
|
|
handlePurge(s, i)
|
|
case "timeout":
|
|
handleTimeout(s, i)
|
|
case "kick":
|
|
handleKick(s, i)
|
|
case "ban":
|
|
handleBan(s, i)
|
|
case "unban":
|
|
handleUnban(s, i)
|
|
case "untimeout":
|
|
handleUntimeout(s, i)
|
|
case "slowmode":
|
|
handleSlowmode(s, i)
|
|
default:
|
|
respondEphemeral(s, i, "Unknown subcommand.")
|
|
}
|
|
}
|
|
|
|
func handlePurge(s *discordgo.Session, i *discordgo.InteractionCreate) {
|
|
if !hasPerm(i, discordgo.PermissionManageMessages) {
|
|
respondEphemeral(s, i, "You need **Manage Messages** for this command.")
|
|
return
|
|
}
|
|
|
|
opt := i.ApplicationCommandData().Options[0]
|
|
var amount int64
|
|
for _, o := range opt.Options {
|
|
if o.Name == "amount" {
|
|
amount = o.IntValue()
|
|
break
|
|
}
|
|
}
|
|
if amount < 1 || amount > 100 {
|
|
respondEphemeral(s, i, "Amount must be between 1 and 100.")
|
|
return
|
|
}
|
|
|
|
msgs, err := s.ChannelMessages(i.ChannelID, int(amount), "", "", "")
|
|
if err != nil {
|
|
respondEphemeral(s, i, "Failed to fetch messages.")
|
|
return
|
|
}
|
|
if len(msgs) == 0 {
|
|
respondEphemeral(s, i, "No messages found to delete.")
|
|
return
|
|
}
|
|
|
|
tooOldCutoff := time.Now().Add(-14 * 24 * time.Hour)
|
|
ids := make([]string, 0, len(msgs))
|
|
for _, m := range msgs {
|
|
if m == nil {
|
|
continue
|
|
}
|
|
if m.Timestamp.Before(tooOldCutoff) {
|
|
continue
|
|
}
|
|
ids = append(ids, m.ID)
|
|
}
|
|
if len(ids) == 0 {
|
|
respondEphemeral(s, i, "No messages can be bulk deleted (older than 14 days).")
|
|
return
|
|
}
|
|
|
|
if len(ids) == 1 {
|
|
if err := s.ChannelMessageDelete(i.ChannelID, ids[0]); err != nil {
|
|
respondEphemeral(s, i, "Failed to delete message.")
|
|
return
|
|
}
|
|
respondEphemeral(s, i, "Deleted 1 message.")
|
|
return
|
|
}
|
|
|
|
if err := s.ChannelMessagesBulkDelete(i.ChannelID, ids); err != nil {
|
|
respondEphemeral(s, i, "Failed to bulk delete messages.")
|
|
return
|
|
}
|
|
|
|
respondEphemeral(s, i, fmt.Sprintf("Deleted %d messages.", len(ids)))
|
|
}
|
|
|
|
func handleTimeout(s *discordgo.Session, i *discordgo.InteractionCreate) {
|
|
if !hasPerm(i, discordgo.PermissionModerateMembers) {
|
|
respondEphemeral(s, i, "You need **Moderate Members** for this command.")
|
|
return
|
|
}
|
|
|
|
opt := i.ApplicationCommandData().Options[0]
|
|
var userOpt *discordgo.ApplicationCommandInteractionDataOption
|
|
var durationInput string
|
|
var reason string
|
|
for _, o := range opt.Options {
|
|
switch o.Name {
|
|
case "user":
|
|
userOpt = o
|
|
case "duration":
|
|
durationInput = strings.TrimSpace(o.StringValue())
|
|
case "reason":
|
|
reason = strings.TrimSpace(o.StringValue())
|
|
}
|
|
}
|
|
if userOpt == nil {
|
|
respondEphemeral(s, i, "Missing user option.")
|
|
return
|
|
}
|
|
target := userOpt.UserValue(s)
|
|
if target == nil {
|
|
respondEphemeral(s, i, "Invalid user.")
|
|
return
|
|
}
|
|
|
|
dur, err := time.ParseDuration(durationInput)
|
|
if err != nil || dur <= 0 {
|
|
respondEphemeral(s, i, "Invalid duration. Examples: `10m`, `1h`, `2h30m`.")
|
|
return
|
|
}
|
|
maxTimeout := 28 * 24 * time.Hour
|
|
if dur > maxTimeout {
|
|
respondEphemeral(s, i, "Duration cannot exceed 28 days.")
|
|
return
|
|
}
|
|
|
|
until := time.Now().UTC().Add(dur)
|
|
if err := s.GuildMemberTimeout(i.GuildID, target.ID, &until); err != nil {
|
|
respondEphemeral(s, i, "Failed to timeout user. Check role hierarchy and permissions.")
|
|
return
|
|
}
|
|
|
|
if reason == "" {
|
|
respondEphemeral(s, i, fmt.Sprintf("Timed out %s for `%s`.", target.Mention(), dur.String()))
|
|
return
|
|
}
|
|
respondEphemeral(s, i, fmt.Sprintf("Timed out %s for `%s`. Reason: %s", target.Mention(), dur.String(), reason))
|
|
}
|
|
|
|
func handleKick(s *discordgo.Session, i *discordgo.InteractionCreate) {
|
|
if !hasPerm(i, discordgo.PermissionKickMembers) {
|
|
respondEphemeral(s, i, "You need **Kick Members** for this command.")
|
|
return
|
|
}
|
|
|
|
opt := i.ApplicationCommandData().Options[0]
|
|
var userOpt *discordgo.ApplicationCommandInteractionDataOption
|
|
var reason string
|
|
for _, o := range opt.Options {
|
|
switch o.Name {
|
|
case "user":
|
|
userOpt = o
|
|
case "reason":
|
|
reason = strings.TrimSpace(o.StringValue())
|
|
}
|
|
}
|
|
if userOpt == nil {
|
|
respondEphemeral(s, i, "Missing user option.")
|
|
return
|
|
}
|
|
target := userOpt.UserValue(s)
|
|
if target == nil {
|
|
respondEphemeral(s, i, "Invalid user.")
|
|
return
|
|
}
|
|
if i.Member != nil && i.Member.User != nil && target.ID == i.Member.User.ID {
|
|
respondEphemeral(s, i, "You cannot kick yourself.")
|
|
return
|
|
}
|
|
if s != nil && s.State != nil && s.State.User != nil && target.ID == s.State.User.ID {
|
|
respondEphemeral(s, i, "I can't kick myself.")
|
|
return
|
|
}
|
|
|
|
if err := s.GuildMemberDeleteWithReason(i.GuildID, target.ID, reason); err != nil {
|
|
respondEphemeral(s, i, "Failed to kick user. Check role hierarchy and permissions.")
|
|
return
|
|
}
|
|
|
|
if reason == "" {
|
|
respondEphemeral(s, i, "Kicked "+target.Mention()+".")
|
|
return
|
|
}
|
|
respondEphemeral(s, i, "Kicked "+target.Mention()+". Reason: "+reason)
|
|
}
|
|
|
|
func handleBan(s *discordgo.Session, i *discordgo.InteractionCreate) {
|
|
if !hasPerm(i, discordgo.PermissionBanMembers) {
|
|
respondEphemeral(s, i, "You need **Ban Members** for this command.")
|
|
return
|
|
}
|
|
|
|
opt := i.ApplicationCommandData().Options[0]
|
|
var userOpt *discordgo.ApplicationCommandInteractionDataOption
|
|
var deleteDays int64
|
|
var reason string
|
|
for _, o := range opt.Options {
|
|
switch o.Name {
|
|
case "user":
|
|
userOpt = o
|
|
case "delete_days":
|
|
deleteDays = o.IntValue()
|
|
case "reason":
|
|
reason = strings.TrimSpace(o.StringValue())
|
|
}
|
|
}
|
|
if userOpt == nil {
|
|
respondEphemeral(s, i, "Missing user option.")
|
|
return
|
|
}
|
|
target := userOpt.UserValue(s)
|
|
if target == nil {
|
|
respondEphemeral(s, i, "Invalid user.")
|
|
return
|
|
}
|
|
if deleteDays < 0 || deleteDays > 7 {
|
|
respondEphemeral(s, i, "delete_days must be between 0 and 7.")
|
|
return
|
|
}
|
|
if i.Member != nil && i.Member.User != nil && target.ID == i.Member.User.ID {
|
|
respondEphemeral(s, i, "You cannot ban yourself.")
|
|
return
|
|
}
|
|
if s != nil && s.State != nil && s.State.User != nil && target.ID == s.State.User.ID {
|
|
respondEphemeral(s, i, "I can't ban myself.")
|
|
return
|
|
}
|
|
|
|
if err := s.GuildBanCreateWithReason(i.GuildID, target.ID, reason, int(deleteDays)); err != nil {
|
|
respondEphemeral(s, i, "Failed to ban user. Check role hierarchy and permissions.")
|
|
return
|
|
}
|
|
|
|
if reason == "" {
|
|
respondEphemeral(s, i, fmt.Sprintf("Banned %s (deleted %d days of messages).", target.Mention(), deleteDays))
|
|
return
|
|
}
|
|
respondEphemeral(s, i, fmt.Sprintf("Banned %s (deleted %d days of messages). Reason: %s", target.Mention(), deleteDays, reason))
|
|
}
|
|
|
|
func handleUnban(s *discordgo.Session, i *discordgo.InteractionCreate) {
|
|
if !hasPerm(i, discordgo.PermissionBanMembers) {
|
|
respondEphemeral(s, i, "You need **Ban Members** for this command.")
|
|
return
|
|
}
|
|
|
|
opt := i.ApplicationCommandData().Options[0]
|
|
var userID string
|
|
var reason string
|
|
for _, o := range opt.Options {
|
|
switch o.Name {
|
|
case "user_id":
|
|
userID = strings.TrimSpace(o.StringValue())
|
|
case "reason":
|
|
reason = strings.TrimSpace(o.StringValue())
|
|
}
|
|
}
|
|
if userID == "" {
|
|
respondEphemeral(s, i, "Missing user ID.")
|
|
return
|
|
}
|
|
|
|
if err := s.GuildBanDelete(i.GuildID, userID); err != nil {
|
|
respondEphemeral(s, i, "Failed to unban user. Make sure the user ID is valid and banned.")
|
|
return
|
|
}
|
|
|
|
if reason == "" {
|
|
respondEphemeral(s, i, "Unbanned user ID `"+userID+"`.")
|
|
return
|
|
}
|
|
respondEphemeral(s, i, "Unbanned user ID `"+userID+"`. Reason: "+reason)
|
|
}
|
|
|
|
func handleUntimeout(s *discordgo.Session, i *discordgo.InteractionCreate) {
|
|
if !hasPerm(i, discordgo.PermissionModerateMembers) {
|
|
respondEphemeral(s, i, "You need **Moderate Members** for this command.")
|
|
return
|
|
}
|
|
|
|
opt := i.ApplicationCommandData().Options[0]
|
|
var userOpt *discordgo.ApplicationCommandInteractionDataOption
|
|
var reason string
|
|
for _, o := range opt.Options {
|
|
switch o.Name {
|
|
case "user":
|
|
userOpt = o
|
|
case "reason":
|
|
reason = strings.TrimSpace(o.StringValue())
|
|
}
|
|
}
|
|
if userOpt == nil {
|
|
respondEphemeral(s, i, "Missing user option.")
|
|
return
|
|
}
|
|
target := userOpt.UserValue(s)
|
|
if target == nil {
|
|
respondEphemeral(s, i, "Invalid user.")
|
|
return
|
|
}
|
|
|
|
if err := s.GuildMemberTimeout(i.GuildID, target.ID, nil); err != nil {
|
|
respondEphemeral(s, i, "Failed to remove timeout. Check role hierarchy and permissions.")
|
|
return
|
|
}
|
|
|
|
if reason == "" {
|
|
respondEphemeral(s, i, "Removed timeout from "+target.Mention()+".")
|
|
return
|
|
}
|
|
respondEphemeral(s, i, "Removed timeout from "+target.Mention()+". Reason: "+reason)
|
|
}
|
|
|
|
func handleSlowmode(s *discordgo.Session, i *discordgo.InteractionCreate) {
|
|
if !hasPerm(i, discordgo.PermissionManageChannels) {
|
|
respondEphemeral(s, i, "You need **Manage Channels** for this command.")
|
|
return
|
|
}
|
|
|
|
opt := i.ApplicationCommandData().Options[0]
|
|
var chOpt *discordgo.ApplicationCommandInteractionDataOption
|
|
var seconds int64
|
|
for _, o := range opt.Options {
|
|
switch o.Name {
|
|
case "channel":
|
|
chOpt = o
|
|
case "seconds":
|
|
seconds = o.IntValue()
|
|
}
|
|
}
|
|
if chOpt == nil {
|
|
respondEphemeral(s, i, "Missing channel option.")
|
|
return
|
|
}
|
|
if seconds < 0 || seconds > 21600 {
|
|
respondEphemeral(s, i, "Seconds must be between 0 and 21600.")
|
|
return
|
|
}
|
|
ch := chOpt.ChannelValue(s)
|
|
if ch == nil {
|
|
respondEphemeral(s, i, "Invalid channel.")
|
|
return
|
|
}
|
|
|
|
secondsInt := int(seconds)
|
|
_, err := s.ChannelEditComplex(ch.ID, &discordgo.ChannelEdit{
|
|
RateLimitPerUser: &secondsInt,
|
|
})
|
|
if err != nil {
|
|
respondEphemeral(s, i, "Failed to update slowmode.")
|
|
return
|
|
}
|
|
|
|
if seconds == 0 {
|
|
respondEphemeral(s, i, "Disabled slowmode in "+ch.Mention()+".")
|
|
return
|
|
}
|
|
respondEphemeral(s, i, fmt.Sprintf("Set slowmode in %s to %d seconds.", ch.Mention(), seconds))
|
|
}
|
|
|
|
func hasPerm(i *discordgo.InteractionCreate, perm int64) bool {
|
|
return i.Member != nil && (i.Member.Permissions&perm) == perm
|
|
}
|
|
|
|
func respondEphemeral(s *discordgo.Session, i *discordgo.InteractionCreate, msg string) {
|
|
_ = s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
|
|
Type: discordgo.InteractionResponseChannelMessageWithSource,
|
|
Data: &discordgo.InteractionResponseData{
|
|
Content: msg,
|
|
Flags: discordgo.MessageFlagsEphemeral,
|
|
},
|
|
})
|
|
}
|
|
|