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 }