moderation

package
v2.49.3 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Dec 20, 2024 License: MIT Imports: 41 Imported by: 1

README

Moderation plugin for YAGPDB

Moderation has tons of features such as bans, timed bans, warns, kicks & mutes!

-TODO-

Documentation

Index

Constants

View Source
const (
	ActionMuted    = "Muted"
	ActionUnMuted  = "Unmuted"
	ActionKicked   = "Kicked"
	ActionBanned   = "Banned"
	ActionUnbanned = "Unbanned"
	ActionWarned   = "Warned"
)
View Source
const DefaultDMMessage = "You have been {{.ModAction}}\n**Reason:** {{.Reason}}"
View Source
const DefaultTimeoutDuration = 10 * time.Minute
View Source
const (
	ErrNoMuteRole = errors.Sentinel("No mute role")
)
View Source
const MaxTimeOutDuration = 40320 * time.Minute
View Source
const MinTimeOutDuration = time.Minute

Variables

View Source
var (
	MAMute           = ModlogAction{Prefix: "Muted", Emoji: "🔇", Color: 0x57728e}
	MAUnmute         = ModlogAction{Prefix: "Unmuted", Emoji: "🔊", Color: 0x62c65f}
	MAKick           = ModlogAction{Prefix: "Kicked", Emoji: "👢", Color: 0xf2a013}
	MABanned         = ModlogAction{Prefix: "Banned", Emoji: "🔨", Color: 0xd64848}
	MAUnbanned       = ModlogAction{Prefix: "Unbanned", Emoji: "🔓", Color: 0x62c65f}
	MAWarned         = ModlogAction{Prefix: "Warned", Emoji: "⚠", Color: 0xfca253}
	MATimeoutAdded   = ModlogAction{Prefix: "Timed out", Emoji: "⏱", Color: 0x9b59b6}
	MATimeoutRemoved = ModlogAction{Prefix: "Timeout removed from", Emoji: "⏱", Color: 0x9b59b6}
	MAGiveRole       = ModlogAction{Prefix: "", Emoji: "➕", Color: 0x53fcf9}
	MARemoveRole     = ModlogAction{Prefix: "", Emoji: "➖", Color: 0x53fcf9}
	MAClearWarnings  = ModlogAction{Prefix: "Cleared warnings", Emoji: "👌", Color: 0x62c65f}
)
View Source
var ActionMap = map[string]string{
	MAMute.Prefix:         "Mute DM",
	MAUnmute.Prefix:       "Unmute DM",
	MAKick.Prefix:         "Kick DM",
	MABanned.Prefix:       "Ban DM",
	MAWarned.Prefix:       "Warn DM",
	MATimeoutAdded.Prefix: "Timeout DM",
}
View Source
var DBSchemas = []string{`
CREATE TABLE IF NOT EXISTS moderation_configs (
	guild_id BIGINT PRIMARY KEY,
	created_at TIMESTAMP WITH TIME ZONE NOT NULL,
	updated_at TIMESTAMP WITH TIME ZONE NOT NULL,
	
	-- Many of the following columns should be non-nullable, but were originally
	-- managed by gorm (which does not add NOT NULL constraints by default) so are
	-- missing them. Unfortunately, it is unfeasible to retroactively fill missing
	-- values with defaults and add the constraints as there are simply too many
	-- rows in production.

	-- For similar legacy reasons, many fields that should have type BIGINT are TEXT.

	kick_enabled BOOLEAN,
	kick_cmd_roles BIGINT[],
	delete_messages_on_kick BOOLEAN,
	kick_reason_optional BOOLEAN,
	kick_message TEXT,

	ban_enabled BOOLEAN,
	ban_cmd_roles BIGINT[],
	ban_reason_optional BOOLEAN,
	ban_message TEXT,
	default_ban_delete_days BIGINT DEFAULT 1,

	timeout_enabled BOOLEAN,
	timeout_cmd_roles BIGINT[],
	timeout_reason_optional BOOLEAN,
	timeout_remove_reason_optional BOOLEAN,
	timeout_message TEXT,
	default_timeout_duration BIGINT DEFAULT 10,

	mute_enabled BOOLEAN,
	mute_cmd_roles BIGINT[],
	mute_role TEXT,
	mute_disallow_reaction_add BOOLEAN,
	mute_reason_optional BOOLEAN,
	unmute_reason_optional BOOLEAN,
	mute_manage_role BOOLEAN,
	mute_remove_roles BIGINT[],
	mute_ignore_channels BIGINT[],
	mute_message TEXT,
	unmute_message TEXT,
	default_mute_duration BIGINT DEFAULT 10,

	warn_commands_enabled BOOLEAN,
	warn_cmd_roles BIGINT[],
	warn_include_channel_logs BOOLEAN,
	warn_send_to_modlog BOOLEAN,
	warn_message TEXT,

	clean_enabled BOOLEAN,
	report_enabled BOOLEAN,
	action_channel TEXT,
	report_channel TEXT,
	error_channel TEXT,
	log_unbans BOOLEAN,
	log_bans BOOLEAN,
	log_kicks BOOLEAN DEFAULT TRUE,
	log_timeouts BOOLEAN,

	give_role_cmd_enabled BOOLEAN,
	give_role_cmd_modlog BOOLEAN,
	give_role_cmd_roles BIGINT[]
);
`, `
-- Tables created with gorm have missing NOT NULL constraints for created_at and
-- updated_at columns; since these columns are never null in existing rows, we can
-- retraoctively add the constraints without needing to update any data.

ALTER TABLE moderation_configs ALTER COLUMN created_at SET NOT NULL;
`, `
ALTER TABLE moderation_configs ALTER COLUMN updated_at SET NOT NULL;
`, `

CREATE TABLE IF NOT EXISTS moderation_warnings (
	id SERIAL PRIMARY KEY,
	created_at TIMESTAMP WITH TIME ZONE NOT NULL,
	updated_at TIMESTAMP WITH TIME ZONE NOT NULL,

	guild_id BIGINT NOT NULL,
	user_id TEXT NOT NULL, -- text instead of bigint for legacy compatibility
	author_id TEXT NOT NULL,

	author_username_discrim TEXT NOT NULL,

	message TEXT NOT NULL,
	logs_link TEXT
);
`, `
CREATE INDEX IF NOT EXISTS idx_moderation_warnings_guild_id ON moderation_warnings(guild_id);
`, `
-- Similar to moderation_warnings.{created_at,updated_at}, there are a number of
-- fields that are never null in existing data but do not have the proper NOT NULL
-- constraints if they were created with gorm. Add them in.

ALTER TABLE moderation_warnings ALTER COLUMN created_at SET NOT NULL;
`, `
ALTER TABLE moderation_warnings ALTER COLUMN updated_at SET NOT NULL;
`, `
ALTER TABLE moderation_warnings ALTER COLUMN guild_id SET NOT NULL;
`, `
ALTER TABLE moderation_warnings ALTER COLUMN user_id SET NOT NULL;
`, `
ALTER TABLE moderation_warnings ALTER COLUMN author_id SET NOT NULL;
`, `
ALTER TABLE moderation_warnings ALTER COLUMN author_username_discrim SET NOT NULL;
`, `
ALTER TABLE moderation_warnings ALTER COLUMN message SET NOT NULL;
`, `

CREATE TABLE IF NOT EXISTS muted_users (
	id SERIAL PRIMARY KEY,
	created_at TIMESTAMP WITH TIME ZONE NOT NULL,
	updated_at TIMESTAMP WITH TIME ZONE NOT NULL,

	expires_at TIMESTAMP WITH TIME ZONE,

	guild_id BIGINT NOT NULL,
	user_id BIGINT NOT NULL,

	author_id BIGINT NOT NULL,
	reason TEXT NOT NULL,

	removed_roles BIGINT[]
);
`, `

ALTER TABLE muted_users ALTER COLUMN created_at SET NOT NULL;
`, `
ALTER TABLE muted_users ALTER COLUMN updated_at SET NOT NULL;
`, `
ALTER TABLE muted_users ALTER COLUMN guild_id SET NOT NULL;
`, `
ALTER TABLE muted_users ALTER COLUMN user_id SET NOT NULL;
`, `
ALTER TABLE muted_users ALTER COLUMN author_id SET NOT NULL;
`, `
ALTER TABLE muted_users ALTER COLUMN reason SET NOT NULL;
`}
View Source
var (
	ErrFailedPerms = errors.New("Failed retrieving perms")
)
View Source
var ModerationCommands = []*commands.YAGCommand{
	{
		CustomEnabled: true,
		CmdCategory:   commands.CategoryModeration,
		Name:          "Ban",
		Aliases:       []string{"banid"},
		Description:   "Bans a member, specify number of days of messages to delete with -ddays (0 to 7)",
		RequiredArgs:  1,
		Arguments: []*dcmd.ArgDef{
			{Name: "User", Type: dcmd.UserID},
			{Name: "Duration", Type: &commands.DurationArg{}, Default: time.Duration(0)},
			{Name: "Reason", Type: dcmd.String},
		},
		ArgSwitches: []*dcmd.ArgDef{
			{Name: "ddays", Help: "Number of days of messages to delete", Type: dcmd.Int},
		},
		RequiredDiscordPermsHelp: "BanMembers or ManageGuild",
		RequireBotPerms:          [][]int64{{discordgo.PermissionAdministrator}, {discordgo.PermissionBanMembers}},
		ArgumentCombos:           [][]int{{0, 1, 2}, {0, 2, 1}, {0, 1}, {0, 2}, {0}},
		SlashCommandEnabled:      true,
		DefaultEnabled:           false,
		IsResponseEphemeral:      false,
		RunFunc: func(parsed *dcmd.Data) (interface{}, error) {
			if parsed.Context().Value(commands.CtxKeyExecutedByNestedCommandTemplate) == true {
				return nil, errors.New("cannot nest exec/execAdmin calls")
			}

			config, target, err := MBaseCmd(parsed, parsed.Args[0].Int64())
			if err != nil {
				return nil, err
			}

			reason := SafeArgString(parsed, 2)
			reason, err = MBaseCmdSecond(parsed, reason, config.BanReasonOptional, discordgo.PermissionBanMembers, config.BanCmdRoles, config.BanEnabled, true)
			if err != nil {
				return nil, err
			}

			if utf8.RuneCountInString(reason) > 470 {
				return "Error: Reason too long (can be max 470 characters).", nil
			}

			err = checkHierarchy(parsed, parsed.Args[0].Int64())
			if err != nil {
				return nil, err
			}

			ddays := int(config.DefaultBanDeleteDays.Int64)
			if parsed.Switches["ddays"].Value != nil {
				ddays = parsed.Switches["ddays"].Int()
			}
			banDuration := parsed.Args[1].Value.(time.Duration)

			var msg *discordgo.Message
			if parsed.TraditionalTriggerData != nil {
				msg = parsed.TraditionalTriggerData.Message
			}
			err = BanUserWithDuration(config, parsed.GuildData.GS.ID, parsed.GuildData.CS, msg, parsed.Author, reason, target, banDuration, ddays, parsed.Context().Value(commands.CtxKeyExecutedByCommandTemplate) == true)
			if err != nil {
				return nil, err
			}

			return GenericCmdResp(MABanned, target, banDuration, true, false), nil
		},
	},
	{
		CustomEnabled: true,
		CmdCategory:   commands.CategoryModeration,
		Name:          "Unban",
		Aliases:       []string{"unbanid"},
		Description:   "Unbans a user. Reason requirement is same as ban command setting.",
		RequiredArgs:  1,
		Arguments: []*dcmd.ArgDef{
			{Name: "User", Type: dcmd.UserID},
			{Name: "Reason", Type: dcmd.String},
		},
		RequiredDiscordPermsHelp: "BanMembers or ManageGuild",
		RequireBotPerms:          [][]int64{{discordgo.PermissionAdministrator}, {discordgo.PermissionBanMembers}},
		SlashCommandEnabled:      true,
		DefaultEnabled:           false,
		IsResponseEphemeral:      false,
		RunFunc: func(parsed *dcmd.Data) (interface{}, error) {
			config, _, err := MBaseCmd(parsed, 0)
			if err != nil {
				return nil, err
			}

			reason := SafeArgString(parsed, 1)
			reason, err = MBaseCmdSecond(parsed, reason, config.BanReasonOptional, discordgo.PermissionBanMembers, config.BanCmdRoles, config.BanEnabled, true)
			if err != nil {
				return nil, err
			}
			targetID := parsed.Args[0].Int64()
			target := &discordgo.User{
				Username:      "unknown",
				Discriminator: "????",
				ID:            targetID,
			}
			targetMem, _ := bot.GetMember(parsed.GuildData.GS.ID, targetID)
			if targetMem != nil {
				return "User is not banned!", nil
			}

			isNotBanned, err := UnbanUser(config, parsed.GuildData.GS.ID, parsed.Author, reason, target)

			if err != nil {
				return nil, err
			}
			if isNotBanned {
				return "User is not banned!", nil
			}

			return GenericCmdResp(MAUnbanned, target, 0, true, true), nil
		},
	},
	{
		CustomEnabled: true,
		CmdCategory:   commands.CategoryModeration,
		Name:          "Kick",
		Description:   "Kicks a member",
		RequiredArgs:  1,
		Arguments: []*dcmd.ArgDef{
			{Name: "User", Type: dcmd.UserID},
			{Name: "Reason", Type: dcmd.String},
		},
		RequiredDiscordPermsHelp: "KickMembers or ManageGuild",
		ArgSwitches: []*dcmd.ArgDef{
			{Name: "cl", Help: "Messages to delete", Type: &dcmd.IntArg{Min: 1, Max: 100}},
		},
		RequireBotPerms:     [][]int64{{discordgo.PermissionAdministrator}, {discordgo.PermissionKickMembers}},
		SlashCommandEnabled: true,
		IsResponseEphemeral: false,
		RunFunc: func(parsed *dcmd.Data) (interface{}, error) {
			if parsed.Context().Value(commands.CtxKeyExecutedByNestedCommandTemplate) == true {
				return nil, errors.New("cannot nest exec/execAdmin calls")
			}

			config, target, err := MBaseCmd(parsed, parsed.Args[0].Int64())
			if err != nil {
				return nil, err
			}

			reason := SafeArgString(parsed, 1)
			reason, err = MBaseCmdSecond(parsed, reason, config.KickReasonOptional, discordgo.PermissionKickMembers, config.KickCmdRoles, config.KickEnabled, true)
			if err != nil {
				return nil, err
			}

			member, err := bot.GetMember(parsed.GuildData.GS.ID, target.ID)
			if err != nil || member == nil {
				return "Member not found", err
			}

			if utf8.RuneCountInString(reason) > 470 {
				return "Error: Reason too long (can be max 470 characters).", nil
			}

			err = checkHierarchy(parsed, parsed.Args[0].Int64())
			if err != nil {
				return nil, err
			}

			toDel := -1
			if parsed.Switches["cl"].Value != nil {
				toDel = parsed.Switches["cl"].Int()
			}

			var msg *discordgo.Message
			if parsed.TraditionalTriggerData != nil {
				msg = parsed.TraditionalTriggerData.Message
			}

			err = KickUser(config, parsed.GuildData.GS.ID, parsed.GuildData.CS, msg, parsed.Author, reason, target, toDel, parsed.Context().Value(commands.CtxKeyExecutedByCommandTemplate) == true)
			if err != nil {
				return nil, err
			}

			return GenericCmdResp(MAKick, target, 0, true, true), nil
		},
	},
	{
		CustomEnabled: true,
		CmdCategory:   commands.CategoryModeration,
		Name:          "Mute",
		Description:   "Mutes a member",
		Arguments: []*dcmd.ArgDef{
			{Name: "User", Type: dcmd.UserID},
			{Name: "Duration", Type: &commands.DurationArg{}},
			{Name: "Reason", Type: dcmd.String},
		},
		RequiredDiscordPermsHelp: "KickMembers or ManageGuild",
		RequireBotPerms:          [][]int64{{discordgo.PermissionAdministrator}, {discordgo.PermissionManageRoles}},
		ArgumentCombos:           [][]int{{0, 1, 2}, {0, 2, 1}, {0, 1}, {0, 2}, {0}},
		SlashCommandEnabled:      true,
		DefaultEnabled:           false,
		IsResponseEphemeral:      false,
		RunFunc: func(parsed *dcmd.Data) (interface{}, error) {
			if parsed.Context().Value(commands.CtxKeyExecutedByNestedCommandTemplate) == true {
				return nil, errors.New("cannot nest exec/execAdmin calls")
			}

			config, target, err := MBaseCmd(parsed, parsed.Args[0].Int64())
			if err != nil {
				return nil, err
			}

			if config.MuteRole == 0 {
				return fmt.Sprintf("No mute role selected. Select one at <%s/moderation>", web.ManageServerURL(parsed.GuildData.GS.ID)), nil
			}

			reason := parsed.Args[2].Str()
			reason, err = MBaseCmdSecond(parsed, reason, config.MuteReasonOptional, discordgo.PermissionKickMembers, config.MuteCmdRoles, config.MuteEnabled, true)
			if err != nil {
				return nil, err
			}

			d := time.Duration(config.DefaultMuteDuration.Int64) * time.Minute
			if parsed.Args[1].Value != nil {
				d = parsed.Args[1].Value.(time.Duration)
			}
			if d > 0 && d < time.Minute {
				d = time.Minute
			}

			logger.Info(d.Seconds())

			member, err := bot.GetMember(parsed.GuildData.GS.ID, target.ID)
			if err != nil || member == nil {
				return "Member not found", err
			}

			err = checkHierarchy(parsed, target.ID)
			if err != nil {
				return nil, err
			}

			var msg *discordgo.Message
			if parsed.TraditionalTriggerData != nil {
				msg = parsed.TraditionalTriggerData.Message
			}
			err = MuteUnmuteUser(config, true, parsed.GuildData.GS.ID, parsed.GuildData.CS, msg, parsed.Author, reason, member, int(d.Minutes()), parsed.Context().Value(commands.CtxKeyExecutedByCommandTemplate) == true)
			if err != nil {
				return nil, err
			}

			common.BotSession.GuildMemberMove(parsed.GuildData.GS.ID, target.ID, 0)
			return GenericCmdResp(MAMute, target, d, true, false), nil
		},
	},
	{
		CustomEnabled: true,
		CmdCategory:   commands.CategoryModeration,
		Name:          "Unmute",
		Description:   "Unmutes a member",
		RequiredArgs:  1,
		Arguments: []*dcmd.ArgDef{
			{Name: "User", Type: dcmd.UserID},
			{Name: "Reason", Type: dcmd.String},
		},
		RequiredDiscordPermsHelp: "KickMembers or ManageGuild",
		RequireBotPerms:          [][]int64{{discordgo.PermissionAdministrator}, {discordgo.PermissionManageRoles}},
		SlashCommandEnabled:      true,
		DefaultEnabled:           false,
		IsResponseEphemeral:      false,
		RunFunc: func(parsed *dcmd.Data) (interface{}, error) {
			if parsed.Context().Value(commands.CtxKeyExecutedByNestedCommandTemplate) == true {
				return nil, errors.New("cannot nest exec/execAdmin calls")
			}

			config, target, err := MBaseCmd(parsed, parsed.Args[0].Int64())
			if err != nil {
				return nil, err
			}

			if config.MuteRole == 0 {
				return "No mute role set up, assign a mute role in the control panel", nil
			}

			reason := parsed.Args[1].Str()
			reason, err = MBaseCmdSecond(parsed, reason, config.UnmuteReasonOptional, discordgo.PermissionKickMembers, config.MuteCmdRoles, config.MuteEnabled, true)
			if err != nil {
				return nil, err
			}

			member, err := bot.GetMember(parsed.GuildData.GS.ID, target.ID)
			if err != nil || member == nil {
				return "Member not found", err
			}

			err = checkHierarchy(parsed, target.ID)
			if err != nil {
				return nil, err
			}

			var msg *discordgo.Message
			if parsed.TraditionalTriggerData != nil {
				msg = parsed.TraditionalTriggerData.Message
			}
			err = MuteUnmuteUser(config, false, parsed.GuildData.GS.ID, parsed.GuildData.CS, msg, parsed.Author, reason, member, 0, parsed.Context().Value(commands.CtxKeyExecutedByCommandTemplate) == true)
			if err != nil {
				return nil, err
			}

			return GenericCmdResp(MAUnmute, target, 0, false, true), nil
		},
	},
	{
		CustomEnabled: true,
		CmdCategory:   commands.CategoryModeration,
		Name:          "Timeout",
		Description:   "Timeout a member",
		Aliases:       []string{"to"},
		Arguments: []*dcmd.ArgDef{
			{Name: "User", Type: dcmd.UserID},
			{Name: "Duration", Type: &commands.DurationArg{}},
			{Name: "Reason", Type: dcmd.String},
		},
		RequiredDiscordPermsHelp: "TimeoutMembers/ModerateMembers or ManageGuild",
		RequireBotPerms:          [][]int64{{discordgo.PermissionAdministrator}, {discordgo.PermissionModerateMembers}},
		ArgumentCombos:           [][]int{{0, 1, 2}, {0, 2, 1}, {0, 1}, {0, 2}, {0}},
		SlashCommandEnabled:      true,
		DefaultEnabled:           false,
		IsResponseEphemeral:      false,
		RunFunc: func(parsed *dcmd.Data) (interface{}, error) {
			if parsed.Context().Value(commands.CtxKeyExecutedByNestedCommandTemplate) == true {
				return nil, errors.New("cannot nest exec/execAdmin calls")
			}
			config, target, err := MBaseCmd(parsed, parsed.Args[0].Int64())
			if err != nil {
				return nil, err
			}

			reason := parsed.Args[2].Str()
			reason, err = MBaseCmdSecond(parsed, reason, config.TimeoutReasonOptional, discordgo.PermissionModerateMembers, config.TimeoutCmdRoles, config.TimeoutEnabled, true)
			if err != nil {
				return nil, err
			}

			d := time.Duration(config.DefaultTimeoutDuration.Int64) * time.Minute
			if parsed.Args[1].Value != nil {
				d = parsed.Args[1].Value.(time.Duration)
			}
			if d < time.Minute {
				d = time.Minute
			}
			if d > MaxTimeOutDuration {
				return fmt.Sprintf("Error: Max duration of Timeouts can be %v days", (MaxTimeOutDuration.Hours() / 24)), nil
			}
			member, err := bot.GetMember(parsed.GuildData.GS.ID, target.ID)
			if err != nil || member == nil {
				return "Member not found", err
			}

			err = checkHierarchy(parsed, target.ID)
			if err != nil {
				return nil, err
			}

			var msg *discordgo.Message
			if parsed.TraditionalTriggerData != nil {
				msg = parsed.TraditionalTriggerData.Message
			}
			err = TimeoutUser(config, parsed.GuildData.GS.ID, parsed.GuildData.CS, msg, parsed.Author, reason, &member.User, d, parsed.Context().Value(commands.CtxKeyExecutedByCommandTemplate) == true)
			if err != nil {
				return nil, err
			}

			return GenericCmdResp(MATimeoutAdded, target, d, true, false), nil
		},
	}, {
		CustomEnabled: true,
		CmdCategory:   commands.CategoryModeration,
		Name:          "RemoveTimeout",
		Aliases:       []string{"untimeout", "cleartimeout", "deltimeout", "rto"},
		Description:   "Removes a member's timeout",
		RequiredArgs:  1,
		Arguments: []*dcmd.ArgDef{
			{Name: "User", Type: dcmd.UserID},
			{Name: "Reason", Type: dcmd.String},
		},
		RequiredDiscordPermsHelp: "TimeoutMember/ModerateMember or ManageGuild",
		RequireBotPerms:          [][]int64{{discordgo.PermissionAdministrator}, {discordgo.PermissionModerateMembers}},
		SlashCommandEnabled:      true,
		DefaultEnabled:           false,
		IsResponseEphemeral:      false,
		RunFunc: func(parsed *dcmd.Data) (interface{}, error) {
			config, target, err := MBaseCmd(parsed, parsed.Args[0].Int64())
			if err != nil {
				return nil, err
			}

			reason := parsed.Args[1].Str()
			reason, err = MBaseCmdSecond(parsed, reason, config.TimeoutReasonOptional, discordgo.PermissionModerateMembers, config.TimeoutCmdRoles, config.TimeoutEnabled, true)
			if err != nil {
				return nil, err
			}

			member, err := bot.GetMember(parsed.GuildData.GS.ID, target.ID)
			if err != nil || member == nil {
				return "Member not found", err
			}

			err = checkHierarchy(parsed, target.ID)
			if err != nil {
				return nil, err
			}

			memberTimeout := member.Member.CommunicationDisabledUntil
			if memberTimeout == nil || memberTimeout.Before(time.Now()) {
				return "Member is not timed out", nil
			}

			err = RemoveTimeout(config, parsed.GuildData.GS.ID, parsed.Author, reason, &member.User)
			if err != nil {
				return nil, err
			}

			return GenericCmdResp(MATimeoutRemoved, target, 0, false, true), nil
		},
	},
	{
		CustomEnabled: true,
		Cooldown:      5,
		CmdCategory:   commands.CategoryModeration,
		Name:          "Report",
		Description:   "Reports a member to the server's staff",
		RequiredArgs:  2,
		Arguments: []*dcmd.ArgDef{
			{Name: "User", Type: dcmd.UserID},
			{Name: "Reason", Type: dcmd.String},
		},
		SlashCommandEnabled: true,
		DefaultEnabled:      false,
		IsResponseEphemeral: true,
		RunFunc: func(parsed *dcmd.Data) (interface{}, error) {
			config, _, err := MBaseCmd(parsed, 0)
			if err != nil {
				return nil, err
			}

			_, err = MBaseCmdSecond(parsed, "", true, 0, nil, config.ReportEnabled, false)
			if err != nil {
				return nil, err
			}

			temp, err := bot.GetMember(parsed.GuildData.GS.ID, parsed.Args[0].Int64())
			if err != nil || temp == nil {
				return nil, err
			}

			target := temp.User

			if target.ID == parsed.Author.ID {
				return "You can't report yourself, silly.", nil
			}

			logLink := CreateLogs(parsed.GuildData.GS.ID, parsed.GuildData.CS.ID, parsed.Author)

			channelID := config.ReportChannel
			if channelID == 0 {
				return "No report channel set up", nil
			}

			topContent := fmt.Sprintf("%s reported **%s (ID %d)**", parsed.Author.Mention(), target.String(), target.ID)

			embed := &discordgo.MessageEmbed{
				Author: &discordgo.MessageEmbedAuthor{
					Name:    fmt.Sprintf("%s (ID %d)", parsed.Author.String(), parsed.Author.ID),
					IconURL: discordgo.EndpointUserAvatar(parsed.Author.ID, parsed.Author.Avatar),
				},
				Description: fmt.Sprintf("🔍**Reported** %s *(ID %d)*\n📄**Reason:** %s ([Logs](%s))\n**Channel:** <#%d>", target.String(), target.ID, parsed.Args[1].Value, logLink, parsed.ChannelID),
				Color:       0xee82ee,
				Thumbnail: &discordgo.MessageEmbedThumbnail{
					URL: discordgo.EndpointUserAvatar(target.ID, target.Avatar),
				},
			}

			send := &discordgo.MessageSend{
				Content: topContent,
				Embeds:  []*discordgo.MessageEmbed{embed},
				AllowedMentions: discordgo.AllowedMentions{
					Parse: []discordgo.AllowedMentionType{discordgo.AllowedMentionTypeUsers},
				},
			}

			_, err = common.BotSession.ChannelMessageSendComplex(channelID, send)
			if err != nil {
				return "Something went wrong while sending your report!", err
			}

			if channelID != parsed.ChannelID || parsed.SlashCommandTriggerData != nil {
				return "User reported to the proper authorities!", nil
			}

			return nil, nil
		},
	},
	{
		CustomEnabled:   true,
		CmdCategory:     commands.CategoryModeration,
		Name:            "Clean",
		Description:     "Delete the last number of messages from chat, optionally filtering by user, max age and regex or ignoring pinned messages.",
		LongDescription: "Specify a regex with \"-r regex_here\" and max age with \"-ma 1h10m\"\nYou can invert the regex match (i.e. only clear messages that do not match the given regex) by supplying the `-im` flag\nNote: Will only look in the last 1k messages, and none > 2 weeks old.",
		Aliases:         []string{"clear", "cl"},
		RequiredArgs:    1,
		Arguments: []*dcmd.ArgDef{
			{Name: "Num", Type: &dcmd.IntArg{Min: 1, Max: 100}},
			{Name: "User", Type: dcmd.UserID, Default: 0},
		},
		ArgSwitches: []*dcmd.ArgDef{
			{Name: "r", Help: "Regex", Type: dcmd.String},
			{Name: "im", Help: "Invert regex match"},
			{Name: "ma", Help: "Max age", Default: time.Duration(0), Type: &commands.DurationArg{}},
			{Name: "minage", Help: "Min age", Default: time.Duration(0), Type: &commands.DurationArg{}},
			{Name: "i", Help: "Regex case insensitive"},
			{Name: "nopin", Help: "Ignore pinned messages"},
			{Name: "a", Help: "Only remove messages with attachments"},
			{Name: "to", Help: "Stop at this msg ID", Type: dcmd.BigInt},
			{Name: "from", Help: "Start at this msg ID", Type: dcmd.BigInt},
		},
		RequiredDiscordPermsHelp: "ManageMessages or ManageGuild",
		RequireBotPerms:          [][]int64{{discordgo.PermissionAdministrator}, {discordgo.PermissionManageMessages}},
		ArgumentCombos:           [][]int{{0}, {0, 1}, {1, 0}},
		SlashCommandEnabled:      true,
		DefaultEnabled:           false,
		IsResponseEphemeral:      false,
		RunFunc: func(parsed *dcmd.Data) (interface{}, error) {
			config, _, err := MBaseCmd(parsed, 0)
			if err != nil {
				return nil, err
			}

			_, err = MBaseCmdSecond(parsed, "", true, discordgo.PermissionManageMessages, nil, config.CleanEnabled, false)
			if err != nil {
				return nil, err
			}

			var filters []MessageFilter

			if userIDFilter := parsed.Args[1].Int64(); userIDFilter != 0 {
				filters = append(filters, &MessageAuthorFilter{userIDFilter})
			}

			if re := parsed.Switches["r"].Str(); re != "" {
				if caseInsensitive := parsed.Switches["i"].Bool(); caseInsensitive {
					if !strings.HasPrefix(re, "(?i)") {
						re = "(?i)" + re
					}
				}

				parsedRe, err := regexp.Compile(re)
				if err != nil {
					return "Invalid regexp", err
				}

				invertMatch := parsed.Switches["im"].Bool()
				filters = append(filters, &RegExpFilter{InvertMatch: invertMatch, Re: parsedRe})
			}

			now := time.Now()
			minAge := parsed.Switches["minage"].Value.(time.Duration)
			maxAge := parsed.Switches["ma"].Value.(time.Duration)
			if minAge != 0 || maxAge != 0 {
				filters = append(filters, &MessageAgeFilter{ReferenceTime: now, MinAge: minAge, MaxAge: maxAge})
			}

			fromID := parsed.Switches["from"].Int64()
			toID := parsed.Switches["to"].Int64()
			if fromID != 0 || toID != 0 {
				filters = append(filters, &MessageIDFilter{FromID: fromID, ToID: toID})
			}

			if parsed.Switches["nopin"].Bool() {
				pinned, err := common.BotSession.ChannelMessagesPinned(parsed.ChannelID)
				if err != nil {
					return "Failed fetching pinned messages", err
				}
				filters = append(filters, NewIgnorePinnedMessagesFilter(pinned))
			}

			if onlyDeleteWithAttachments := parsed.Switches["a"].Bool(); onlyDeleteWithAttachments {
				filters = append(filters, &MessagesWithAttachmentsFilter{})
			}

			var triggerID int64
			if parsed.TriggerType == dcmd.TriggerTypeSlashCommands {
				m, err := common.BotSession.GetOriginalInteractionResponse(common.BotApplication.ID, parsed.SlashCommandTriggerData.Interaction.Token)
				if err != nil {
					return "Failed fetching original interaction response", err
				}
				triggerID = m.ID
			} else {
				triggerID = parsed.TraditionalTriggerData.Message.ID
			}

			deleteLimit := parsed.Args[0].Int()
			fetchLimit := deleteLimit + 1
			if len(filters) > 0 {
				fetchLimit = deleteLimit * 50
			}
			if fetchLimit > 1000 {
				fetchLimit = 1000
			}

			msgs, err := bot.GetMessages(parsed.GuildData.GS.ID, parsed.ChannelID, fetchLimit, false)
			if err != nil {
				return "Failed fetching messages", err
			}

			var toDelete []int64
			filter := CombinedANDFilter{filters}
			for _, msg := range msgs {

				if now.Sub(msg.ParsedCreatedAt) > (14*time.Hour*24)-time.Minute {
					continue
				}

				if msg.ID == triggerID {
					continue
				}

				if filter.Matches(msg) {
					toDelete = append(toDelete, msg.ID)
					if len(toDelete) >= deleteLimit {
						break
					}
				}
			}

			var resp string
			switch numDeleted := len(toDelete); numDeleted {
			case 0:
				resp = "Deleted 0 messages! :')"
			case 1:
				err = common.BotSession.ChannelMessageDelete(parsed.ChannelID, toDelete[0])
				resp = "Deleted 1 message! :')"
			default:
				err = common.BotSession.ChannelMessagesBulkDelete(parsed.ChannelID, toDelete)
				resp = fmt.Sprintf("Deleted %d messages! :')", numDeleted)
			}

			if err != nil {
				return "Failed deleting messages", err
			}
			return dcmd.NewTemporaryResponse(time.Second*5, resp, true), nil
		},
	},
	{
		CustomEnabled: true,
		CmdCategory:   commands.CategoryModeration,
		Name:          "Reason",
		Description:   "Add/Edit a modlog reason",
		RequiredArgs:  2,
		Arguments: []*dcmd.ArgDef{
			{Name: "Message-ID", Type: dcmd.BigInt},
			{Name: "Reason", Type: dcmd.String},
		},
		RequiredDiscordPermsHelp: "KickMembers or ManageGuild",
		SlashCommandEnabled:      true,
		DefaultEnabled:           false,
		RunFunc: func(parsed *dcmd.Data) (interface{}, error) {
			config, _, err := MBaseCmd(parsed, 0)
			if err != nil {
				return nil, err
			}

			_, err = MBaseCmdSecond(parsed, "", true, discordgo.PermissionKickMembers, nil, true, false)
			if err != nil {
				return nil, err
			}

			if config.ActionChannel == 0 {
				return "No mod log channel set up", nil
			}

			msg, err := common.BotSession.ChannelMessage(config.ActionChannel, parsed.Args[0].Int64())
			if err != nil {
				return nil, err
			}

			if msg.Author.ID != common.BotUser.ID {
				return "I didn't make that message", nil
			}

			if len(msg.Embeds) < 1 {
				return "This entry is either too old or you're trying to mess with me...", nil
			}

			embed := msg.Embeds[0]
			updateEmbedReason(parsed.Author, parsed.Args[1].Str(), embed)
			_, err = common.BotSession.ChannelMessageEditEmbed(config.ActionChannel, msg.ID, embed)
			if err != nil {
				return nil, err
			}

			return "👌", nil
		},
	},
	{
		CustomEnabled: true,
		CmdCategory:   commands.CategoryModeration,
		Name:          "Warn",
		Description:   "Warns a user, warnings are saved using the bot. Use -warnings to view them.",
		RequiredArgs:  2,
		Arguments: []*dcmd.ArgDef{
			{Name: "User", Type: dcmd.UserID},
			{Name: "Reason", Type: dcmd.String},
		},
		RequiredDiscordPermsHelp: "ManageMessages or ManageGuild",
		SlashCommandEnabled:      true,
		DefaultEnabled:           false,
		IsResponseEphemeral:      false,
		RunFunc: func(parsed *dcmd.Data) (interface{}, error) {
			if parsed.Context().Value(commands.CtxKeyExecutedByNestedCommandTemplate) == true {
				return nil, errors.New("cannot nest exec/execAdmin calls")
			}

			config, target, err := MBaseCmd(parsed, parsed.Args[0].Int64())
			if err != nil {
				return nil, err
			}
			_, err = MBaseCmdSecond(parsed, "", true, discordgo.PermissionManageMessages, config.WarnCmdRoles, config.WarnCommandsEnabled, true)
			if err != nil {
				return nil, err
			}

			member, err := bot.GetMember(parsed.GuildData.GS.ID, target.ID)
			if err != nil || member == nil {
				return "Member not found", err
			}

			var msg *discordgo.Message
			if parsed.TraditionalTriggerData != nil {
				msg = parsed.TraditionalTriggerData.Message
			}
			err = WarnUser(config, parsed.GuildData.GS.ID, parsed.GuildData.CS, msg, parsed.Author, target, parsed.Args[1].Str(), parsed.Context().Value(commands.CtxKeyExecutedByCommandTemplate) == true)
			if err != nil {
				return nil, err
			}

			return GenericCmdResp(MAWarned, target, 0, false, true), nil
		},
	},
	{
		CustomEnabled: true,
		CmdCategory:   commands.CategoryModeration,
		Name:          "Warnings",
		Description:   "Lists warning of a user.",
		Aliases:       []string{"Warns"},
		RequiredArgs:  1,
		Arguments: []*dcmd.ArgDef{
			{Name: "User", Type: dcmd.UserID, Default: 0},
			{Name: "Page", Type: &dcmd.IntArg{Max: 10000}, Default: 0},
		},
		ArgSwitches: []*dcmd.ArgDef{
			{Name: "id", Help: "Warning ID", Type: dcmd.Int},
		},
		RequiredDiscordPermsHelp: "ManageMessages or ManageGuild",
		SlashCommandEnabled:      true,
		DefaultEnabled:           false,
		RunFunc: func(parsed *dcmd.Data) (interface{}, error) {
			var err error
			config, _, err := MBaseCmd(parsed, 0)
			if err != nil {
				return nil, err
			}

			_, err = MBaseCmdSecond(parsed, "", true, discordgo.PermissionManageMessages, config.WarnCmdRoles, true, true)
			if err != nil {
				return nil, err
			}

			if parsed.Switches["id"].Value != nil {
				id := parsed.Switches["id"].Int()
				warning, err := models.ModerationWarnings(
					models.ModerationWarningWhere.ID.EQ(id),

					models.ModerationWarningWhere.GuildID.EQ(parsed.GuildData.GS.ID),
				).OneG(parsed.Context())
				if err != nil {
					if err == sql.ErrNoRows {
						return fmt.Sprintf("Could not find warning with ID `%d`", id), nil
					}
					return nil, err
				}

				return &discordgo.MessageEmbed{
					Title:       fmt.Sprintf("Warning#%d - User : %s", warning.ID, warning.UserID),
					Description: fmt.Sprintf("<t:%d:f> - **Reason** : %s", warning.CreatedAt.Unix(), warning.Message),
					Footer:      &discordgo.MessageEmbedFooter{Text: fmt.Sprintf("By: %s (%13s)", warning.AuthorUsernameDiscrim, warning.AuthorID)},
				}, nil
			}
			page := parsed.Args[1].Int()
			if page < 1 {
				page = 1
			}
			if parsed.Context().Value(paginatedmessages.CtxKeyNoPagination) != nil {
				return PaginateWarnings(parsed)(nil, page)
			}

			return paginatedmessages.NewPaginatedResponse(parsed.GuildData.GS.ID, parsed.GuildData.CS.ID, page, 0, PaginateWarnings(parsed)), nil
		},
	},
	{
		CustomEnabled: true,
		CmdCategory:   commands.CategoryModeration,
		Name:          "EditWarning",
		Description:   "Edit a warning, id is the first number of each warning from the warnings command",
		RequiredArgs:  2,
		Arguments: []*dcmd.ArgDef{
			{Name: "Id", Type: dcmd.Int},
			{Name: "NewMessage", Type: dcmd.String},
		},
		RequiredDiscordPermsHelp: "ManageMessages or ManageGuild",
		SlashCommandEnabled:      true,
		DefaultEnabled:           false,
		RunFunc: func(parsed *dcmd.Data) (interface{}, error) {
			config, _, err := MBaseCmd(parsed, 0)
			if err != nil {
				return nil, err
			}

			_, err = MBaseCmdSecond(parsed, "", true, discordgo.PermissionManageMessages, config.WarnCmdRoles, config.WarnCommandsEnabled, true)
			if err != nil {
				return nil, err
			}

			warningID := parsed.Args[0].Int()
			updatedMessage := fmt.Sprintf("%s (updated by %s (%d))", parsed.Args[1].Str(), parsed.Author.String(), parsed.Author.ID)
			numUpdated, err := models.ModerationWarnings(
				models.ModerationWarningWhere.ID.EQ(warningID),

				models.ModerationWarningWhere.GuildID.EQ(parsed.GuildData.GS.ID),
			).UpdateAllG(parsed.Context(), models.M{"message": updatedMessage})
			if err != nil {
				return "Failed editing warning", err
			}
			if numUpdated == 0 {
				return fmt.Sprintf("Could not find warning with ID `%d`", warningID), nil
			}

			return "👌", nil
		},
	},
	{
		CustomEnabled: true,
		CmdCategory:   commands.CategoryModeration,
		Name:          "DelWarning",
		Aliases:       []string{"dw", "delwarn", "deletewarning"},
		Description:   "Deletes a warning, id is the first number of each warning from the warnings command",
		RequiredArgs:  1,
		Arguments: []*dcmd.ArgDef{
			{Name: "Id", Type: dcmd.Int},
			{Name: "Reason", Type: dcmd.String},
		},
		RequiredDiscordPermsHelp: "ManageMessages or ManageGuild",
		SlashCommandEnabled:      true,
		DefaultEnabled:           false,
		RunFunc: func(parsed *dcmd.Data) (interface{}, error) {
			config, _, err := MBaseCmd(parsed, 0)
			if err != nil {
				return nil, err
			}

			_, err = MBaseCmdSecond(parsed, "", true, discordgo.PermissionManageMessages, config.WarnCmdRoles, config.WarnCommandsEnabled, true)
			if err != nil {
				return nil, err
			}

			warningID := parsed.Args[0].Int()
			numDeleted, err := models.ModerationWarnings(
				models.ModerationWarningWhere.ID.EQ(warningID),

				models.ModerationWarningWhere.GuildID.EQ(parsed.GuildData.GS.ID),
			).DeleteAllG(parsed.Context())
			if err != nil {
				return "Failed deleting warning", err
			}
			if numDeleted == 0 {
				return fmt.Sprintf("Could not find warning with ID `%d`", warningID), nil
			}

			return "👌", nil
		},
	},
	{
		CustomEnabled: true,
		CmdCategory:   commands.CategoryModeration,
		Name:          "ClearWarnings",
		Aliases:       []string{"clw"},
		Description:   "Clears the warnings of a user",
		RequiredArgs:  1,
		Arguments: []*dcmd.ArgDef{
			{Name: "User", Type: dcmd.UserID},
			{Name: "Reason", Type: dcmd.String},
		},
		RequiredDiscordPermsHelp: "ManageMessages or ManageGuild",
		SlashCommandEnabled:      true,
		DefaultEnabled:           false,
		RunFunc: func(parsed *dcmd.Data) (interface{}, error) {

			config, target, err := MBaseCmd(parsed, parsed.Args[0].Int64())
			if err != nil {
				return nil, err
			}

			_, err = MBaseCmdSecond(parsed, "", true, discordgo.PermissionManageMessages, config.WarnCmdRoles, config.WarnCommandsEnabled, true)
			if err != nil {
				return nil, err
			}

			numDeleted, err := models.ModerationWarnings(
				models.ModerationWarningWhere.GuildID.EQ(parsed.GuildData.GS.ID),
				models.ModerationWarningWhere.UserID.EQ(discordgo.StrID(target.ID)),
			).DeleteAllG(parsed.Context())
			if err != nil {
				return "Failed deleting warnings", err
			}

			reason := parsed.Args[1].Str()
			err = CreateModlogEmbed(config, parsed.Author, MAClearWarnings, target, reason, "")
			if err != nil {
				return "Failed sending modlog", err
			}

			return fmt.Sprintf("Deleted %d warnings.", numDeleted), nil
		},
	},
	{
		CmdCategory: commands.CategoryModeration,
		Name:        "TopWarnings",
		Aliases:     []string{"topwarns"},
		Description: "Shows ranked list of warnings on the server",
		Arguments: []*dcmd.ArgDef{
			{Name: "Page", Type: dcmd.Int, Default: 0},
		},
		ArgSwitches: []*dcmd.ArgDef{
			{Name: "id", Help: "List userIDs"},
		},
		RequiredDiscordPermsHelp: "ManageMessages or ManageGuild",
		SlashCommandEnabled:      true,
		DefaultEnabled:           false,
		RunFunc: paginatedmessages.PaginatedCommand(0, func(parsed *dcmd.Data, p *paginatedmessages.PaginatedMessage, page int) (*discordgo.MessageEmbed, error) {

			showUserIDs := false
			config, _, err := MBaseCmd(parsed, 0)
			if err != nil {
				return nil, err
			}

			_, err = MBaseCmdSecond(parsed, "", true, discordgo.PermissionManageMessages, config.WarnCmdRoles, true, true)
			if err != nil {
				return nil, err
			}

			if parsed.Switches["id"].Value != nil && parsed.Switches["id"].Value.(bool) {
				showUserIDs = true
			}

			offset := (page - 1) * 15
			entries, err := TopWarns(parsed.GuildData.GS.ID, offset, 15)
			if err != nil {
				return nil, err
			}

			if len(entries) < 1 && p != nil && p.LastResponse != nil {
				return nil, paginatedmessages.ErrNoResults
			}

			embed := &discordgo.MessageEmbed{
				Title: "Ranked list of warnings",
			}

			out := "```\n# - Warns - User\n"
			for _, v := range entries {
				if !showUserIDs {
					user := v.Username
					if user == "" {
						user = "unknown ID:" + strconv.FormatInt(v.UserID, 10)
					}
					out += fmt.Sprintf("#%02d: %4d - %s\n", v.Rank, v.WarnCount, user)
				} else {
					out += fmt.Sprintf("#%02d: %4d - %d\n", v.Rank, v.WarnCount, v.UserID)
				}
			}

			count, err := models.ModerationWarnings(models.ModerationWarningWhere.GuildID.EQ(parsed.GuildData.GS.ID)).CountG(context.Background())
			if err != nil {
				return nil, err
			}

			out += "```\n" + fmt.Sprintf("Total Server Warnings: `%d`", count)

			embed.Description = out

			return embed, nil

		}),
	},
	{
		CustomEnabled: true,
		CmdCategory:   commands.CategoryModeration,
		Name:          "GiveRole",
		Aliases:       []string{"grole", "arole", "addrole"},
		Description:   "Gives a role to the specified member, with optional expiry",

		RequiredArgs: 2,
		Arguments: []*dcmd.ArgDef{
			{Name: "User", Type: dcmd.UserID},
			{Name: "Role", Type: &commands.RoleArg{}},
			{Name: "Duration", Type: &commands.DurationArg{}, Default: time.Duration(0)},
		},
		RequiredDiscordPermsHelp: "ManageRoles or ManageGuild",
		RequireBotPerms:          [][]int64{{discordgo.PermissionAdministrator}, {discordgo.PermissionManageGuild}, {discordgo.PermissionManageRoles}},
		SlashCommandEnabled:      true,
		DefaultEnabled:           false,
		RunFunc: func(parsed *dcmd.Data) (interface{}, error) {
			config, target, err := MBaseCmd(parsed, parsed.Args[0].Int64())
			if err != nil {
				return nil, err
			}

			_, err = MBaseCmdSecond(parsed, "", true, discordgo.PermissionManageRoles, config.GiveRoleCmdRoles, config.GiveRoleCmdEnabled, true)
			if err != nil {
				return nil, err
			}

			member, err := bot.GetMember(parsed.GuildData.GS.ID, target.ID)
			if err != nil || member == nil {
				return "Member not found", err
			}

			role := parsed.Args[1].Value.(*discordgo.Role)
			if role == nil {
				return "Couldn't find the specified role", nil
			}

			if !bot.IsMemberAboveRole(parsed.GuildData.GS, parsed.GuildData.MS, role) {
				return "Can't give roles above you", nil
			}

			dur := parsed.Args[2].Value.(time.Duration)

			if common.ContainsInt64Slice(member.Member.Roles, role.ID) && dur <= 0 {
				return "That user already has that role", nil
			}

			err = common.AddRoleDS(member, role.ID)
			if err != nil {
				return nil, err
			}

			if dur > 0 {
				err := scheduledevents2.ScheduleRemoveRole(parsed.Context(), parsed.GuildData.GS.ID, target.ID, role.ID, time.Now().Add(dur))
				if err != nil {
					return nil, err
				}
			}

			scheduledevents2.CancelAddRole(parsed.Context(), parsed.GuildData.GS.ID, parsed.Author.ID, role.ID)

			action := MAGiveRole
			action.Prefix = "Gave the role " + role.Name + " to "
			if config.GiveRoleCmdModlog && config.ActionChannel != 0 {
				if dur > 0 {
					action.Footer = "Duration: " + common.HumanizeDuration(common.DurationPrecisionMinutes, dur)
				}
				CreateModlogEmbed(config, parsed.Author, action, target, "", "")
			}

			return GenericCmdResp(action, target, dur, true, dur <= 0), nil
		},
	},
	{
		CustomEnabled: true,
		CmdCategory:   commands.CategoryModeration,
		Name:          "RemoveRole",
		Aliases:       []string{"rrole", "takerole", "trole"},
		Description:   "Removes the specified role from the target",

		RequiredArgs: 2,
		Arguments: []*dcmd.ArgDef{
			{Name: "User", Type: dcmd.UserID},
			{Name: "Role", Type: &commands.RoleArg{}},
		},
		RequiredDiscordPermsHelp: "ManageRoles or ManageGuild",
		RequireBotPerms:          [][]int64{{discordgo.PermissionAdministrator}, {discordgo.PermissionManageGuild}, {discordgo.PermissionManageRoles}},
		SlashCommandEnabled:      true,
		DefaultEnabled:           false,
		RunFunc: func(parsed *dcmd.Data) (interface{}, error) {
			config, target, err := MBaseCmd(parsed, parsed.Args[0].Int64())
			if err != nil {
				return nil, err
			}

			_, err = MBaseCmdSecond(parsed, "", true, discordgo.PermissionManageRoles, config.GiveRoleCmdRoles, config.GiveRoleCmdEnabled, true)
			if err != nil {
				return nil, err
			}

			member, err := bot.GetMember(parsed.GuildData.GS.ID, target.ID)
			if err != nil || member == nil {
				return "Member not found", err
			}

			role := parsed.Args[1].Value.(*discordgo.Role)
			if role == nil {
				return "Couldn't find the specified role", nil
			}

			if !bot.IsMemberAboveRole(parsed.GuildData.GS, parsed.GuildData.MS, role) {
				return "Can't remove roles above you", nil
			}

			err = common.RemoveRoleDS(member, role.ID)
			if err != nil {
				return nil, err
			}

			scheduledevents2.CancelRemoveRole(parsed.Context(), parsed.GuildData.GS.ID, parsed.Author.ID, role.ID)

			action := MARemoveRole
			action.Prefix = "Removed the role " + role.Name + " from "
			if config.GiveRoleCmdModlog && config.ActionChannel != 0 {
				CreateModlogEmbed(config, parsed.Author, action, target, "", "")
			}

			return GenericCmdResp(action, target, 0, true, true), nil
		},
	},
}
View Source
var PageHTML string

