diff --git a/src/bot.ts b/src/bot.ts index 049004f..b320a66 100644 --- a/src/bot.ts +++ b/src/bot.ts @@ -35,7 +35,7 @@ import { import { registerReactionSpamHandler } from "./handlers/reactionSpam"; import { registerRestrictionHandlers } from "./handlers/restrictions"; import { registerRoleHandlers } from "./handlers/roles"; -import { registerSpamPatternHandlers } from "./handlers/spamPatterns"; +import { registerSpamReactHandlers } from "./handlers/spamReacts"; import { registerViolationHandlers } from "./handlers/violations"; import { messageFilterMiddleware } from "./middleware/messageFilter"; import { ChatIndexerService } from "./services/chatIndexerService"; @@ -169,7 +169,7 @@ async function main() { registerGamblingCommands(bot); // Roll gambling game registerDuelCommands(bot); // Duel 2-player game registerCallbackHandlers(bot); // Inline keyboard callback handlers - registerSpamPatternHandlers(bot); // Spam profile pattern management + registerSpamReactHandlers(bot); // Spam reaction pattern management registerReactionSpamHandler(bot); // Reaction-based spam detection // Session text handler for multi-step interactive flows diff --git a/src/commands/help.ts b/src/commands/help.ts index 517c307..082fe90 100644 --- a/src/commands/help.ts +++ b/src/commands/help.ts @@ -128,7 +128,7 @@ export function registerHelpCommand(bot: Telegraf): void { // Handle help category callbacks - specific categories only, exclude 'menu' and 'games' bot.action( - /^help_(wallet|shared|user|giveaways|payments|elevated|admin|owner|spampatterns)$/, + /^help_(wallet|shared|user|giveaways|payments|elevated|admin|owner)$/, async (ctx) => { const category = ctx.match[1]; const userId = ctx.from?.id; @@ -248,10 +248,7 @@ function buildHelpMenu(role: string): InlineKeyboardMarkup { } if (role === "owner") { - buttons.push([ - { text: "Spam Patterns", callback_data: "help_spampatterns" }, - { text: "Owner", callback_data: "help_owner" }, - ]); + buttons.push([{ text: "Owner", callback_data: "help_owner" }]); } return { inline_keyboard: buttons }; @@ -506,38 +503,6 @@ const helpContent: Record = { " Contribute JUNO from your balance to the game treasury. Helps fund game payouts like /roll wins.", ]), - spampatterns: fmt([ - bold("Spam Pattern Management"), - "\n\n", - "Manage patterns matched against profiles of users who react to messages. Low-message users matching a pattern are permanently banned.\n\n", - bold("Profile Fields:"), - "\n", - bold("bio"), - " - The user's biography text\n", - bold("channel"), - " - The user's linked personal channel title\n", - bold("both"), - " (default) - Match against both fields\n\n", - "/addspampattern [pattern] [bio|channel|both]\n", - " Add a spam profile pattern. No args for interactive mode.\n\n", - "/removespampattern \n", - " Remove a pattern by its ID.\n\n", - "/listspampatterns\n", - " View all active custom patterns.\n\n", - "/testspampattern \n", - " Test a pattern against sample text without saving.\n\n", - "/spampatternhelp\n", - " Detailed guide with examples.\n\n", - bold("Examples:"), - "\n", - code('/addspampattern "bonus" channel'), - '\n Bans users with "BONUS" in their channel title\n\n', - code("/addspampattern /elon\\s*musk/i channel"), - '\n Bans users with "Elon Musk" scam channels\n\n', - code('/addspampattern "18+" bio'), - '\n Bans users with "18+" in their bio', - ]), - owner: fmt([ bold("Owner Commands"), "\n\n", @@ -594,7 +559,20 @@ const helpContent: Record = { bold("Moderation:"), "\n", "/clearviolations \n", - " Clear all violations for a user.", + " Clear all violations for a user.\n\n", + bold("Spam Reaction Patterns:"), + "\n", + "Manage patterns matched against profiles of users who react to messages. Matching users are permanently banned.\n\n", + "/addspamreact [pattern] [bio|channel|both]\n", + " Add a spam reaction pattern. No args for interactive mode.\n\n", + "/removespamreact \n", + " Remove a pattern by its ID.\n\n", + "/listspamreacts\n", + " View all active custom patterns.\n\n", + "/testspamreact \n", + " Test a pattern against sample text without saving.\n\n", + "/spamreacthelp\n", + " Detailed guide with examples.", ]), }; @@ -609,7 +587,6 @@ const categoryRoleRequirements: Record = { payments: ["pleb", "elevated", "admin", "owner"], elevated: ["elevated", "admin", "owner"], admin: ["admin", "owner"], - spampatterns: ["owner"], owner: ["owner"], }; diff --git a/src/handlers/callbacks.ts b/src/handlers/callbacks.ts index 7e4f2db..5586858 100644 --- a/src/handlers/callbacks.ts +++ b/src/handlers/callbacks.ts @@ -35,7 +35,7 @@ import { import { AmountPrecision } from "../utils/precision"; import { checkIsElevated, isImmuneToModeration } from "../utils/roles"; import { formatUserIdDisplay, resolveUserId } from "../utils/userResolver"; -import { addPattern, type SpamPatternField } from "./spamPatterns"; +import { addPattern, type SpamReactField } from "./spamReacts"; interface Giveaway { id: number; @@ -294,6 +294,68 @@ ${bold("Select auto-jail settings:")} ); } +/** + * Applies a restriction to a user and sends confirmation. + * Shared by the auto-jail callback (non-regex) and the regex pattern text handler. + */ +async function applyRestriction( + ctx: Context, + adminId: number, + targetId: number, + restrictionType: string, + action: string | undefined, + severity: "delete" | "mute" | "jail", + autoJailSetting: string, + threshold: number, + jailDuration: number, + jailFine: number, +): Promise { + addUserRestriction( + targetId, + restrictionType, + action, + undefined, + undefined, + severity, + threshold, + jailDuration, + jailFine, + ); + + StructuredLogger.logSecurityEvent("Restriction added via interactive flow", { + adminId, + userId: targetId, + operation: "add_restriction", + restriction: restrictionType, + action, + severity, + autoJailSetting, + threshold, + jailDuration, + jailFine, + }); + + const targetDisplay = formatUserIdDisplay(targetId); + const autoJailText = + autoJailSetting === "disabled" + ? "Auto-jail: Disabled" + : `Auto-jail: After ${threshold} violations (${Math.round(jailDuration / 1440)} day(s), ${jailFine.toFixed(1)} JUNO fine)`; + + await ctx.reply( + fmt`${bold("Restriction Applied")} + +Type: ${restrictionType} +Target: ${targetDisplay} +${action ? fmt`Pattern: ${code(action)}` : ""} +Severity: ${severity} +${autoJailText} + +Use ${code(`/listrestrictions ${targetId}`)} to view all restrictions.`, + ); + + clearSession(adminId); +} + /** * Handles auto-jail settings selection and applies the restriction. * This is the final step (step 3) of the interactive restriction creation flow. @@ -362,49 +424,41 @@ async function handleAutoJailCallback( break; } - // Apply the restriction - addUserRestriction( + // For regex_block, we need to ask for the pattern before applying + if (restrictionType === "regex_block") { + session.data.threshold = threshold; + session.data.jailDuration = jailDuration; + session.data.jailFine = jailFine; + session.data.autoJailSetting = autoJailSetting; + setSession(userId, "add_restriction", 4, session.data); + + await ctx.editMessageText( + fmt`${bold("Regex Block Pattern")} + +Reply with the pattern to block. Examples: +${code('"fa99ot"')} - simple text match +${code('"spam*here"')} - wildcard match +${code("/\\b(word1|word2)\\b/i")} - regex pattern + +Multiple words can be combined with | in regex: +${code("/\\b(fa99ot|fa990t)\\b/i")}`, + ); + return; + } + + // Apply the restriction (non-regex types) + applyRestriction( + ctx, + userId, targetId, restrictionType, undefined, - undefined, - undefined, - severity, - threshold, - jailDuration, - jailFine, - ); - - StructuredLogger.logSecurityEvent("Restriction added via interactive flow", { - adminId: userId, - userId: targetId, - operation: "add_restriction", - restriction: restrictionType, severity, autoJailSetting, threshold, jailDuration, jailFine, - }); - - const targetDisplay = formatUserIdDisplay(targetId); - const autoJailText = - autoJailSetting === "disabled" - ? "Auto-jail: Disabled" - : `Auto-jail: After ${threshold} violations (${Math.round(jailDuration / 1440)} day(s), ${jailFine.toFixed(1)} JUNO fine)`; - - await ctx.editMessageText( - fmt`${bold("Restriction Applied")} - -Type: ${restrictionType} -Target: ${targetDisplay} -Severity: ${severity} -${autoJailText} - -Use ${code(`/listrestrictions ${targetId}`)} to view all restrictions.`, ); - - clearSession(userId); } /** @@ -1180,8 +1234,8 @@ export async function handleSessionText(ctx: Context): Promise { case "list_remove_white": case "list_remove_black": return await processListSession(ctx, session, text); - case "add_spam_pattern": - return await processAddSpamPatternSession(ctx, session, text); + case "add_spam_react": + return await processAddSpamReactSession(ctx, session, text); default: return false; } @@ -1228,6 +1282,44 @@ async function processAddRestrictionSession( const { restrictionType } = session.data; + // Step 4: Capture regex pattern for regex_block type + if (session.step === 4 && restrictionType === "regex_block") { + const pattern = text.trim(); + if (!pattern) { + await ctx.reply("Please provide a non-empty pattern."); + return true; + } + + const { + targetId, + severity, + threshold, + jailDuration, + jailFine, + autoJailSetting, + } = session.data; + + // Strip surrounding quotes if present + let action = pattern; + if (action.startsWith('"') && action.endsWith('"')) { + action = action.slice(1, -1); + } + + await applyRestriction( + ctx, + userId, + targetId, + restrictionType, + action, + severity, + autoJailSetting, + threshold, + jailDuration, + jailFine, + ); + return true; + } + // Step 1: Resolve the target user from text input const targetId = resolveUserId(text.trim()); if (!targetId) { @@ -1598,7 +1690,7 @@ async function processListSession( } /** - * Handles spam pattern field selection from the inline keyboard. + * Handles spam react field selection from the inline keyboard. * Stores the selected field in session and prompts for the pattern text. * * @param ctx - Telegraf callback query context @@ -1617,14 +1709,14 @@ async function handleSpamFieldCallback( return; } - const field = data.replace("spamfield_", "") as SpamPatternField; + const field = data.replace("spamfield_", "") as SpamReactField; const fieldLabel = field === "bio" ? "Bio" : field === "channel" ? "Channel Title" : "Both"; - setSession(userId, "add_spam_pattern", 1, { field }); + setSession(userId, "add_spam_react", 1, { field }); await ctx.editMessageText( - fmt`${bold(`Add Spam Pattern [${fieldLabel}]`)} + fmt`${bold(`Add Spam Reaction Pattern [${fieldLabel}]`)} Reply with the pattern to match against user profiles. @@ -1641,7 +1733,7 @@ ${code("*crypto*giveaway*")} - matches "Free Crypto Giveaway"`, } /** - * Processes text input for the add_spam_pattern interactive session. + * Processes text input for the add_spam_react interactive session. * Called when a user replies with a pattern after selecting a field. * * @param ctx - Telegraf context @@ -1649,7 +1741,7 @@ ${code("*crypto*giveaway*")} - matches "Free Crypto Giveaway"`, * @param text - User's text input (the pattern) * @returns True if handled */ -async function processAddSpamPatternSession( +async function processAddSpamReactSession( ctx: Context, session: SessionData, text: string, @@ -1663,7 +1755,7 @@ async function processAddSpamPatternSession( return true; } - const field = session.data.field as SpamPatternField; + const field = session.data.field as SpamReactField; const pattern = text.trim(); clearSession(userId); diff --git a/src/handlers/reactionSpam.ts b/src/handlers/reactionSpam.ts index 2a7db2e..4e22971 100644 --- a/src/handlers/reactionSpam.ts +++ b/src/handlers/reactionSpam.ts @@ -13,7 +13,7 @@ import { execute, get } from "../database"; import { scheduleDelete } from "../utils/autoDelete"; import { logger, StructuredLogger } from "../utils/logger"; import { isAdmin, isOwner } from "../utils/roles"; -import { getDbSpamPatterns } from "./spamPatterns"; +import { getDbSpamReacts } from "./spamReacts"; /** Minimum messages a user must have sent before being exempt from spam checks */ const MIN_MESSAGES_FOR_EXEMPTION = 10; @@ -115,7 +115,7 @@ function detectSpamProfile( } // Check DB-managed patterns with field targeting - const dbPatterns = getDbSpamPatterns(); + const dbPatterns = getDbSpamReacts(); for (const dbPattern of dbPatterns) { const fieldsToCheck: [string, string | undefined][] = []; if (dbPattern.matchField === "bio" || dbPattern.matchField === "both") { diff --git a/src/handlers/spamPatterns.ts b/src/handlers/spamReacts.ts similarity index 69% rename from src/handlers/spamPatterns.ts rename to src/handlers/spamReacts.ts index 319176e..066c4fd 100644 --- a/src/handlers/spamPatterns.ts +++ b/src/handlers/spamReacts.ts @@ -1,20 +1,20 @@ /** - * Spam profile pattern management handlers for the CAC Admin Bot. - * Provides commands for adding, removing, listing, and testing spam patterns + * Spam reaction pattern management handlers for the CAC Admin Bot. + * Provides commands for adding, removing, listing, and testing patterns * that are matched against user bios and personal channel titles during * reaction-based spam detection. * * Patterns are stored in the database and loaded with caching to avoid * repeated DB queries on every reaction event. * - * @module handlers/spamPatterns + * @module handlers/spamReacts */ import type { Context, Telegraf } from "telegraf"; import { bold, code, fmt } from "telegraf/format"; import { execute, get, query } from "../database"; import { adminOrHigher, ownerOnly } from "../middleware"; -import { spamPatternFieldKeyboard } from "../utils/keyboards"; +import { spamReactFieldKeyboard } from "../utils/keyboards"; import { logger, StructuredLogger } from "../utils/logger"; import { type CompiledPattern, @@ -22,32 +22,32 @@ import { validatePattern, } from "../utils/safeRegex"; -/** Valid match field values for spam patterns */ -export type SpamPatternField = "bio" | "channel" | "both"; +/** Valid match field values for spam react patterns */ +export type SpamReactField = "bio" | "channel" | "both"; -/** Database row shape for a spam pattern */ -export interface SpamPatternRow { +/** Database row shape for a spam react pattern */ +export interface SpamReactRow { id: number; pattern: string; - match_field: SpamPatternField; + match_field: SpamReactField; description: string | null; added_by: number | null; created_at: number; } -/** Compiled spam pattern with field targeting */ -export interface CompiledSpamPattern { +/** Compiled spam react pattern with field targeting */ +export interface CompiledSpamReact { id: number; raw: string; compiled: CompiledPattern; - matchField: SpamPatternField; + matchField: SpamReactField; description: string | null; } // --- Pattern Cache --- /** Cached compiled patterns */ -let cachedPatterns: CompiledSpamPattern[] | null = null; +let cachedPatterns: CompiledSpamReact[] | null = null; /** Timestamp of last cache refresh */ let cacheTimestamp = 0; @@ -56,32 +56,32 @@ let cacheTimestamp = 0; const CACHE_TTL_MS = 60_000; /** - * Invalidates the spam pattern cache. + * Invalidates the spam react pattern cache. * Call after adding or removing patterns to ensure immediate effect. */ -export function invalidateSpamPatternCache(): void { +export function invalidateSpamReactCache(): void { cachedPatterns = null; cacheTimestamp = 0; } /** - * Returns compiled spam patterns from the database, with caching. + * Returns compiled spam react patterns from the database, with caching. * Patterns are refreshed every 60 seconds or when the cache is explicitly invalidated. * - * @returns Array of compiled spam patterns + * @returns Array of compiled spam react patterns */ -export function getDbSpamPatterns(): CompiledSpamPattern[] { +export function getDbSpamReacts(): CompiledSpamReact[] { const now = Date.now(); if (cachedPatterns && now - cacheTimestamp < CACHE_TTL_MS) { return cachedPatterns; } - const rows = query( + const rows = query( "SELECT * FROM spam_patterns ORDER BY id", [], ); - const compiled: CompiledSpamPattern[] = []; + const compiled: CompiledSpamReact[] = []; for (const row of rows) { try { const pattern = compileSafeRegex(row.pattern); @@ -93,7 +93,7 @@ export function getDbSpamPatterns(): CompiledSpamPattern[] { description: row.description, }); } catch (error) { - logger.error("Failed to compile DB spam pattern", { + logger.error("Failed to compile DB spam react pattern", { patternId: row.id, pattern: row.pattern, error, @@ -107,26 +107,26 @@ export function getDbSpamPatterns(): CompiledSpamPattern[] { } /** - * Registers all spam pattern management commands with the bot. + * Registers all spam reaction pattern management commands with the bot. * * Commands: - * - /addspampattern - Add a spam profile pattern (owner only) - * - /removespampattern - Remove a pattern by ID (owner only) - * - /listspampatterns - List all active patterns (admin or higher) - * - /spampatternhelp - Detailed usage guide with examples + * - /addspamreact - Add a spam reaction pattern (owner only) + * - /removespamreact - Remove a pattern by ID (owner only) + * - /listspamreacts - List all active patterns (admin or higher) + * - /spamreacthelp - Detailed usage guide with examples * * @param bot - Telegraf bot instance */ -export function registerSpamPatternHandlers(bot: Telegraf): void { +export function registerSpamReactHandlers(bot: Telegraf): void { /** - * Command: /addspampattern [pattern] [bio|channel|both] + * Command: /addspamreact [pattern] [bio|channel|both] * * Interactive mode (no args): Shows field selection keyboard. * Command mode: Adds pattern directly. * * Permission: Owner only */ - bot.command("addspampattern", ownerOnly, async (ctx) => { + bot.command("addspamreact", ownerOnly, async (ctx) => { const userId = ctx.from?.id; if (!userId) return; @@ -135,7 +135,7 @@ export function registerSpamPatternHandlers(bot: Telegraf): void { // No args -> interactive keyboard flow if (!rawArgs.trim()) { return ctx.reply( - fmt`${bold("Add Spam Profile Pattern")} + fmt`${bold("Add Spam Reaction Pattern")} Select which profile field this pattern should match against: @@ -146,24 +146,24 @@ ${bold("Both")} - Match against both fields After selecting, you'll be prompted to enter the pattern. Or use the command directly: -${code('/addspampattern "pattern" [bio|channel|both]')} +${code('/addspamreact "pattern" [bio|channel|both]')} -For detailed help: ${code("/spampatternhelp")}`, +For detailed help: ${code("/spamreacthelp")}`, { - reply_markup: spamPatternFieldKeyboard, + reply_markup: spamReactFieldKeyboard, }, ); } // Parse args: pattern (possibly quoted) followed by optional field - const { pattern, field, description } = parseSpamPatternArgs(rawArgs); + const { pattern, field, description } = parseSpamReactArgs(rawArgs); if (!pattern) { return ctx.reply( fmt`Invalid syntax. Usage: -${code('/addspampattern "pattern" [bio|channel|both]')} -${code("/addspampattern /regex/i [bio|channel|both]")} +${code('/addspamreact "pattern" [bio|channel|both]')} +${code("/addspamreact /regex/i [bio|channel|both]")} -For help: ${code("/spampatternhelp")}`, +For help: ${code("/spamreacthelp")}`, ); } @@ -171,12 +171,12 @@ For help: ${code("/spampatternhelp")}`, }); /** - * Command: /removespampattern - * Removes a spam pattern by its ID. + * Command: /removespamreact + * Removes a spam reaction pattern by its ID. * * Permission: Owner only */ - bot.command("removespampattern", ownerOnly, async (ctx) => { + bot.command("removespamreact", ownerOnly, async (ctx) => { const userId = ctx.from?.id; if (!userId) return; @@ -185,9 +185,9 @@ For help: ${code("/spampatternhelp")}`, if (!idStr) { return ctx.reply( - fmt`Usage: ${code("/removespampattern ")} + fmt`Usage: ${code("/removespamreact ")} -Use ${code("/listspampatterns")} to see pattern IDs.`, +Use ${code("/listspamreacts")} to see pattern IDs.`, ); } @@ -196,47 +196,47 @@ Use ${code("/listspampatterns")} to see pattern IDs.`, return ctx.reply("Invalid pattern ID. Must be a number."); } - const existing = get( + const existing = get( "SELECT * FROM spam_patterns WHERE id = ?", [patternId], ); if (!existing) { - return ctx.reply(`No spam pattern found with ID ${patternId}.`); + return ctx.reply(`No spam react pattern found with ID ${patternId}.`); } execute("DELETE FROM spam_patterns WHERE id = ?", [patternId]); - invalidateSpamPatternCache(); + invalidateSpamReactCache(); - StructuredLogger.logSecurityEvent("Spam pattern removed", { + StructuredLogger.logSecurityEvent("Spam react pattern removed", { userId, - operation: "remove_spam_pattern", + operation: "remove_spam_react", patternId, pattern: existing.pattern, matchField: existing.match_field, }); await ctx.reply( - fmt`Spam pattern #${patternId} removed. + fmt`Spam react pattern #${patternId} removed. Pattern: ${code(existing.pattern)} Field: ${existing.match_field}`, ); }); /** - * Command: /listspampatterns - * Lists all active spam profile patterns. + * Command: /listspamreacts + * Lists all active spam reaction patterns. * * Permission: Admin or higher */ - bot.command("listspampatterns", adminOrHigher, async (ctx) => { - const rows = query( + bot.command("listspamreacts", adminOrHigher, async (ctx) => { + const rows = query( "SELECT * FROM spam_patterns ORDER BY id", [], ); if (rows.length === 0) { return ctx.reply( - "No custom spam patterns configured. Only built-in patterns are active.", + "No custom spam react patterns configured. Only built-in patterns are active.", ); } @@ -247,28 +247,28 @@ Field: ${existing.match_field}`, }); await ctx.reply( - fmt`${bold("Spam Profile Patterns")} + fmt`${bold("Spam Reaction Patterns")} ${lines.join("\n")} ${bold("Built-in patterns")} (always active): 18+, secret place, onlyfans, adult content, private video, hot content, free nudes, dating site, sexy content, bonus scam, elon musk -Use ${code("/removespampattern ")} to remove a pattern.`, +Use ${code("/removespamreact ")} to remove a pattern.`, ); }); /** - * Command: /testspampattern + * Command: /testspamreact * Tests a pattern against sample text without saving it. * * Permission: Owner only */ - bot.command("testspampattern", ownerOnly, async (ctx) => { + bot.command("testspamreact", ownerOnly, async (ctx) => { const rawArgs = ctx.message?.text.split(" ").slice(1).join(" ") || ""; if (!rawArgs.trim()) { return ctx.reply( - fmt`Usage: ${code('/testspampattern "pattern" sample text here')} + fmt`Usage: ${code('/testspamreact "pattern" sample text here')} Tests whether a pattern would match the given sample text.`, ); @@ -277,8 +277,8 @@ Tests whether a pattern would match the given sample text.`, const { pattern, remainder } = parsePatternAndRemainder(rawArgs); if (!pattern || !remainder) { return ctx.reply( - fmt`Usage: ${code('/testspampattern "pattern" sample text here')} -${code("/testspampattern /regex/i sample text here")}`, + fmt`Usage: ${code('/testspamreact "pattern" sample text here')} +${code("/testspamreact /regex/i sample text here")}`, ); } @@ -305,16 +305,16 @@ Result: ${matches ? "MATCH" : "no match"}`, }); /** - * Command: /spampatternhelp - * Displays detailed usage guide for spam pattern management. + * Command: /spamreacthelp + * Displays detailed usage guide for spam reaction pattern management. * * Permission: Admin or higher */ - bot.command("spampatternhelp", adminOrHigher, async (ctx) => { + bot.command("spamreacthelp", adminOrHigher, async (ctx) => { await ctx.reply( - fmt`${bold("Spam Profile Pattern Guide")} + fmt`${bold("Spam Reaction Pattern Guide")} -Spam patterns are matched against the profiles of users who react to messages. If a low-message user's profile matches, they are permanently banned. +Spam reaction patterns are matched against the profiles of users who react to messages. If a low-message user's profile matches, they are permanently banned. ${bold("Profile Fields:")} @@ -329,48 +329,48 @@ ${bold("both")} (default) - Matches against both fields ${bold("Pattern Types:")} ${bold("Simple text")} (case-insensitive substring match): -${code('/addspampattern "bonus" channel')} +${code('/addspamreact "bonus" channel')} Matches: "BONUS 1000$", "Get your bonus now" ${bold("Wildcards")} (* = any chars, ? = single char): -${code('/addspampattern "free*nudes" bio')} +${code('/addspamreact "free*nudes" bio')} Matches: "free nudes", "free hot nudes" ${bold("Regex")} (/pattern/flags format): -${code("/addspampattern /bonus\\s*\\d+\\s*\\$/i channel")} +${code("/addspamreact /bonus\\s*\\d+\\s*\\$/i channel")} Matches: "BONUS 1000$", "bonus 500 $" ${bold("Examples:")} Block "crypto giveaway" scam channels: -${code('/addspampattern "crypto giveaway" channel')} +${code('/addspamreact "crypto giveaway" channel')} Block channels with dollar amounts: -${code("/addspampattern /\\d+\\s*\\$/i channel")} +${code("/addspamreact /\\d+\\s*\\$/i channel")} Block bio spam with adult content: -${code("/addspampattern /cam\\s*girl/i bio")} +${code("/addspamreact /cam\\s*girl/i bio")} Block "Elon Musk" scam channels: -${code('/addspampattern "elon musk" channel')} +${code('/addspamreact "elon musk" channel')} ${bold("Testing:")} -${code('/testspampattern "bonus" BONUS 1000$')} +${code('/testspamreact "bonus" BONUS 1000$')} Tests a pattern against sample text without saving. ${bold("Management:")} -${code("/listspampatterns")} - View all patterns -${code("/removespampattern ")} - Remove by ID`, +${code("/listspamreacts")} - View all patterns +${code("/removespamreact ")} - Remove by ID`, ); }); - logger.info("Spam pattern management handlers registered"); + logger.info("Spam reaction pattern handlers registered"); } // --- Internal helpers --- /** - * Adds a spam pattern to the database after validation. + * Adds a spam reaction pattern to the database after validation. * * @param ctx - Telegraf context for replying * @param userId - ID of the user adding the pattern @@ -382,7 +382,7 @@ export async function addPattern( ctx: Context, userId: number, pattern: string, - field: SpamPatternField, + field: SpamReactField, description?: string, ): Promise { // Validate the pattern @@ -405,7 +405,7 @@ export async function addPattern( } // Check for duplicate - const existing = get( + const existing = get( "SELECT * FROM spam_patterns WHERE pattern = ?", [sanitized], ); @@ -421,16 +421,16 @@ export async function addPattern( "INSERT INTO spam_patterns (pattern, match_field, description, added_by) VALUES (?, ?, ?, ?)", [sanitized, field, description || null, userId], ); - invalidateSpamPatternCache(); + invalidateSpamReactCache(); - const inserted = get( + const inserted = get( "SELECT * FROM spam_patterns WHERE pattern = ?", [sanitized], ); - StructuredLogger.logSecurityEvent("Spam pattern added", { + StructuredLogger.logSecurityEvent("Spam react pattern added", { userId, - operation: "add_spam_pattern", + operation: "add_spam_react", patternId: inserted?.id, pattern: sanitized, matchField: field, @@ -438,7 +438,7 @@ export async function addPattern( }); await ctx.reply( - fmt`Spam pattern added (#${inserted?.id || "?"}). + fmt`Spam react pattern added (#${inserted?.id || "?"}). Pattern: ${code(sanitized)} Field: ${field}${description ? `\nDescription: ${description}` : ""} @@ -447,20 +447,20 @@ Bots matching this in their profile will be auto-banned on first reaction.`, } /** - * Parses command arguments for /addspampattern. + * Parses command arguments for /addspamreact. * Supports quoted patterns and optional field/description. * * @param rawArgs - Raw argument string after the command * @returns Parsed pattern, field, and description */ -function parseSpamPatternArgs(rawArgs: string): { +function parseSpamReactArgs(rawArgs: string): { pattern: string | null; - field: SpamPatternField; + field: SpamReactField; description?: string; } { let pattern: string | null = null; let remaining = rawArgs.trim(); - let field: SpamPatternField = "both"; + let field: SpamReactField = "both"; // Handle quoted pattern if (remaining.startsWith('"')) { @@ -497,7 +497,7 @@ function parseSpamPatternArgs(rawArgs: string): { // Parse optional field const parts = remaining.split(/\s+/); if (parts[0] && ["bio", "channel", "both"].includes(parts[0])) { - field = parts[0] as SpamPatternField; + field = parts[0] as SpamReactField; remaining = parts.slice(1).join(" ").trim(); } @@ -509,7 +509,7 @@ function parseSpamPatternArgs(rawArgs: string): { } /** - * Parses a pattern and remaining text from /testspampattern args. + * Parses a pattern and remaining text from /testspamreact args. * * @param rawArgs - Raw argument string * @returns Pattern and remainder text diff --git a/src/utils/keyboards.ts b/src/utils/keyboards.ts index 61278c8..284fe96 100644 --- a/src/utils/keyboards.ts +++ b/src/utils/keyboards.ts @@ -242,10 +242,10 @@ export const mainMenuKeyboard: InlineKeyboardMarkup = { }; /** - * Spam pattern field selection keyboard. - * Used in the interactive /addspampattern flow. + * Spam react field selection keyboard. + * Used in the interactive /addspamreact flow. */ -export const spamPatternFieldKeyboard: InlineKeyboardMarkup = { +export const spamReactFieldKeyboard: InlineKeyboardMarkup = { inline_keyboard: [ [ { text: "Bio", callback_data: "spamfield_bio" },