Skip to content

Commit

Permalink
Continued WiP
Browse files Browse the repository at this point in the history
Added some additional functions/checks
Still lots of work to do.
  • Loading branch information
FM1337 committed Apr 13, 2024
1 parent 6ba470f commit 8d504c4
Show file tree
Hide file tree
Showing 7 changed files with 246 additions and 6 deletions.
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# ASB

A work in progress anti spam bot for discord.
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -41,4 +41,5 @@ require (
golang.org/x/sys v0.19.0 // indirect
golang.org/x/text v0.14.0 // indirect
golang.org/x/tools v0.20.0 // indirect
mvdan.cc/xurls/v2 v2.5.0 // indirect
)
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -160,3 +160,5 @@ gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C
gopkg.in/yaml.v3 v3.0.0-20200605160147-a5ece683394c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
mvdan.cc/xurls/v2 v2.5.0 h1:lyBNOm8Wo71UknhUs4QTFUNNMyxy2JEIaKKo0RWOh+8=
mvdan.cc/xurls/v2 v2.5.0/go.mod h1:yQgaGQ1rFtJUzkmKiHYSSfuQxqfYmd//X6PxvholpeE=
112 changes: 111 additions & 1 deletion internal/bot/handlers/message.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,15 @@
package handlers

import "github.com/bwmarrin/discordgo"
import (
"crypto/md5"
"fmt"
"regexp"
"strings"

"github.com/FM1337/ASB/internal/models"
"github.com/bwmarrin/discordgo"
"mvdan.cc/xurls/v2"
)

func (h *Handlers) MessageCreate(s *discordgo.Session, m *discordgo.MessageCreate) {
// Ignore all messages created by the bot itself
Expand All @@ -14,4 +23,105 @@ func (h *Handlers) MessageCreate(s *discordgo.Session, m *discordgo.MessageCreat
return
}

// Check if the bot is enabled for the server or if it should ignore the message

if !h.data.Enabled(m.GuildID) || !h.data.ShouldIgnore(m.GuildID, m.ChannelID, m.Author.ID, m.Member.Roles) {
// Do nothing
return
}

// used to determine the level of suspiciousness the message/author is at.
susLevel := 0
// if this gets set to true, we stop all remaining checks and take an action
instaAction := false

// grab the config
cfg := h.data.GetConfiguration(m.GuildID)

// If enabled, messages containing links should increase the sus level
if cfg.FlagLinks && xurls.Relaxed().MatchString(m.Content) {
susLevel++
}

// we should check for blacklisted words and increase if any are found
if h.data.CheckForBlacklistedContent(m.Content, m.GuildID) {
susLevel++
}

if susLevel == 0 {
// At this point if the level is still 0, it's more than likely not spam so we can bail here
return
}

// if the sus level isn't 0 and ratelimit is turned on, let's start that magic

if cfg.EnforceRatelimit {
// hash the message
hash := fmt.Sprintf("%x", md5.Sum([]byte(strings.ToLower(m.Content))))
seenBefore, count := h.data.MessageSeenBefore(m.GuildID, m.Author.ID, hash)
if count+1 >= cfg.RateLimitCount {
// User has hit the limit, we need to action now
instaAction = true
} else if seenBefore {
// okay we've seen this messag before from this user in the server within the
// rate limit time, let's reset the timer for them and increment their count
} else {
// first time seeing this message, let's create a new entry for them.
}
}

if instaAction {
// Take action now.
doAction(s, m, &cfg)
return
}

if cfg.CheckInvites {
// check for an invite
hasInvite, inviteCode := grabDiscordLink(m.Content)
if hasInvite {
// get information about the invite
invite, err := s.Invite(inviteCode)
if err != nil {
// error handling
return
}
// increase the sus level by 1 just for containing an invite
susLevel++

if h.data.CheckForBlacklistedContent(invite.Guild.Name, m.ChannelID) {
// Blackedlisted content found, sus goes up by 1
susLevel++
}
}
}

}

func grabDiscordLink(msg string) (bool, string) {
r, _ := regexp.Compile("discord.gg/[a-zA-z0-9]{1,10}")

match := r.FindString(strings.ToLower(msg))

split := strings.Split(match, "/")

if len(split) == 2 {
match = split[1]
}

return match != "", match
}

func doAction(s *discordgo.Session, m *discordgo.MessageCreate, cfg *models.ServerConfig) {
if cfg.Ban {
// Note until discordgo implements the ban delete message seconds query param, we'll just default to 1 day
// Easiest action to take, we ban and clear

err := s.GuildBanCreateWithReason(m.ChannelID, m.Author.ID, "User was flagged as a spammer by security bot", 1)
if err != nil {
// likely a permission error, log it anyway (TODO logging)
}

return
}
}
120 changes: 120 additions & 0 deletions internal/bot/memory/data.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package memory