Functions

func AddMemberMuteRole

func AddMemberMuteRole(config *Config, id int64, currentRoles []int64) (removedRoles []int64, err error)

func BanUser

func BanUser(config *Config, guildID int64, channel *dstate.ChannelState, message *discordgo.Message, author *discordgo.User, reason string, user *discordgo.User, executedByCommandTemplate bool) error

func BanUserWithDuration

func BanUserWithDuration(config *Config, guildID int64, channel *dstate.ChannelState, message *discordgo.Message, author *discordgo.User, reason string, user *discordgo.User, duration time.Duration, deleteMessageDays int, executedByCommandTemplate bool) error

func CreateLogs

func CreateLogs(guildID, channelID int64, user *discordgo.User) string

func CreateModlogEmbed

func CreateModlogEmbed(config *Config, author *discordgo.User, action ModlogAction, target *discordgo.User, reason, logLink string) error

func DeleteMessages

func DeleteMessages(guildID, channelID int64, filterUser int64, deleteNum, fetchNum int) (int, error)

func FindAuditLogEntry

func FindAuditLogEntry(guildID int64, typ discordgo.AuditLogAction, targetUser int64, within time.Duration) (author *discordgo.User, entry *discordgo.AuditLogEntry)

func FindRole

func FindRole(gs *dstate.GuildSet, roleS string) *discordgo.Role

