Skip to content

Commit

Permalink
moderation: refactor clean command (botlabs-gg#1669)
Browse files Browse the repository at this point in the history
  • Loading branch information
jo3-l authored and ashishjh-bst committed Jun 27, 2024
1 parent 77e8e54 commit 9569c93
Showing 1 changed file with 168 additions and 170 deletions.
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

0 comments on commit 9569c93

Please sign in to comment.