import (
"context"
"fmt"
"slices"
"strings"

Expand Down Expand Up @@ -80,6 +81,7 @@ func LoadData(db *ent.Client) (*Data, error) {
ExcludedChannels: config.ExcludedChannels,
ExcludedRoles: config.ExcludedRoles,
ExcluedUsers: config.ExcludedUsers,
RateLimitCount: config.RatelimitMessage,
RateLimitTime: config.RatelimitTime,
TimeoutTime: config.TimeoutTime,
BanMessageDeleteTime: config.BanDeleteMessageTime,
Expand Down Expand Up @@ -148,6 +150,7 @@ func (d *Data) AddServer(serverId, ownerId string) (error, error) {
ExcludedChannels: config.ExcludedChannels,
ExcludedRoles: config.ExcludedRoles,
ExcluedUsers: config.ExcludedUsers,
RateLimitCount: config.RatelimitMessage,
RateLimitTime: config.RatelimitTime,
TimeoutTime: config.TimeoutTime,
BanMessageDeleteTime: config.BanDeleteMessageTime,
Expand All @@ -165,6 +168,95 @@ func (d *Data) ServerExists(serverId string) bool {
return ok
}

func (d *Data) EnableServer(serverId string) (bool, error) {
if !d.ServerExists(serverId) {
return false, fmt.Errorf("your server doesn't exist in the bot's memory")
}

// check to see if the bot is already enabled

if d.enabled[serverId] {
return false, fmt.Errorf("bot already enabled")
}

// grab the config
cfg := d.GetConfiguration(serverId)

// this is to determine the highest possible level of sus that can
// be detected based on enabled checks
highestPossibleSusLevel := 0

hasBlacklist := len(d.blacklist[serverId]) > 0

if hasBlacklist {
highestPossibleSusLevel++
}

if cfg.CheckLinks {
highestPossibleSusLevel++
}

if cfg.CheckInvites {
highestPossibleSusLevel++

// if there's a blacklist, there's a possibility of the invite server name being in there.
if hasBlacklist {
highestPossibleSusLevel++
}
}

if cfg.EnforceRatelimit {
highestPossibleSusLevel++
}

if cfg.FlagLinks {
highestPossibleSusLevel++
}

// We need at 3, if not then the bot can't reliably flag spammers

if highestPossibleSusLevel < 3 {
return false, fmt.Errorf("security configured too low, high chance of false flagging or missing actual spammers")
}

return d.changeServerStatus(serverId, true)
}

func (d *Data) DisableServer(serverId string) (bool, error) {
if !d.ServerExists(serverId) {
return false, fmt.Errorf("your server doesn't exist in the bot's memory")
}

// check to see if the bot is already disabled
if !d.enabled[serverId] {
return false, fmt.Errorf("bot already disabled")
}

return d.changeServerStatus(serverId, false)
}

func (d *Data) changeServerStatus(serverId string, status bool) (bool, error) {
srv, err := d.db.Server.Query().Where(server.ServerID(serverId)).First(context.Background())
if err != nil {
if ent.IsNotFound(err) {
return false, fmt.Errorf("server missing from database")
}
// TODO log the error to the error channel/probably sentry but don't send the exposed error
return false, fmt.Errorf("unexpected server error occurred while fetching server")
}

_, err = srv.Update().SetEnabled(status).Save(context.TODO())

if err != nil {
return false, fmt.Errorf("unexpected server error occurred while saving server enabled status")
}

// update memory db
d.enabled[serverId] = status

return true, nil
}

func (d *Data) UpdateConfiguration(serverId string, config models.ServerConfig) {}

func (d *Data) GetConfiguration(serverId string) models.ServerConfig {
Expand Down Expand Up @@ -195,6 +287,34 @@ func (d *Data) Enabled(serverId string) bool {
return d.enabled[serverId]
}

func (d *Data) ShouldIgnore(serverId, channelId, userId string, roleIds []string) bool {
cfg := d.configuration[serverId]

// Check if the message is in a channel or is a user for a server that we should ignore
if slices.Contains(cfg.ExcludedChannels, channelId) || slices.Contains(cfg.ExcluedUsers, userId) {
return true
}

// check for er
return slices.ContainsFunc(cfg.ExcludedRoles, func(roleId string) bool {
return slices.Contains(roleIds, roleId)
})

}

func (d *Data) MessageSeenBefore(serverId, userId, hash string) (bool, int) {
cooldowns := d.cooldown[serverId]
index := slices.IndexFunc(cooldowns, func(c models.Cooldown) bool {
return c.HashId == hash && c.UserId == userId
})

if index == -1 {
return false, 0
}

return true, cooldowns[index].Count
}

// Cleans up unused data (such as unused words) TODO
func (d *Data) cleanup() {
}
2 changes: 1 addition & 1 deletion internal/database/schema/server_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ func (ServerConfig) Fields() []ent.Field {

field.Int("ratelimit_message").Default(3).Comment("The amount of times the same message can be sent within the rate limit period before being considered spam"),

field.Enum("ratelimit_time").Values().Values("30s", "1m", "2m", "3m", "4m", "5m").Default("5m").Comment("The ratelimit cooldown time, message tracking will be reset after this time period"),
field.Enum("ratelimit_time").Values().Values("30s", "1m", "2m", "3m", "4m", "5m").Default("2m").Comment("The ratelimit cooldown time, message tracking will be reset after this time period"),
field.Enum("timeout_time").Values().Values("60s", "5m", "10m", "1h", "1d", "1w").Default("1h").Comment("The discord timeout time assigned to a spammer"),
field.Enum("ban_delete_message_time").Values("1h", "6h", "12h", "1d", "3d", "1w").Default("1h").Comment("The discord time to remove messages sent by a spammer"),
}
Expand Down
12 changes: 8 additions & 4 deletions internal/models/data.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,15 +26,19 @@ type ServerConfig struct {
ExcludedRoles []string
ExcluedUsers []string

// Ratelimit limit
RateLimitCount int

/** pre defined values */
RateLimitTime serverconfig.RatelimitTime
TimeoutTime serverconfig.TimeoutTime
BanMessageDeleteTime serverconfig.BanDeleteMessageTime
}

type Cooldown struct {
UserId string
HashId string
Count int
ResetAt time.Time
UserId string
HashId string
MessageIds []string
Count int
ResetAt time.Time
}

0 comments on commit 8d504c4

Please sign in to comment.