func GenericCmdResp

func GenericCmdResp(action ModlogAction, target *discordgo.User, duration time.Duration, zeroDurPermanent bool, noDur bool) string

func HandleChannelCreateUpdate

func HandleChannelCreateUpdate(evt *eventsystem.EventData) (retry bool, err error)

func HandleClearServerWarnings

func HandleClearServerWarnings(w http.ResponseWriter, r *http.Request) (web.TemplateData, error)

Clear all server warnigns

func HandleGuildBanAddRemove

func HandleGuildBanAddRemove(evt *eventsystem.EventData)

func HandleGuildCreate

func HandleGuildCreate(evt *eventsystem.EventData)

func HandleGuildMemberRemove

func HandleGuildMemberRemove(evt *eventsystem.EventData) (retry bool, err error)

func HandleGuildMemberTimeoutChange added in v2.4.0

func HandleGuildMemberTimeoutChange(evt *eventsystem.EventData) (retry bool, err error)

func HandleGuildMemberUpdate

func HandleGuildMemberUpdate(evt *eventsystem.EventData) (retry bool, err error)

func HandleMemberJoin

func HandleMemberJoin(evt *eventsystem.EventData) (retry bool, err error)

func HandleModeration

func HandleModeration(w http.ResponseWriter, r *http.Request) (web.TemplateData, error)

