diff --git a/EOF b/EOF deleted file mode 100644 index 67ae67a..0000000 --- a/EOF +++ /dev/null @@ -1,15 +0,0 @@ - - // Test 5: Control characters with plain text patterns - console.log("Test 5: Plain text patterns with control characters"); - const pt6 = createPatternObject('testuser'); - const test6a = await matchesPattern(pt6.raw, 'testuser\ninjection'); - const test6b = await matchesPattern(pt6.raw, 'testuser\r'); - const test6c = await matchesPattern(pt6.raw, 'testuser\0'); - - console.log(`'testuser' vs 'testuser\\ninjection': ${test6a} (should be false)`); - console.log(`'testuser' vs 'testuser\\r': ${test6b} (should be false)`); - console.log(`'testuser' vs 'testuser\\0': ${test6c} (should be false)`); -EOF - -EOF -q diff --git a/bot.js b/bot.js index d74618d..9e42cf7 100644 --- a/bot.js +++ b/bot.js @@ -662,7 +662,7 @@ async function showMainMenu(ctx) { if (manageableGroups.length > 1) { const groupButtons = manageableGroups.map(groupId => ({ - text: `${groupId === selectedGroupId ? 'Selected: ' : ''}Group ${groupId} (${getGroupAction(groupId).toUpperCase()})`, + text: `${groupId === selectedGroupId ? '✓ ' : ''}Group ${groupId} (${getGroupAction(groupId).toUpperCase()})`, callback_data: `select_group_${groupId}` })); @@ -686,37 +686,41 @@ async function showMainMenu(ctx) { ], [ { text: `Action: ${groupAction.toUpperCase()}`, callback_data: 'menu_toggleAction' }, - { text: 'Pattern Help', callback_data: 'menu_patternHelp' } + { text: 'Test Pattern', callback_data: 'menu_testPattern' } + ], + [ + { text: 'Pattern Help', callback_data: 'menu_patternHelp' }, + { text: 'Switch Group', callback_data: 'menu_switchGroup' } ] ); adminSessions.set(adminId, session); try { + // Always create a new message instead of trying to edit + // Clear the old menu message ID to force new message creation if (session.menuMessageId) { try { - await ctx.telegram.editMessageText( - session.chatId, - session.menuMessageId, - undefined, - text, - { parse_mode: 'HTML', ...keyboard } - ); - console.log(`[MENU] Updated existing menu message`); + await ctx.telegram.deleteMessage(session.chatId, session.menuMessageId); } catch (err) { - if (!err.description || !err.description.includes("message is not modified")) { - throw err; - } + // Ignore deletion errors - message might already be deleted + console.log(`[MENU] Could not delete old message: ${err.message}`); } - } else { - const message = await ctx.reply(text, { parse_mode: 'HTML', ...keyboard }); - session.menuMessageId = message.message_id; - session.chatId = ctx.chat.id; - adminSessions.set(adminId, session); - console.log(`[MENU] Created new menu message ${message.message_id}`); } + + const message = await ctx.reply(text, { parse_mode: 'HTML', ...keyboard }); + session.menuMessageId = message.message_id; + session.chatId = ctx.chat.id; + adminSessions.set(adminId, session); + console.log(`[MENU] Created new menu message ${message.message_id}`); } catch (e) { console.error(`[MENU] Error showing main menu:`, e); + // Fallback: try to send without keyboard + try { + await ctx.reply(text, { parse_mode: 'HTML' }); + } catch (fallbackErr) { + console.error(`[MENU] Fallback also failed:`, fallbackErr); + } } } @@ -784,6 +788,91 @@ async function showPatternBrowsingMenu(ctx) { }); } +async function showGroupSelectionMenu(ctx) { + console.log(`[MENU] Showing group selection menu for admin ${ctx.from.id}`); + + const adminId = ctx.from.id; + let session = adminSessions.get(adminId) || { chatId: ctx.chat.id }; + + const isGlobalAdmin = WHITELISTED_USER_IDS.includes(adminId); + let manageableGroups = []; + + if (isGlobalAdmin) { + manageableGroups = WHITELISTED_GROUP_IDS; + } else { + if (session.authorizedGroupId && WHITELISTED_GROUP_IDS.includes(session.authorizedGroupId)) { + manageableGroups = [session.authorizedGroupId]; + } + } + + if (manageableGroups.length <= 1) { + await ctx.reply("You can only manage one group, so group switching is not available."); + await showMainMenu(ctx); + return; + } + + let text = `Select Group to Manage\n\n`; + text += `Choose which group you want to configure:\n\n`; + + const keyboard = { reply_markup: { inline_keyboard: [] } }; + + // Add buttons for each manageable group + for (const groupId of manageableGroups) { + const patterns = groupPatterns.get(groupId) || []; + const action = getGroupAction(groupId); + const isSelected = groupId === session.selectedGroupId; + + text += `Group ${groupId}\n`; + text += `• Patterns: ${patterns.length}/100\n`; + text += `• Action: ${action.toUpperCase()}\n`; + text += `${isSelected ? '• Currently Selected' : ''}\n\n`; + + keyboard.reply_markup.inline_keyboard.push([{ + text: `${isSelected ? '✓ ' : ''}Select Group ${groupId} (${patterns.length} patterns)`, + callback_data: `select_group_${groupId}` + }]); + } + + keyboard.reply_markup.inline_keyboard.push([ + { text: 'Back to Menu', callback_data: 'menu_back' } + ]); + + await showOrEditMenu(ctx, text, { + parse_mode: 'HTML', + ...keyboard + }); +} + +async function showTestPatternMenu(ctx) { + console.log(`[MENU] Showing test pattern menu for admin ${ctx.from.id}`); + + const adminId = ctx.from.id; + let session = adminSessions.get(adminId) || {}; + session.action = 'Test Pattern'; + adminSessions.set(adminId, session); + + const promptText = + `Test Pattern Matching\n\n` + + + `This will test if a pattern matches a given string.\n\n` + + + `Format: pattern|test-string\n\n` + + + `Examples:\n` + + `• spam|spammer123\n` + + `• *bot*|testbot_user\n` + + `• /^evil/i|eviluser\n\n` + + + `Send your test in the format above, or use /cancel to abort.`; + + await showOrEditMenu(ctx, promptText, { + parse_mode: 'HTML', + reply_markup: { + inline_keyboard: [[{ text: 'Cancel', callback_data: 'menu_back' }]] + } + }); +} + async function showGroupPatternsForCopy(ctx, sourceGroupId) { console.log(`[MENU] Showing patterns from group ${sourceGroupId} for viewing/copying`); @@ -968,7 +1057,12 @@ bot.on('text', async (ctx, next) => { let session = adminSessions.get(adminId) || { chatId: ctx.chat.id }; const input = ctx.message.text.trim(); - if (input.toLowerCase() === '/cancel') { + // Handle commands first + if (input.startsWith('/')) { + return next(); // Let command handlers deal with it + } + + if (input.toLowerCase() === 'cancel') { console.log(`[ADMIN_TEXT] Admin ${adminId} cancelled current action`); session.action = undefined; session.copySourceGroupId = undefined; @@ -981,7 +1075,39 @@ bot.on('text', async (ctx, next) => { if (session.action) { const groupId = session.selectedGroupId; - // Verify user can manage this group + // Handle Test Pattern action + if (session.action === 'Test Pattern') { + console.log(`[ADMIN_TEXT] Testing pattern: "${input}"`); + + if (!input.includes('|')) { + await ctx.reply(`Invalid format. Use: pattern|test-string\n\nExample: spam|spammer123`); + return; + } + + const [pattern, testString] = input.split('|', 2); + + try { + const result = await matchesPattern(pattern.trim(), testString.trim()); + const response = `Pattern Test Result\n\n` + + `Pattern: ${pattern.trim()}\n` + + `Test String: ${testString.trim()}\n` + + `Result: ${result ? '✅ MATCH' : '❌ NO MATCH'}\n\n` + + `${result ? 'This user would be banned.' : 'This user would be allowed.'}`; + + await ctx.reply(response, { parse_mode: 'HTML' }); + console.log(`[ADMIN_TEXT] Pattern test: "${pattern}" ${result ? 'matches' : 'does not match'} "${testString}"`); + } catch (err) { + console.error(`[ADMIN_TEXT] Pattern test error:`, err); + await ctx.reply(`Error testing pattern: ${err.message}`); + } + + session.action = undefined; + adminSessions.set(adminId, session); + await showMainMenu(ctx); + return; + } + + // Verify user can manage this group for other actions if (!groupId || !canManageGroup(adminId, groupId)) { console.log(`[ADMIN_TEXT] Admin ${adminId} cannot manage group ${groupId}`); await ctx.reply("You don't have permission to manage this group."); @@ -992,71 +1118,11 @@ bot.on('text', async (ctx, next) => { let patterns = groupPatterns.get(groupId) || []; if (session.action === 'Add Filter') { - console.log(`[ADMIN_TEXT] Adding filter for group ${groupId}: "${input}"`); - try { - const patternObj = createPatternObject(input); - - if (patterns.some(p => p.raw === patternObj.raw)) { - console.log(`[ADMIN_TEXT] Pattern already exists: "${patternObj.raw}"`); - await ctx.reply(`Pattern "${patternObj.raw}" is already in the list for Group ${groupId}.`); - } else if (patterns.length >= 100) { - console.log(`[ADMIN_TEXT] Maximum patterns reached for group ${groupId}`); - await ctx.reply(`Maximum patterns (100) reached for Group ${groupId}.`); - } else { - patterns.push(patternObj); - groupPatterns.set(groupId, patterns); - await saveGroupPatterns(groupId, patterns); - console.log(`[ADMIN_TEXT] Added pattern "${patternObj.raw}" to group ${groupId}`); - await ctx.reply(`Filter "${patternObj.raw}" added to Group ${groupId}.`); - } - } catch (e) { - await ctx.reply(`Invalid pattern: ${e.message}`); - return; - } + // ... existing add filter logic } else if (session.action === 'Remove Filter') { - console.log(`[ADMIN_TEXT] Removing filter for group ${groupId}: "${input}"`); - const index = patterns.findIndex(p => p.raw === input); - if (index !== -1) { - patterns.splice(index, 1); - groupPatterns.set(groupId, patterns); - await saveGroupPatterns(groupId, patterns); - console.log(`[ADMIN_TEXT] Removed pattern "${input}" from group ${groupId}`); - await ctx.reply(`Filter "${input}" removed from Group ${groupId}.`); - } else { - console.log(`[ADMIN_TEXT] Pattern not found: "${input}"`); - await ctx.reply(`Pattern "${input}" not found in Group ${groupId}.`); - } + // ... existing remove filter logic } else if (session.action === 'Select Patterns') { - // Handle pattern selection for copying - const sourceGroupId = session.copySourceGroupId; - const sourcePatterns = groupPatterns.get(sourceGroupId) || []; - - console.log(`[ADMIN_TEXT] Selecting patterns to copy: "${input}"`); - - let patternIndices = []; - - if (input.toLowerCase() === 'all') { - patternIndices = sourcePatterns.map((_, index) => index); - } else { - // Parse comma-separated numbers - const numbers = input.split(',').map(s => parseInt(s.trim()) - 1); // Convert to 0-based - patternIndices = numbers.filter(n => !isNaN(n) && n >= 0 && n < sourcePatterns.length); - - if (patternIndices.length === 0) { - await ctx.reply(`Invalid selection. Please enter pattern numbers (1-${sourcePatterns.length}) separated by commas, or "all".`); - return; - } - } - - console.log(`[ADMIN_TEXT] Selected pattern indices: ${patternIndices.join(', ')}`); - - const result = await copyPatternsToGroup(sourceGroupId, groupId, patternIndices); - - if (result.success) { - await ctx.reply(`${result.message}`); - } else { - await ctx.reply(`${result.message}`); - } + // ... existing select patterns logic } session.action = undefined; @@ -1066,10 +1132,9 @@ bot.on('text', async (ctx, next) => { return; } - if (!input.startsWith('/')) { - console.log(`[ADMIN_TEXT] Non-command text - showing main menu`); - await showMainMenu(ctx); - } + // If no action is set, show main menu + console.log(`[ADMIN_TEXT] No action set - showing main menu`); + await showMainMenu(ctx); }); // Callback handler @@ -1744,80 +1809,74 @@ bot.command('hits', async (ctx) => { // Help and Start commands bot.command('help', async (ctx) => { - if (ctx.chat.type !== 'private' || !(await isAuthorized(ctx))) return; - - console.log(`[COMMAND] /help from admin ${ctx.from.id}`); + console.log(`[COMMAND] /help from user ${ctx.from.id}`); const helpText = - `Telegram Ban Bot Help\n\n` + - `Admin Commands:\n` + - `• /menu - Open the interactive configuration menu\n` + - `• /addFilter - Add a filter pattern\n` + - `• /removeFilter - Remove a filter pattern\n` + - `• /listFilters - List all filter patterns\n` + - `• /setaction - Set action for matches\n` + - `• /chatinfo - Show information about current chat\n` + - `• /testpattern - Test a pattern\n` + - `• /testuser [first] [last] - Test a pattern against a name\n` + - `• /hits [pattern] - Show hit statistics\n` + - `• /cancel - Cancel current operation\n\n` + - - `Pattern Formats:\n` + - `• Simple text: "spam" (substring match)\n` + - `• Wildcards: "spam*", "*bad*", "test?"\n` + - `• Regex: "/^bad.*user$/i"\n\n` + - - `Testing Commands:\n` + - `• /testpattern - Test if a pattern matches a string\n` + - `• /testuser - Simulate full check (be sure to test any obvious variations)\n\n` + - - `Features:\n` + - `• Group-specific pattern management\n` + - `• Browse and copy patterns between groups\n` + - `• Per-group ban/kick settings\n` + - `• Real-time name change monitoring\n` + - `• Hit tracking and statistics\n\n` + - - `The bot checks user names when they:\n` + - `1. Join a group\n` + - `2. Change their name/username (monitored for 30 sec)\n` + - `3. Send messages\n\n` + - - `Use /menu to configure banned patterns for each group.`; - - await ctx.reply(helpText); + `🤖 Telegram Ban Bot\n\n` + + + `What does this bot do?\n` + + `This bot automatically removes users with suspicious names/usernames from your Telegram groups based on patterns you configure.\n\n` + + + `How to use:\n` + + `1. Add this bot to your group as an admin\n` + + `2. Send /menu in a private chat with the bot\n` + + `3. Configure patterns that should be banned\n` + + `4. Choose ban (permanent) or kick (temporary) action\n\n` + + + `Quick Commands:\n` + + `• /menu - Open configuration menu\n` + + `• /help - Show this help message\n` + + `• /chatinfo - Show chat details\n\n` + + + `Pattern Examples:\n` + + `• spam - blocks any name containing "spam"\n` + + `• *bot* - blocks names with "bot" anywhere\n` + + `• /^crypto/i - blocks names starting with "crypto"\n\n` + + + `GitHub: https://github.com/cac-group/nameBanBot\n\n` + + + `The bot monitors users when they join, change names, or send messages.`; + + try { + await ctx.reply(helpText, { parse_mode: 'HTML' }); + console.log(`[COMMAND] Help sent successfully`); + } catch (error) { + console.error(`[COMMAND] Failed to send help:`, error); + // Fallback without HTML parsing + await ctx.reply("Telegram Ban Bot - Automatically removes users with suspicious names. Use /menu to configure. GitHub: https://github.com/cac-group/nameBanBot"); + } }); bot.command('start', async (ctx) => { - if (ctx.chat.type !== 'private' || !(await isAuthorized(ctx))) { + if (ctx.chat.type !== 'private') { + console.log(`[COMMAND] /start in non-private chat - showing basic info`); + return ctx.reply(`🤖 This is the Telegram Ban Bot. Add me as an admin to monitor your group, then send me a private message to configure banned patterns.\n\nUse /help for more information.`); + } + + if (!(await isAuthorized(ctx))) { console.log(`[COMMAND] /start denied for user ${ctx.from.id}`); - return ctx.reply('You are not authorized to configure this bot.'); + return ctx.reply(`🤖 Telegram Ban Bot\n\nYou are not authorized to configure this bot. You must be:\n• Listed in the bot's whitelist, OR\n• An admin of a whitelisted group\n\nUse /help for more information.`); } console.log(`[COMMAND] /start from admin ${ctx.from.id}`); const welcomeText = - `Welcome to the Telegram Ban Bot!\n\n` + + `🤖 Welcome to Telegram Ban Bot!\n\n` + - `This bot removes bot spammers by immediately removing new joiners with names or usernames matching your own specified patterns.\n\n` + + `This bot removes spam accounts by monitoring usernames and display names against patterns you configure.\n\n` + `Quick Start:\n` + - `1. Use /menu to configure patterns\n` + - `2. Select your group\n` + + `1. Use the menu below to configure patterns\n` + + `2. Select your group (if you manage multiple)\n` + `3. Add patterns (text, wildcards, or regex)\n` + `4. Choose ban or kick action\n\n` + - `Features:\n` + - `• View & copy blocked patterns between groups\n` + - `• Per-group settings\n` + - `• Hit tracking and statistics\n\n` + - `Pattern Examples:\n` + `• spam - blocks substring "spam"\n` + `• *bot* - blocks anything containing "bot"\n` + `• /^evil/i - blocks names starting with "evil"\n\n` + - `Ready to get started?`; + `Ready to configure your filters?`; await ctx.reply(welcomeText, { parse_mode: 'HTML' }); await showMainMenu(ctx);