@@ -13,6 +13,7 @@ import { execute, get } from "../database";
1313import { scheduleDelete } from "../utils/autoDelete" ;
1414import { logger , StructuredLogger } from "../utils/logger" ;
1515import { isAdmin , isOwner } from "../utils/roles" ;
16+ import { getDbSpamPatterns } from "./spamPatterns" ;
1617
1718/** Minimum messages a user must have sent before being exempt from spam checks */
1819const MIN_MESSAGES_FOR_EXEMPTION = 10 ;
@@ -46,6 +47,8 @@ const SPAM_BIO_PATTERNS = [
4647 / m e e t \s * s i n g l e / i, // Dating spam
4748 / s e x y ? \s * ( g i r l | v i d e o | p h o t o ) / i, // Explicit content spam
4849 / \uD83D \uDD1E / , // 18+ emoji
50+ / b o n u s \s * \d + \s * \$ / i, // "BONUS 1000$" scam channel spam
51+ / e l o n \s * m u s k / i, // Elon Musk crypto scam channels
4952] ;
5053
5154/**
@@ -91,22 +94,54 @@ interface UserProfile {
9194}
9295
9396/**
94- * Checks if any profile text matches spam patterns.
97+ * Checks if any profile text matches spam patterns (built-in or DB-managed) .
9598 *
9699 * @param profile - The user's profile info (bio and/or personal chat title)
97- * @returns The matched field name , or null if no match
100+ * @returns Object with matched field and pattern info , or null if no match
98101 */
99- function detectSpamProfile ( profile : UserProfile ) : string | null {
102+ function detectSpamProfile (
103+ profile : UserProfile ,
104+ ) : { field : string ; patternSource : string } | null {
100105 const fields : [ string , string | undefined ] [ ] = [
101106 [ "bio" , profile . bio ] ,
102107 [ "personal_chat" , profile . personalChatTitle ] ,
103108 ] ;
104109
110+ // Check built-in patterns (always match both fields)
105111 for ( const [ field , value ] of fields ) {
106112 if ( value && SPAM_BIO_PATTERNS . some ( ( pattern ) => pattern . test ( value ) ) ) {
107- return field ;
113+ return { field, patternSource : "builtin" } ;
108114 }
109115 }
116+
117+ // Check DB-managed patterns with field targeting
118+ const dbPatterns = getDbSpamPatterns ( ) ;
119+ for ( const dbPattern of dbPatterns ) {
120+ const fieldsToCheck : [ string , string | undefined ] [ ] = [ ] ;
121+ if ( dbPattern . matchField === "bio" || dbPattern . matchField === "both" ) {
122+ fieldsToCheck . push ( [ "bio" , profile . bio ] ) ;
123+ }
124+ if ( dbPattern . matchField === "channel" || dbPattern . matchField === "both" ) {
125+ fieldsToCheck . push ( [ "personal_chat" , profile . personalChatTitle ] ) ;
126+ }
127+
128+ for ( const [ field , value ] of fieldsToCheck ) {
129+ if ( value ) {
130+ try {
131+ dbPattern . compiled . regex . lastIndex = 0 ;
132+ if ( dbPattern . compiled . regex . test ( value ) ) {
133+ return {
134+ field,
135+ patternSource : `db#${ dbPattern . id } :${ dbPattern . raw } ` ,
136+ } ;
137+ }
138+ } catch {
139+ // Skip broken patterns
140+ }
141+ }
142+ }
143+ }
144+
110145 return null ;
111146}
112147
@@ -391,16 +426,17 @@ export function registerReactionSpamHandler(bot: Telegraf<Context>): void {
391426 personalChat : profile . personalChatTitle ?? null ,
392427 } ) ;
393428
394- const spamField = detectSpamProfile ( profile ) ;
395- if ( spamField ) {
429+ const spamMatch = detectSpamProfile ( profile ) ;
430+ if ( spamMatch ) {
396431 const matchedValue =
397- spamField === "bio" ? profile . bio : profile . personalChatTitle ;
432+ spamMatch . field === "bio" ? profile . bio : profile . personalChatTitle ;
398433
399434 StructuredLogger . logSecurityEvent ( "Spam bot detected via profile" , {
400435 userId : user . id ,
401436 username : user . username ,
402- matchedField : spamField ,
437+ matchedField : spamMatch . field ,
403438 matchedValue : matchedValue ?. substring ( 0 , 200 ) ,
439+ patternSource : spamMatch . patternSource ,
404440 chatId : chat . id ,
405441 operation : "reaction_spam_profile" ,
406442 } ) ;
@@ -423,7 +459,8 @@ export function registerReactionSpamHandler(bot: Telegraf<Context>): void {
423459 username : user . username ,
424460 firstName : user . first_name ,
425461 chatId : chat . id ,
426- reason : `spam_${ spamField } ` ,
462+ reason : `spam_${ spamMatch . field } ` ,
463+ patternSource : spamMatch . patternSource ,
427464 matchedValue : matchedValue ?. substring ( 0 , 100 ) ,
428465 } ) ;
429466 } catch ( banError ) {
0 commit comments