HandleModeration servers the moderation page itself

func HandlePostModeration

func HandlePostModeration(w http.ResponseWriter, r *http.Request) (web.TemplateData, error)

HandlePostModeration updates the settings

func HandleRefreshMuteOverrides

func HandleRefreshMuteOverrides(evt *pubsub.Event)

func HandleRefreshMuteOverridesCreateRole

func HandleRefreshMuteOverridesCreateRole(evt *pubsub.Event)

func KickUser

func KickUser(config *Config, guildID int64, channel *dstate.ChannelState, message *discordgo.Message, author *discordgo.User, reason string, user *discordgo.User, del int, executedFromCommandTemplate bool) error

func LockMemberMuteMW

func LockMemberMuteMW(next eventsystem.HandlerFunc) eventsystem.HandlerFunc

Since updating mutes are now a complex operation with removing roles and whatnot, to avoid weird bugs from happening we lock it so it can only be updated one place per user

func LockMute

func LockMute(uID int64)

func MBaseCmdSecond

func MBaseCmdSecond(cmdData *dcmd.Data, reason string, reasonArgOptional bool, neededPerm int64, additionalPermRoles []int64, enabled, additionalPermRolesAvailable bool) (oreason string, err error)

func MuteUnmuteUser

func MuteUnmuteUser(config *Config, mute bool, guildID int64, channel *dstate.ChannelState, message *discordgo.Message, author *discordgo.User, reason string, member *dstate.MemberState, duration int, executedByCommandTemplate bool) error

