Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

moderation: refactor clean command #1669

Merged
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
338 changes: 168 additions & 170 deletions moderation/commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -642,118 +642,114 @@ var ModerationCommands = []*commands.YAGCommand{
return nil, err
}

userFilter := parsed.Args[1].Int64()
var filters []MessageFilter

num := parsed.Args[0].Int()

var triggerID int64
ignoreTrigger := parsed.Source != dcmd.TriggerSourceDM && parsed.Context().Value(commands.CtxKeyExecutedByCC) == nil
if ignoreTrigger {
if parsed.TriggerType == dcmd.TriggerTypeSlashCommands {
m, err := common.BotSession.GetOriginalInteractionResponse(common.BotApplication.ID, parsed.SlashCommandTriggerData.Interaction.Token)
if err != nil {
return nil, err
}

triggerID = m.ID
} else {
triggerID = parsed.TraditionalTriggerData.Message.ID
}
}

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

if num < 1 {
if num < 0 {
return errors.New("Bot is having a stroke <https://www.youtube.com/watch?v=dQw4w9WgXcQ>"), nil
}
return errors.New("Can't delete nothing"), nil
}

filtered := false

// Check if we should regex match this
re := ""
if parsed.Switches["r"].Value != nil {
filtered = true
re = parsed.Switches["r"].Str()

// Add the case insensitive flag if needed
if parsed.Switches["i"].Value != nil && parsed.Switches["i"].Value.(bool) {
if re := parsed.Switches["r"].Str(); re != "" {
if caseInsensitive := parsed.Switches["i"].Bool(); caseInsensitive {
if !strings.HasPrefix(re, "(?i)") {
re = "(?i)" + re
}
}
}
invertRegexMatch := parsed.Switch("im").Value != nil && parsed.Switch("im").Value.(bool)

// Check if we have a max age
ma := parsed.Switches["ma"].Value.(time.Duration)
if ma != 0 {
filtered = true
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})
}

