From 574edde0308217062a87b41701467b812802e87d Mon Sep 17 00:00:00 2001 From: Dallas Winger Date: Thu, 9 May 2024 20:44:38 -0400 Subject: [PATCH] start building out automatic spam detection, push some stale changes, very incomplete --- commands/handlers.go | 4 ++ commands/moderation.go | 5 +- commands/monitoring.go | 116 +++++++++++++++++++++++++++++++++++++++++ commands/products.go | 40 ++++++++++++++ models/monitoring.go | 18 +++++++ models/products.go | 60 +++++++++++++-------- 6 files changed, 218 insertions(+), 25 deletions(-) create mode 100644 commands/monitoring.go create mode 100644 models/monitoring.go diff --git a/commands/handlers.go b/commands/handlers.go index 1fea8a8..b0e6bf0 100644 --- a/commands/handlers.go +++ b/commands/handlers.go @@ -204,11 +204,15 @@ func (c *Commands) ProcessMessage(s *discordgo.Session, m interface{}) { switch m.(type) { case *discordgo.MessageCreate: // Pass Messages to the command processor + track(m.(*discordgo.MessageCreate).Message) err := c.ProcessCommand(s, m.(*discordgo.MessageCreate)) if err != nil { log.Println("[!] Error: " + err.Error()) } break + case *discordgo.MessageEdit: + //TODO logging + break case *discordgo.MessageDelete: // Log deleted messages to the logging channel. err := c.ProcessMessageDelete(s, m.(*discordgo.MessageDelete)) diff --git a/commands/moderation.go b/commands/moderation.go index 5835a26..a7b2f9b 100644 --- a/commands/moderation.go +++ b/commands/moderation.go @@ -2,12 +2,11 @@ package commands import ( "errors" + "github.com/bwmarrin/discordgo" + "github.com/foxtrot/scuzzy/actions" "strconv" "strings" "time" - - "github.com/bwmarrin/discordgo" - "github.com/foxtrot/scuzzy/actions" ) func (c *Commands) handleSetSlowmode(s *discordgo.Session, m *discordgo.MessageCreate) error { diff --git a/commands/monitoring.go b/commands/monitoring.go new file mode 100644 index 0000000..7634b6e --- /dev/null +++ b/commands/monitoring.go @@ -0,0 +1,116 @@ +package commands + +import ( + "github.com/bwmarrin/discordgo" + "github.com/foxtrot/scuzzy/models" + "hash/fnv" + "time" +) + +var Tracking map[uint32]models.TrackedMessage +var windowToConsiderIrrelevant = time.Minute * 5 +var windowToConsiderDuplicate = time.Second * 30 +var crossChannelSpamCountThreshold = 1 +var spamCountThreshold = 3 + +func hashContent(s string) uint32 { + h := fnv.New32a() + _, err := h.Write([]byte(s)) + if err != nil { + return 0 + } + return h.Sum32() +} + +func track(m *discordgo.Message) bool { + //todo ignore self bot + hash := hashContent(m.Content) + record, exists := Tracking[hash] + if !exists { + newMessage := models.TrackedMessage{ + Author: m.Author, + Instances: map[string]models.CachedMessage{}, + MessageContent: m.Content, + } + newMessage.Instances[m.ChannelID] = models.CachedMessage{ + ID: m.ID, + CreatedAt: time.Now(), + Count: 1, + } + Tracking[hash] = newMessage + } else { + //ModerationReason := "[DELETED + TIMEOUT] Multiple of the same exact message sent repeatedly in the same channel within a short period of time is considered spam and was removed" + chanMessage, existsInChannel := record.Instances[m.ChannelID] + if existsInChannel { + chanMessage.Count++ + record.Instances[m.ChannelID] = chanMessage + Tracking[hash] = record + + if time.Since(chanMessage.CreatedAt) > windowToConsiderIrrelevant { + // this tracking has "expired" + delete(record.Instances, m.ChannelID) + Tracking[hash] = record + return false + } + countViolation := chanMessage.Count > spamCountThreshold + frequencyViolation := time.Since(chanMessage.CreatedAt) < windowToConsiderDuplicate + switch true { + case countViolation && frequencyViolation: + // remove obviously spam qualified by both frequency and quantity + cleanupDuplicates(record.Instances) + timeoutUser() + return true + case countViolation: + cleanupDuplicates(record.Instances) + return true + case frequencyViolation: + cleanupDuplicates(record.Instances) + return true + default: + warnUser() + return false + } + } else { + channelCount := 0 + smallestTimeSince := time.Duration(10000000) + for _, _ = range record.Instances { + channelCount++ + since := time.Since(chanMessage.CreatedAt) + if smallestTimeSince < since { + smallestTimeSince = since + } + } + countViolation := channelCount > crossChannelSpamCountThreshold + frequencyViolation := smallestTimeSince < windowToConsiderDuplicate + switch true { + case countViolation && frequencyViolation: + // remove obviously spam qualified by both frequency and quantity + cleanupDuplicates(record.Instances) + timeoutUser() + return true + case countViolation: + cleanupDuplicates(record.Instances) + return true + case frequencyViolation: + cleanupDuplicates(record.Instances) + return true + default: + warnUser() + return false + } + } + } + return false +} + +func cleanupDuplicates(instances map[string]models.CachedMessage) { + +} + +func warnUser() { + +} + +func timeoutUser() { + +} diff --git a/commands/products.go b/commands/products.go index b0704ba..0d9021c 100644 --- a/commands/products.go +++ b/commands/products.go @@ -11,6 +11,46 @@ import ( "strings" ) +// TODO for use in command arguments +/* Match on one two or three word arguments*/ +func aliasMatch(product models.Product, args []string) bool { + return strings.Contains(strings.Join(product.Aliases[:], " "), args[1]) || + strings.Contains(strings.Join(product.Aliases, " "), strings.Join(args[1:3], " ")) || + strings.Contains(strings.Join(product.Aliases, " "), strings.Join(args[1:4], " ")) +} + +func (c *Commands) matchAlias(args []string) models.Product { + switch true { + case aliasMatch(models.Ducky, args): + return models.Ducky + case aliasMatch(models.PayloadStudio, args): + return models.PayloadStudio + case aliasMatch(models.Bunny, args): + return models.Bunny + case aliasMatch(models.Croc, args): + return models.Croc + case aliasMatch(models.Shark, args): + return models.Shark + case aliasMatch(models.CloudC2, args): + return models.CloudC2 + case aliasMatch(models.Crab, args): + return models.Crab + case aliasMatch(models.Pineapple, args): + return models.Pineapple + case aliasMatch(models.Coconut, args): + return models.Coconut + case aliasMatch(models.Squirrel, args): + return models.Squirrel + case aliasMatch(models.Turtle, args): + return models.Turtle + case aliasMatch(models.OMG, args): + return models.OMG + default: + return models.Default + } +} + +// why i didnt just use a map? because im dumb func (c *Commands) getProductFromChannelID(cid string) models.Product { switch cid { case models.Ducky.ChannelID: diff --git a/models/monitoring.go b/models/monitoring.go new file mode 100644 index 0000000..35e36b5 --- /dev/null +++ b/models/monitoring.go @@ -0,0 +1,18 @@ +package models + +import ( + "github.com/bwmarrin/discordgo" + "time" +) + +type CachedMessage struct { + ID string + CreatedAt time.Time + Count int +} + +type TrackedMessage struct { + Author *discordgo.User `json:"author"` + Instances map[string]CachedMessage + MessageContent string +} diff --git a/models/products.go b/models/products.go index 399d6da..5e90cf6 100644 --- a/models/products.go +++ b/models/products.go @@ -1,38 +1,43 @@ package models type Product struct { - ProductName string `json:"productName"` - Urllabel string `json:"urllabel"` - Emoji string `json:"logo"` - ChannelID string `json:"channelid"` - SpaceID string `json:"spaceid"` - Docslink string `json:"docslink"` - Shoplink string `json:"shoplink"` - Payloadslink string `json:"payloadslink"` - Githublink string `json:"githublink"` - InviteLink string `json:"invitelink"` - SupportLink string `json:"supportlink"` + ProductName string `json:"productName"` + Aliases []string `json:"aliases"` + Urllabel string `json:"urllabel"` + Emoji string `json:"logo"` + ChannelID string `json:"channelid"` + SpaceID string `json:"spaceid"` + Docslink string `json:"docslink"` + Shoplink string `json:"shoplink"` + Payloadslink string `json:"payloadslink"` + Githublink string `json:"githublink"` + InviteLink string `json:"invitelink"` + SupportLink string `json:"supportlink"` + GettingStarted string `json:"gettingstarted"` } var GITBOOKAPI = "https://api.gitbook.com/v1/spaces/" var SEARCHENDPOINT = "/search/ask" var Default = Product{ - ProductName: "", - Urllabel: "", - Emoji: "<:hak5:1063661436187975701>", - ChannelID: "824326326076702770", - SpaceID: "", - Docslink: "https://docs.hak5.org", - Shoplink: "https://shop.hak5.org", - Payloadslink: "https://payloadhub.com", - Githublink: "https://github.com/hak5", - InviteLink: "https://discord.gg/hak5", - SupportLink: "https://hak5.org/support", + ProductName: "", + Aliases: []string{}, + Urllabel: "", + Emoji: "<:hak5:1063661436187975701>", + ChannelID: "824326326076702770", + SpaceID: "", + Docslink: "https://docs.hak5.org", + Shoplink: "https://shop.hak5.org", + Payloadslink: "https://payloadhub.com", + Githublink: "https://github.com/hak5", + InviteLink: "https://discord.gg/hak5", + SupportLink: "https://hak5.org/support", + GettingStarted: "https://docs.hak5.org", } var Ducky = Product{ ProductName: "USB Rubber Ducky", + Aliases: []string{"usb rubber ducky", "ducky", "duck", "rubber ducky"}, Urllabel: "usb-rubber-ducky", Emoji: "<:Rubber_Ducky:1063661661795385384>", ChannelID: "522275837651714048", @@ -48,6 +53,7 @@ var Ducky = Product{ var PayloadStudio = Product{ ProductName: "PayloadStudio", Urllabel: Default.Urllabel, + Aliases: []string{"PayloadStudio", "PS", "PSP", "payload studio"}, Emoji: "<:payload_studio:1053210968064262234>", ChannelID: "1006233482957164634", SpaceID: "RgTCQkzfO7AUWTT3gFAq", @@ -61,6 +67,7 @@ var PayloadStudio = Product{ var CloudC2 = Product{ ProductName: "Cloud C2", Urllabel: Default.Urllabel, + Aliases: []string{"CloudC2", "Cloud C2", "C2"}, Emoji: "<:Cloud_C2:1063661578181943336>", ChannelID: "522276096746717184", SpaceID: "<:Cloud_C2:1063661578181943336>", @@ -74,6 +81,7 @@ var CloudC2 = Product{ var Crab = Product{ ProductName: "Screen Crab", Urllabel: Default.Urllabel, + Aliases: []string{"screen crab", "crab"}, Emoji: "<:Screen_Crab:1063661831756988427>", ChannelID: "608057573517557809", SpaceID: "-MiWySN4BHDJlUatEfm3", @@ -87,6 +95,7 @@ var Crab = Product{ var Coconut = Product{ ProductName: "WiFi Coconut", Urllabel: Default.Urllabel, + Aliases: []string{"wifi coconut", "coconut"}, Emoji: "<:WiFi_Coconut:1063661830309953606>", ChannelID: "1007363521098551349", SpaceID: "DkilLranx3TNqKgzbBZ9", @@ -100,6 +109,7 @@ var Coconut = Product{ var Pineapple = Product{ ProductName: "WiFi Pineapple", Urllabel: Default.Urllabel, + Aliases: []string{"wifi pineapple", "pineapple"}, Emoji: "<:WiFi_Pineapple:1063661628207411251>", ChannelID: "522275782731497473", SpaceID: "-Mhuhsyl_byoEWXOc5EU", @@ -114,6 +124,7 @@ var Croc = Product{ ProductName: "Key Croc", Urllabel: "key-croc", Emoji: "<:Key_Croc:1063661834093199431>", + Aliases: []string{"key croc", "croc"}, ChannelID: "709235715120168970", SpaceID: "-MhLOzjhonMdC6SLKqRt", Docslink: "https://docs.hak5.org/key-croc/", @@ -127,6 +138,7 @@ var Bunny = Product{ ProductName: "Bash Bunny", Urllabel: "bash-bunny", Emoji: "<:Bash_Bunny:1063661828753858680>", + Aliases: []string{"bash bunny", "bunny", "bb"}, ChannelID: "1009919913877590146", SpaceID: "nxJgJ9UdPfrcuL1U8DpL", Docslink: "https://docs.hak5.org/bash-bunny/", @@ -140,6 +152,7 @@ var Squirrel = Product{ ProductName: "Packet Squirrel", Urllabel: "packet-squirrel", Emoji: "<:packet_squirrel:1063661837398315079>", + Aliases: []string{"packet squirrel", "squirrel"}, ChannelID: "522275913031745548", SpaceID: "-MiX86qQFvjhg-a6RwWr", Docslink: "https://docs.hak5.org/packet-squirrel", @@ -152,6 +165,7 @@ var Squirrel = Product{ var Turtle = Product{ ProductName: "LAN Turtle", Urllabel: "lan-turtle", + Aliases: []string{"lan turtle", "turtle"}, Emoji: "<:lan_turtle:1063661838937620480>", ChannelID: "522275913031745548", SpaceID: "N8g6UIGOyv4mW7rOOuC8", @@ -165,6 +179,7 @@ var Turtle = Product{ var Shark = Product{ ProductName: "Shark Jack", Urllabel: "shark-jack", + Aliases: []string{"shark jack", "shark"}, Emoji: "<:Shark_Jack:1063661835888381952>", ChannelID: "610344558655438858", SpaceID: "-MhxW6geyenAGJvaKW11", @@ -178,6 +193,7 @@ var Shark = Product{ var OMG = Product{ ProductName: "O.MG Devices", Urllabel: "omg", + Aliases: []string{"omg", "omg cable", "omg adapter", "omg plug", "plug", "cable"}, Emoji: "<:omg:1063696145022468116>", ChannelID: "953129985131040838", SpaceID: Default.SpaceID,