Unmut or mute a user, ignore duration if unmuting TODO: i don't think we need to track mutes in its own database anymore now with the new scheduled event system

func PaginateWarnings

func PaginateWarnings(parsed *dcmd.Data) func(p *paginatedmessages.PaginatedMessage, page int) (*discordgo.MessageEmbed, error)

func RedisKeyBannedUser

func RedisKeyBannedUser(guildID, userID int64) string

func RedisKeyLockedMute

func RedisKeyLockedMute(guildID, userID int64) string

func RedisKeyMutedUser

func RedisKeyMutedUser(guildID, userID int64) string

func RedisKeyUnbannedUser

func RedisKeyUnbannedUser(guildID, userID int64) string

func RefreshMuteOverrideForChannel

func RefreshMuteOverrideForChannel(config *Config, channel dstate.ChannelState)

func RefreshMuteOverrides

func RefreshMuteOverrides(guildID int64, createRole bool)

Refreshes the mute override on the channel, currently it only adds it.

func RegisterPlugin

func RegisterPlugin()

func RemoveMemberMuteRole

func RemoveMemberMuteRole(config *Config, id int64, currentRoles []int64, mute *models.MutedUser) (err error)

func RemoveTimeout added in v2.3.0

func RemoveTimeout(config *Config, guildID int64, author *discordgo.User, reason string, user *discordgo.User) error