// Check if we have a min age
now := time.Now()
minAge := parsed.Switches["minage"].Value.(time.Duration)
if minAge != 0 {
filtered = true
maxAge := parsed.Switches["ma"].Value.(time.Duration)
if minAge != 0 || maxAge != 0 {
filters = append(filters, &MessageAgeFilter{ReferenceTime: now, MinAge: minAge, MaxAge: maxAge})
}

// Check if set to break at a certain ID
toID := int64(0)
if parsed.Switches["to"].Value != nil {
filtered = true
toID = parsed.Switches["to"].Int64()
fromID := parsed.Switches["from"].Int64()
toID := parsed.Switches["to"].Int64()
if fromID != 0 || toID != 0 {
filters = append(filters, &MessageIDFilter{FromID: fromID, ToID: toID})
}

// Check if set to break at a certain ID
fromID := int64(0)
if parsed.Switches["from"].Value != nil {
filtered = true
fromID = parsed.Switches["from"].Int64()
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 toID > 0 && fromID > 0 && fromID < toID {
return errors.New("from messageID cannot be less than to messageID"), nil
if onlyDeleteWithAttachments := parsed.Switches["a"].Bool(); onlyDeleteWithAttachments {
filters = append(filters, &MessagesWithAttachmentsFilter{})
}

// Check if we should ignore pinned messages
pe := false
if parsed.Switches["nopin"].Value != nil && parsed.Switches["nopin"].Value.(bool) {
pe = true
filtered = true
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
}

// Check if we should only delete messages with attachments
attachments := false
if parsed.Switches["a"].Value != nil && parsed.Switches["a"].Value.(bool) {
attachments = true
filtered = true
deleteLimit := parsed.Args[0].Int()
fetchLimit := deleteLimit + 1 // +1 for triggering message
if len(filters) > 0 {
fetchLimit = deleteLimit * 50
}

limitFetch := num
if userFilter != 0 || filtered {
limitFetch = num * 50 // Maybe just change to full fetch?
if fetchLimit > 1000 {
fetchLimit = 1000
}

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

var toDelete []int64
filter := CombinedANDFilter{filters} // all filters need to match for message to be deleted
for _, msg := range msgs {
// Can only bulk delete messages up to 2 weeks old (but add 1 minute buffer to be safe.)
if now.Sub(msg.ParsedCreatedAt) > (14*time.Hour*24)-time.Minute {
continue
}
// Don't delete the trigger message.
if msg.ID == triggerID {
continue
}

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

// Wait a second so the client dosen't gltich out
time.Sleep(time.Second)
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)
}

numDeleted, err := AdvancedDeleteMessages(parsed.GuildData.GS.ID, parsed.ChannelID, triggerID, userFilter, re, invertRegexMatch, toID, fromID, ma, minAge, pe, attachments, num, limitFetch)
deleteMessageWord := "messages"
if numDeleted == 1 {
deleteMessageWord = "message"
if err != nil {
return "Failed deleting messages", err
}
return dcmd.NewTemporaryResponse(time.Second*5, fmt.Sprintf("Deleted %d %s! :')", numDeleted, deleteMessageWord), true), err
return dcmd.NewTemporaryResponse(time.Second*5, resp, true), nil
},
},
{
Expand Down Expand Up @@ -1213,111 +1209,113 @@ var ModerationCommands = []*commands.YAGCommand{
},
}

func AdvancedDeleteMessages(guildID, channelID int64, triggerID int64, filterUser int64, regex string, invertRegexMatch bool, toID int64, fromID int64, maxAge time.Duration, minAge time.Duration, pinFilterEnable bool, attachmentFilterEnable bool, deleteNum, fetchNum int) (int, error) {
var compiledRegex *regexp.Regexp
if regex != "" {
// Start by compiling the regex
var err error
compiledRegex, err = regexp.Compile(regex)
if err != nil {
return 0, err
}
}
type MessageFilter interface {
Matches(msg *dstate.MessageState) (delete bool)
}

var pinnedMessages map[int64]struct{}
if pinFilterEnable {
//Fetch pinned messages from channel and make a map with ids as keys which will make it easy to verify if a message with a given ID is pinned message
messageSlice, err := common.BotSession.ChannelMessagesPinned(channelID)
if err != nil {
return 0, err
}
pinnedMessages = make(map[int64]struct{}, len(messageSlice))
for _, msg := range messageSlice {
pinnedMessages[msg.ID] = struct{}{} //empty struct works because we are not really interested in value
// All the child filters need to match for the message to be deleted.
type CombinedANDFilter struct{ Filters []MessageFilter }

func (f *CombinedANDFilter) Matches(msg *dstate.MessageState) (delete bool) {
for _, filter := range f.Filters {
if !filter.Matches(msg) {
return false
}
}
return true
}

msgs, err := bot.GetMessages(guildID, channelID, fetchNum, false)
if err != nil {
return 0, err
}
// Only delete messages from the specified user.
type MessageAuthorFilter struct{ UserID int64 }

toDelete := make([]int64, 0)
now := time.Now()
for i := 0; i < len(msgs); i++ {
if msgs[i].ID == triggerID {
continue
}
func (f *MessageAuthorFilter) Matches(msg *dstate.MessageState) (delete bool) {
return msg.Author.ID == f.UserID
}

if filterUser != 0 && msgs[i].Author.ID != filterUser {
continue
}
// Only delete messages matching the regex (or, if InvertMatch==true, only
// delete messages not matching the regex.)
type RegExpFilter struct {
InvertMatch bool
Re *regexp.Regexp
}

// Can only bulk delete messages up to 2 weeks (but add 1 minute buffer account for time sync issues and other smallies)
if now.Sub(msgs[i].ParsedCreatedAt) > (time.Hour*24*14)-time.Minute {
continue
}
func (f *RegExpFilter) Matches(msg *dstate.MessageState) (delete bool) {
delete = f.Re.MatchString(msg.Content)
if f.InvertMatch {
delete = !delete
}
return
}

// Check regex
if compiledRegex != nil {
ok := compiledRegex.MatchString(msgs[i].Content)
if invertRegexMatch {
ok = !ok
}
if !ok {
continue
}
}
// Only delete messages satisfying MinAge<=age<=MaxAge.
type MessageAgeFilter struct {
ReferenceTime time.Time // Calculate the age of messages relative to this time.

// Check max age
if maxAge != 0 && now.Sub(msgs[i].ParsedCreatedAt) > maxAge {
continue
}
// 0 means no min age requirement (and likewise for max age.)
MinAge time.Duration
MaxAge time.Duration
}

// Check min age
if minAge != 0 && now.Sub(msgs[i].ParsedCreatedAt) < minAge {
continue
}
func (f *MessageAgeFilter) Matches(msg *dstate.MessageState) (delete bool) {
age := f.ReferenceTime.Sub(msg.ParsedCreatedAt)
if f.MinAge != 0 && age < f.MinAge {
return false
}
if f.MaxAge != 0 && age > f.MaxAge {
return false
}
return true
}

// Check if pinned message to ignore
if pinFilterEnable {
if _, found := pinnedMessages[msgs[i].ID]; found {
continue
}
}
// Do not delete pinned messages.
type IgnorePinnedMessagesFilter struct {
PinnedMsgIDs map[int64]struct{}
}

// Continue only if current msg ID is > fromID
if fromID > 0 && fromID < msgs[i].ID {
continue
}
func NewIgnorePinnedMessagesFilter(pinned []*discordgo.Message) *IgnorePinnedMessagesFilter {
ids := make(map[int64]struct{})
for _, msg := range pinned {
ids[msg.ID] = struct{}{}
}
return &IgnorePinnedMessagesFilter{ids}
}

// Continue only if current msg ID is < toID
if toID > 0 && toID > msgs[i].ID {
continue
}
func (f *IgnorePinnedMessagesFilter) Matches(msg *dstate.MessageState) (delete bool) {
if _, pinned := f.PinnedMsgIDs[msg.ID]; pinned {
return false
}
return true
}

// Check whether to ignore messages without attachments
if attachmentFilterEnable && len(msgs[i].Attachments) == 0 {
continue
}
// Only delete messages with attachments.
type MessagesWithAttachmentsFilter struct{}

toDelete = append(toDelete, msgs[i].ID)
func (*MessagesWithAttachmentsFilter) Matches(msg *dstate.MessageState) (delete bool) {
return len(msg.Attachments) > 0
}

//log.Println("Deleting", msgs[i].ContentWithMentionsReplaced())
if len(toDelete) >= deleteNum || len(toDelete) >= 100 {
break
}
}
// Only delete messages satisfying ToID<=id<=FromID.
type MessageIDFilter struct {
// 0 means no start ID set (and likewise for end ID.)
FromID int64
ToID int64
}

if len(toDelete) < 1 {
return 0, nil
} else if len(toDelete) == 1 {
err = common.BotSession.ChannelMessageDelete(channelID, toDelete[0])
} else {
err = common.BotSession.ChannelMessagesBulkDelete(channelID, toDelete)
func (f *MessageIDFilter) Matches(msg *dstate.MessageState) (delete bool) {
// Don't delete if id < ToID or id > FromID.
//
// increasing id ------------->
// ToID ... FromID
// ^ ^
// | |
// id < ToID id > FromID
if f.ToID != 0 && msg.ID < f.ToID {
return false
}

return len(toDelete), err
if f.FromID != 0 && msg.ID > f.FromID {
return false
}
return true
}

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