diff --git a/moderation/commands.go b/moderation/commands.go index 91a063de0..a5176aa31 100644 --- a/moderation/commands.go +++ b/moderation/commands.go @@ -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 "), 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 }, }, { @@ -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 {