func SafeArgString

func SafeArgString(data *dcmd.Data, arg int) string

func SaveConfig added in v2.40.0

func SaveConfig(config *Config) error

func TimeoutUser added in v2.3.0

func TimeoutUser(config *Config, guildID int64, channel *dstate.ChannelState, message *discordgo.Message, author *discordgo.User, reason string, user *discordgo.User, duration time.Duration, executedByCommandTemplate bool) error

func UnbanUser

func UnbanUser(config *Config, guildID int64, author *discordgo.User, reason string, user *discordgo.User) (bool, error)

func UnlockMute

func UnlockMute(uID int64)

func WarnUser

func WarnUser(config *Config, guildID int64, channel *dstate.ChannelState, msg *discordgo.Message, author *discordgo.User, target *discordgo.User, message string, executedByCommandTemplate bool) error

Types

type CombinedANDFilter added in v2.40.0

type CombinedANDFilter struct{ Filters []MessageFilter }

All the child filters need to match for the message to be deleted.

func (*CombinedANDFilter) Matches added in v2.40.0

func (f *CombinedANDFilter) Matches(msg *dstate.MessageState) (delete bool)

type Config

type Config struct {
	GuildID   int64
	CreatedAt time.Time
	UpdatedAt time.Time

	// Kick
	KickEnabled          bool
	KickCmdRoles         types.Int64Array `valid:"role,true"`
	DeleteMessagesOnKick bool
	KickReasonOptional   bool
	KickMessage          string `valid:"template,5000"`

	// Ban
	BanEnabled           bool
	BanCmdRoles          types.Int64Array `valid:"role,true"`
	BanReasonOptional    bool
	BanMessage           string     `valid:"template,5000"`
	DefaultBanDeleteDays null.Int64 `valid:"0,7"`

	// Timeout
	TimeoutEnabled              bool
	TimeoutCmdRoles             types.Int64Array `valid:"role,true"`
	TimeoutReasonOptional       bool
	TimeoutRemoveReasonOptional bool
	TimeoutMessage              string     `valid:"template,5000"`
	DefaultTimeoutDuration      null.Int64 `valid:"1,40320"`

	// Mute/unmute
	MuteEnabled             bool
	MuteCmdRoles            types.Int64Array `valid:"role,true"`
	MuteRole                int64            `valid:"role,true"`
	MuteDisallowReactionAdd bool
	MuteReasonOptional      bool
	UnmuteReasonOptional    bool
	MuteManageRole          bool
	MuteRemoveRoles         types.Int64Array `valid:"role,true"`
	MuteIgnoreChannels      types.Int64Array `valid:"channel,true"`
	MuteMessage             string           `valid:"template,5000"`
	UnmuteMessage           string           `valid:"template,5000"`
	DefaultMuteDuration     null.Int64       `valid:"0,"`

	// Warn
	WarnCommandsEnabled    bool
	WarnCmdRoles           types.Int64Array `valid:"role,true"`
	WarnIncludeChannelLogs bool
	WarnSendToModlog       bool
	WarnMessage            string `valid:"template,5000"`

	// Misc
	CleanEnabled  bool
	ReportEnabled bool
	ActionChannel int64 `valid:"channel,true"`
	ReportChannel int64 `valid:"channel,true"`
	ErrorChannel  int64 `valid:"channel,true"`
	LogUnbans     bool
	LogBans       bool
	LogKicks      bool
	LogTimeouts   bool

	GiveRoleCmdEnabled bool
	GiveRoleCmdModlog  bool
	GiveRoleCmdRoles   types.Int64Array `valid:"role,true"`
}

