dkforest

A forum and chat platform (onion)
git clone https://git.dasho.dev/n0tr1v/dkforest.git
Log | Files | Refs | LICENSE

spamInterceptor.go (6791B)


      1 package interceptors
      2 
      3 import (
      4 	"dkforest/pkg/config"
      5 	"dkforest/pkg/database"
      6 	dutils "dkforest/pkg/database/utils"
      7 	"dkforest/pkg/utils"
      8 	"dkforest/pkg/web/handlers/interceptors/command"
      9 	"errors"
     10 	"fmt"
     11 	"github.com/sirupsen/logrus"
     12 	"regexp"
     13 	"strings"
     14 	"sync"
     15 	"time"
     16 )
     17 
     18 type SpamInterceptor struct{}
     19 
     20 type Filter struct {
     21 	ID      int64
     22 	IsRegex bool
     23 	Term    string
     24 	Rgx     *regexp.Regexp
     25 	Kick    bool
     26 	Hb      bool
     27 	NbMsg   int64
     28 }
     29 
     30 var filters []Filter
     31 var filtersMtx sync.RWMutex
     32 
     33 func LoadFilters(db *database.DkfDB) {
     34 	filtersMtx.Lock()
     35 	defer filtersMtx.Unlock()
     36 	filters = make([]Filter, 0)
     37 	dbFilters, _ := db.GetSpamFilters()
     38 	for _, dbFilter := range dbFilters {
     39 		f := Filter{IsRegex: dbFilter.IsRegex, ID: dbFilter.ID}
     40 		if dbFilter.Action == 1 {
     41 			f.Kick = true
     42 		} else if dbFilter.Action == 2 {
     43 			f.Hb = true
     44 		}
     45 		if dbFilter.IsRegex {
     46 			f.Rgx = regexp.MustCompile(dbFilter.Filter)
     47 		} else {
     48 			f.Term = dbFilter.Filter
     49 		}
     50 		f.NbMsg = dbFilter.NbMsg
     51 		filters = append(filters, f)
     52 	}
     53 }
     54 
     55 // Check the filters that we have in the database.
     56 func checkDynamicFilters(c *command.Command, lowerCaseMessage string, silentSelfKick bool) error {
     57 	filtersMtx.RLock()
     58 	defer filtersMtx.RUnlock()
     59 	for _, f := range filters {
     60 		isMatch := (f.IsRegex && f.Rgx.MatchString(c.Message)) ||
     61 			(!f.IsRegex && strings.Contains(lowerCaseMessage, f.Term))
     62 		if isMatch {
     63 			dynFilterMsg := fmt.Sprintf("dyn filter: #%d", f.ID)
     64 			nbMsgCond := f.NbMsg == 0 || c.AuthUser.GeneralMessagesCount <= f.NbMsg
     65 			if f.Hb && nbMsgCond {
     66 				dutils.SelfHellBan(c.DB, c.AuthUser, dynFilterMsg)
     67 				return ErrSilent
     68 			} else if f.Kick && nbMsgCond {
     69 				_ = dutils.SelfKick(c.DB, *c.AuthUser, silentSelfKick, dynFilterMsg)
     70 				return ErrSpamFilterTriggered
     71 			} else if nbMsgCond {
     72 				return ErrSpamFilterTriggered
     73 			}
     74 		}
     75 	}
     76 	return nil
     77 }
     78 
     79 func (i SpamInterceptor) InterceptMsg(c *command.Command) {
     80 	lowerCaseMessage := strings.ToLower(c.Message)
     81 	silentSelfKick := config.SilentSelfKick.Load()
     82 
     83 	if c.Room.IsOfficialRoom() {
     84 		if err := checkDynamicFilters(c, lowerCaseMessage, silentSelfKick); err != nil {
     85 			if !errors.Is(err, ErrSilent) {
     86 				c.Err = err
     87 			}
     88 			return
     89 		}
     90 		if err := checkSpam(c.DB, c.OrigMessage, lowerCaseMessage, c.AuthUser); err != nil {
     91 			c.Err = err
     92 			return
     93 		}
     94 	}
     95 
     96 	// Check CP links
     97 	if checkCPLinks(c.DB, c.Message) {
     98 		c.Err = errors.New("forbidden url")
     99 		return
    100 	}
    101 
    102 	if !c.AuthUser.CanUseUppercase {
    103 		c.Message = strings.ToLower(c.Message)
    104 	}
    105 }
    106 
    107 var ErrSilent = errors.New("")
    108 var ErrSpamFilterTriggered = errors.New("spam filter triggered")
    109 
    110 func checkSpam(db *database.DkfDB, origMessage, lowerCaseMessage string, authUser *database.User) error {
    111 	silentSelfKick := config.SilentSelfKick.Load()
    112 
    113 	// Auto kick upper case typing retards
    114 	if authUser.GeneralMessagesCount <= 5 {
    115 		count, total := utils.CountUppercase(origMessage)
    116 		pct := float64(count) / float64(total)
    117 		if total > 5 && pct > 0.8 {
    118 			_ = dutils.SelfKick(db, *authUser, silentSelfKick, "uppercase typing retard")
    119 			return ErrSpamFilterTriggered
    120 		}
    121 	}
    122 
    123 	if autoKickSpammers(authUser, lowerCaseMessage) {
    124 		_ = dutils.SelfKick(db, *authUser, silentSelfKick, "auto kick spammers")
    125 		return ErrSpamFilterTriggered
    126 	}
    127 
    128 	tot, wordsMap := utils.WordCount(lowerCaseMessage)
    129 	if tot >= 5 {
    130 		totalUniqueWords := len(wordsMap)
    131 		uniqueRatio := float64(totalUniqueWords) / float64(tot)
    132 		repeatedWordsCount := 0
    133 		for word, count := range wordsMap {
    134 			if len(word) >= 5 && count > 10 {
    135 				repeatedWordsCount++
    136 			}
    137 		}
    138 		retardRatio := float64(repeatedWordsCount) / float64(totalUniqueWords)
    139 		//fmt.Println(tot, totalUniqueWords, uniqueRatio, repeatedWordsCount, retardRatio, wordsMap)
    140 		if uniqueRatio < 0.2 {
    141 			logrus.Error("failed unique ratio: " + utils.TruncStr(origMessage, 75, "…"))
    142 			return errors.New("failed unique ratio")
    143 		}
    144 		if retardRatio > 0.1 {
    145 			logrus.Error("failed retard ratio: " + utils.TruncStr(origMessage, 75, "…"))
    146 			return errors.New("failed retard ratio")
    147 		}
    148 	}
    149 
    150 	if authUser.GeneralMessagesCount < 10 {
    151 		if autoKickProfanity(tot, wordsMap) {
    152 			_ = dutils.SelfKick(db, *authUser, silentSelfKick, "autoKickProfanity")
    153 			return ErrSpamFilterTriggered
    154 		}
    155 	}
    156 
    157 	if authUser.GeneralMessagesCount < 4 {
    158 		if (wordsMap["need"] > 0 && wordsMap["help"] > 0) ||
    159 			(wordsMap["help"] > 0 && wordsMap["me"] > 0) ||
    160 			(wordsMap["make"] > 0 && wordsMap["money"] > 0) ||
    161 			(wordsMap["interesting"] > 0 && (wordsMap["link"] > 0 || wordsMap["links"] > 0)) ||
    162 			wordsMap["porn"] > 0 ||
    163 			wordsMap["pedo"] > 0 ||
    164 			wordsMap["murder"] > 0 {
    165 			_ = dutils.SelfKick(db, *authUser, silentSelfKick, "wordsMap #1")
    166 			return ErrSpamFilterTriggered
    167 		}
    168 	}
    169 
    170 	if authUser.GeneralMessagesCount < 10 {
    171 		if ((wordsMap["learn"] > 0 || wordsMap["teach"] > 0) && (wordsMap["hacking"] > 0 || wordsMap["hack"] > 0)) ||
    172 			(wordsMap["cook"] > 0 && wordsMap["meth"] > 0) ||
    173 			(wordsMap["creepy"] > 0 && (wordsMap["site"] > 0 || wordsMap["sites"] > 0)) ||
    174 			(wordsMap["porn"] > 0 && (wordsMap["link"] > 0 || wordsMap["links"] > 0)) ||
    175 			(wordsMap["topic"] > 0 && wordsMap["link"] > 0) {
    176 			_ = dutils.SelfKick(db, *authUser, silentSelfKick, "wordsMap #2")
    177 			return ErrSpamFilterTriggered
    178 		}
    179 	}
    180 
    181 	if authUser.GeneralMessagesCount < 20 || time.Since(authUser.CreatedAt) < 5*time.Hour {
    182 		if wordsMap["cp"] > 0 && (wordsMap["link"] > 0 || wordsMap["links"] > 0) {
    183 			_ = dutils.SelfKick(db, *authUser, silentSelfKick, "wordsMap #3")
    184 			return ErrSpamFilterTriggered
    185 		}
    186 	}
    187 
    188 	return nil
    189 }
    190 
    191 func autoKickProfanityTmp(orig string) bool {
    192 	tot, m := utils.WordCount(strings.ToLower(orig))
    193 	return autoKickProfanity(tot, m)
    194 }
    195 
    196 func autoKickProfanity(tot int, wordsMap map[string]int) bool {
    197 	if tot > 4 && countProfanity(wordsMap) >= 4 {
    198 		return true
    199 	}
    200 	return false
    201 }
    202 
    203 func countProfanity(wordsMap map[string]int) int {
    204 	profanityWords := []string{"anus", "asshole", "cock", "dick", "nigger", "niggers", "nigga", "niggas", "sex", "rape", "porn",
    205 		"cunt", "murder", "fuck", "blood", "corpse", "hole", "slut", "bitch", "shit", "poop", "butt", "faggot",
    206 		"submissive", "slurping", "suck", "nuts", "gore", "stupid", "dumb", "jerking", "rotten", "rotted", "stinky"}
    207 	profanity := 0
    208 	for _, w := range profanityWords {
    209 		if n, ok := wordsMap[w]; ok {
    210 			profanity += n
    211 		}
    212 	}
    213 	return profanity
    214 }
    215 
    216 var spamCharsRgx = regexp.MustCompile("[^a-z0-9]+")
    217 
    218 func autoKickSpammers(authUser *database.User, lowerCaseMessage string) bool {
    219 	if authUser.GeneralMessagesCount <= 10 {
    220 		processedString := spamCharsRgx.ReplaceAllString(lowerCaseMessage, "")
    221 		return strings.Contains(processedString, "lemybeauty") ||
    222 			strings.Contains(processedString, "blacktorcc") ||
    223 			strings.Contains(processedString, "profjerry") ||
    224 			strings.Contains(processedString, "shopdarkse")
    225 	}
    226 	return false
    227 }