func BotCachedGetConfig added in v2.41.0

func BotCachedGetConfig(guildID int64) (*Config, error)

func BotCachedGetConfigIfNotSet added in v2.41.0

func BotCachedGetConfigIfNotSet(guildID int64, config *Config) (*Config, error)

func FetchConfig added in v2.41.0

func FetchConfig(guildID int64) (*Config, error)

func MBaseCmd

func MBaseCmd(cmdData *dcmd.Data, targetID int64) (config *Config, targetUser *discordgo.User, err error)

func (*Config) ToModel added in v2.40.0

func (c *Config) ToModel() *models.ModerationConfig

type ContextKey

type ContextKey int
const (
	ContextKeyConfig ContextKey = iota
)

type IgnorePinnedMessagesFilter added in v2.40.0

type IgnorePinnedMessagesFilter struct {
	PinnedMsgIDs map[int64]struct{}
}

Do not delete pinned messages.

func NewIgnorePinnedMessagesFilter added in v2.40.0

func NewIgnorePinnedMessagesFilter(pinned []*discordgo.Message) *IgnorePinnedMessagesFilter

func (*IgnorePinnedMessagesFilter) Matches added in v2.40.0

func (f *IgnorePinnedMessagesFilter) Matches(msg *dstate.MessageState) (delete bool)

type MessageAgeFilter added in v2.40.0

type MessageAgeFilter struct {
	ReferenceTime time.Time // Calculate the age of messages relative to this time.

	// 0 means no min age requirement (and likewise for max age.)
	MinAge time.Duration
	MaxAge time.Duration
}

Only delete messages satisfying MinAge<=age<=MaxAge.

func (*MessageAgeFilter) Matches added in v2.40.0

func (f *MessageAgeFilter) Matches(msg *dstate.MessageState) (delete bool)

type MessageAuthorFilter added in v2.40.0

type MessageAuthorFilter struct{ UserID int64 }

Only delete messages from the specified user.

func (*MessageAuthorFilter) Matches added in v2.40.0

func (f *MessageAuthorFilter) Matches(msg *dstate.MessageState) (delete bool)

type MessageFilter added in v2.40.0

type MessageFilter interface {
	Matches(msg *dstate.MessageState) (delete bool)
}

type MessageIDFilter added in v2.40.0

type MessageIDFilter struct {
	// 0 means no start ID set (and likewise for end ID.)
	FromID int64
	ToID   int64
}

Only delete messages satisfying ToID<=id<=FromID.

func (*MessageIDFilter) Matches added in v2.40.0

func (f *MessageIDFilter) Matches(msg *dstate.MessageState) (delete bool)

type MessagesWithAttachmentsFilter added in v2.40.0

type MessagesWithAttachmentsFilter struct{}

Only delete messages with attachments.

func (*MessagesWithAttachmentsFilter) Matches added in v2.40.0

func (*MessagesWithAttachmentsFilter) Matches(msg *dstate.MessageState) (delete bool)

type ModlogAction

type ModlogAction struct {
	Prefix string
	Emoji  string
	Color  int

	Footer string
}

func (ModlogAction) String

func (m ModlogAction) String() string

type Plugin

type Plugin struct{}

func (*Plugin) AddCommands

func (p *Plugin) AddCommands()

func (*Plugin) AllFeatureFlags

func (p *Plugin) AllFeatureFlags() []string

func (*Plugin) BotInit

func (p *Plugin) BotInit()

func (*Plugin) InitWeb

func (p *Plugin) InitWeb()

func (*Plugin) LoadServerHomeWidget

func (p *Plugin) LoadServerHomeWidget(w http.ResponseWriter, r *http.Request) (web.TemplateData, error)

func (*Plugin) PluginInfo

func (p *Plugin) PluginInfo() *common.PluginInfo

func (*Plugin) ShardMigrationReceive

func (p *Plugin) ShardMigrationReceive(evt dshardorchestrator.EventType, data interface{})

func (*Plugin) UpdateFeatureFlags

func (p *Plugin) UpdateFeatureFlags(guildID int64) ([]string, error)

type Punishment

type Punishment int
const (
	PunishmentKick Punishment = iota
	PunishmentBan
	PunishmentTimeout
)

type RegExpFilter added in v2.40.0

type RegExpFilter struct {
	InvertMatch bool
	Re          *regexp.Regexp
}

Only delete messages matching the regex (or, if InvertMatch==true, only delete messages not matching the regex.)

func (*RegExpFilter) Matches added in v2.40.0

func (f *RegExpFilter) Matches(msg *dstate.MessageState) (delete bool)

type ScheduledUnbanData

type ScheduledUnbanData struct {
	UserID int64 `json:"user_id"`
}

type ScheduledUnmuteData

type ScheduledUnmuteData struct {
	UserID int64 `json:"user_id"`
}

type TemplatesWarning added in v2.40.0

type TemplatesWarning struct {
	ID        int
	CreatedAt time.Time
	UpdatedAt time.Time

	GuildID int64
	UserID  int64

	AuthorID              string
	AuthorUsernameDiscrim string

	Message  string
	LogsLink string
}

Needed to maintain backward compatibility with previous implementation of getWarnings using gorm, in which LogsLink was marked as a string instead of a null.String.

type WarnRankEntry

type WarnRankEntry struct {
	Rank      int    `json:"rank"`
	UserID    int64  `json:"user_id"`
	Username  string `json:"username"`
	WarnCount int64  `json:"warn_count"`
}

func TopWarns

func TopWarns(guildID int64, offset, limit int) ([]*WarnRankEntry, error)

Directories

Path Synopsis

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL