diff --git a/.gitignore b/.gitignore index d6e1430..e65f575 100644 --- a/.gitignore +++ b/.gitignore @@ -1,38 +1,24 @@ -node_modules/ - -# Logs -logs/* -!logs/.gitkeep -*.log - # Environment files .env -*.env -**/*.env .env.* -env -env.* - -# Runtime JSON state (do not commit) -/data/*.json -/users.json -/giveaways.json -/promo.json -/env.json - -# Legacy runtime state -bot_state.json +*.env -# Generated runtime backups -data/backups/* -!data/backups/.gitkeep +# Logs +logs/ +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* -# Service log artifacts -runewager-*.log -crash.log +# Node modules +node_modules/ -# Runtime admin event log (append-only, not for version control) -data/admin-events.log +# Temp + cache +.tmp/ +.cache/ +dist/ +build/ -# Rollback marker (runtime artifact) -.last_rollback +# System files +.DS_Store +Thumbs.db diff --git a/index.js b/index.js index 8b2b7ab..b9f69e7 100644 --- a/index.js +++ b/index.js @@ -249,6 +249,96 @@ const NON_USERNAME_WORDS = new Set([ 'thank', 'please', 'go', 'home', 'main', 'more', 'info', 'test', 'bye', 'nope', ]); +// Slash commands implemented in this file. Used for unknown-command fallback. +const REGISTERED_COMMANDS = new Set([ + 'A', + 'a', + 'admin', + 'admin_backup', + 'admin_log', + 'admin_notify', + 'affiliate', + 'announce', + 'approve_group', + 'bonus', + 'bonusstatus', + 'boost_referrals', + 'boostmeter', + 'broadcast_failed', + 'broadcast_retry', + 'bugreport', + 'bugreports', + 'cancel', + 'checkin', + 'claim_history', + 'commands', + 'deploy', + 'deploy_status', + 'discord', + 'discord_confirm', + 'discord_stats', + 'eligible', + 'exportbugs', + 'fixaccount', + 'funnel', + 'giveaway', + 'gw_graphic', + 'gw_pause', + 'gw_resume', + 'gwhistory', + 'health', + 'help', + 'join', + 'language', + 'leaderboard', + 'leaderboard_weekly', + 'link', + 'linkaccount', + 'linkrunewager', + 'list_groups', + 'logs', + 'menu', + 'mygiveaways', + 'off', + 'on', + 'pick_winner', + 'play', + 'profile', + 'promo', + 'promo_cooldown', + 'promocheck', + 'referral', + 'refreshuser', + 'resolvebug', + 'scan_eligibility', + 'setpromo', + 'settings', + 'signup', + 'start_giveaway', + 'startapp', + 'status', + 'stuck', + 'support', + 't', + 'testall', + 'testgiveaway', + 'tipadd', + 'tipedit', + 'tiplist', + 'tipremove', + 'tips', + 'tipsettings', + 'tiptest', + 'tiptoggle', + 'top', + 'tp', + 'unapprove_group', + 'version', + 'wager30_admin', + 'walkthrough', + 'whois', +]); + const WAGER_BONUS_RULES_TEXT = '๐ŸŽฏ *GambleCodez 30 SC Bonus โ€” Full Details*\n\n' + `${AFFILIATE_REMINDER_TEXT}\n\n` @@ -692,27 +782,33 @@ function isAdmin(ctx) { return ADMIN_IDS.includes(Number(ctx.from.id)); } +function persistAdminMode(user, enabled) { + user.adminModeOn = enabled; + persistRuntimeState(); +} + +async function refreshAdminMenuHeader(ctx, user) { + const chatType = ctx.chat && ctx.chat.type ? ctx.chat.type : null; + if (chatType !== 'private') return; + await sendPersistentAdminMenu(ctx, user); +} + function requireAdmin(ctx) { - const cmd = ctx.message ? String(ctx.message.text || '').split(' ')[0].replace('/', '') : 'command'; + const cmd = ctx.message + ? String(ctx.message.text || '').split(' ')[0].replace('/', '') + : (ctx.callbackQuery && ctx.callbackQuery.data) ? String(ctx.callbackQuery.data) : 'command'; if (!isAdmin(ctx)) { sendCommandError(ctx, { command: cmd, - reason: 'โ›” This command is for admins only.', - howToUse: 'N/A โ€” admin-only command', - example: 'N/A', + reason: 'Admin only', + howToUse: 'This command is restricted to administrators', + example: '/help', }).catch(() => {}); return false; } - // Admin is in user-mode (/on) โ€” treat exactly as a regular user const user = getUser(ctx); - if (user.adminModeOn) { - sendCommandError(ctx, { - command: cmd, - reason: "โ›” Admin-only command. You're currently in user mode (/on).", - howToUse: 'Use /off to switch back to admin mode, then retry.', - example: '/off', - }).catch(() => {}); - return false; + if (!user.adminModeOn) { + persistAdminMode(user, true); } return true; } @@ -901,7 +997,7 @@ function mainMenuKeyboard(isAdminUser = false, page = 1, user = null) { if (isAdminUser) { rows.push([ Markup.button.callback('๐Ÿ”ง Admin Panel', 'menu_admin_tab'), - Markup.button.callback('๐Ÿ“Š Admin Dashboard', 'admin_dashboard_action'), + Markup.button.callback('๐Ÿ›  Admin Dashboard', 'admin_dashboard'), ]); } // Pagination: โ—€๏ธ Back to Page 1 @@ -939,7 +1035,7 @@ function mainMenuKeyboard(isAdminUser = false, page = 1, user = null) { Markup.button.callback('๐Ÿ’ฌ Join Group', 'menu_join_group'), ], // Admin shortcut โ€” only visible to admins - ...(isAdminUser ? [[Markup.button.callback('๐Ÿ›  Admin Dashboard', 'open_admin_dashboard')]] : []), + ...(isAdminUser ? [[Markup.button.callback('๐Ÿ›  Admin Dashboard', 'admin_dashboard')]] : []), // Pagination: Next โ–ถ๏ธ [Markup.button.callback('More Options โ–ถ๏ธ', 'menu_page_2')], ]; @@ -1230,7 +1326,7 @@ async function sendSettingsMenu(ctx, user) { async function sendMainMenu(ctx, user, page = 1) { const name = user.firstName ? `, ${user.firstName}` : ''; - const adminUser = ADMIN_IDS.includes(Number(user.id)); + const adminUser = ADMIN_IDS.includes(Number(user.id)) && user.adminModeOn; const statusLine = buildUserStatusLine(user); const pageLabel = page === 2 ? ' โ€” Page 2/2: Community & Extras' : ' โ€” Page 1/2: Main Actions'; @@ -1256,7 +1352,7 @@ function userMainMenuKeyboard(isAdminUser) { [Markup.button.callback('โ“ Help / Commands', 'pmenu_help')], ]; if (isAdminUser) { - rows.push([Markup.button.callback('๐Ÿ›  Admin Menu', 'pmenu_admin')]); + rows.push([Markup.button.callback('๐Ÿ›  Admin Dashboard', 'admin_dashboard')]); } return Markup.inlineKeyboard(rows); } @@ -1285,8 +1381,8 @@ function userMainMenuText(user) { async function sendPersistentUserMenu(ctx, user) { const chatId = ctx.chat ? ctx.chat.id : (ctx.callbackQuery && ctx.callbackQuery.message ? ctx.callbackQuery.message.chat.id : null); if (!chatId) return; - // Hide admin button when admin is in user-mode (/on) - const isAdminUser = ADMIN_IDS.includes(Number(user.id)) && !user.adminModeOn; + // Show admin button only when admin view is ON + const isAdminUser = ADMIN_IDS.includes(Number(user.id)) && user.adminModeOn; // Delete previous persistent menu message if it exists if (user.mainMenuMsgId && user.mainMenuChatId) { @@ -1297,6 +1393,15 @@ async function sendPersistentUserMenu(ctx, user) { user.mainMenuChatId = null; } + // Ensure admin menu is not lingering when user menu is shown + if (user.adminMenuMsgId && user.adminMenuChatId) { + try { + await ctx.telegram.deleteMessage(user.adminMenuChatId, user.adminMenuMsgId); + } catch (_) { /* ignore */ } + user.adminMenuMsgId = null; + user.adminMenuChatId = null; + } + const text = userMainMenuText(user); const keyboard = userMainMenuKeyboard(isAdminUser); const sent = await ctx.telegram.sendMessage(chatId, text, { parse_mode: 'Markdown', ...keyboard }); @@ -1307,19 +1412,17 @@ async function sendPersistentUserMenu(ctx, user) { /** Keyboard for the persistent ADMIN MAIN MENU */ function adminMainMenuKeyboard() { return Markup.inlineKeyboard([ - [Markup.button.callback('๐Ÿ“Ÿ System Status', 'pamenu_status')], - [Markup.button.callback('๐Ÿ“ˆ Stats Dashboard', 'pamenu_stats')], - [Markup.button.callback('๐ŸŽ‰ Start Giveaway', 'pamenu_start_giveaway')], - [Markup.button.callback('๐ŸŽ Active Giveaways', 'pamenu_active_giveaways')], - [Markup.button.callback('๐Ÿ›  Tools', 'pamenu_tools')], - [Markup.button.callback('๐Ÿ“– Admin Commands Reference', 'pamenu_admin_help')], - [Markup.button.callback('๐Ÿž Bug Reports', 'pamenu_bug_reports')], + [Markup.button.callback('๐Ÿ‘ค Users & Stats', 'pamenu_stats')], + [Markup.button.callback('๐ŸŽ‰ Giveaways', 'pamenu_active_giveaways')], + [Markup.button.callback('๐Ÿ“ฃ Promo Tools', 'admin_cat_promo')], + [Markup.button.callback('โš™๏ธ System Tools', 'admin_cat_system')], + [Markup.button.callback('๐Ÿ”ง Mode Toggle: ON (/on)', 'admin_cmd_mode_on'), Markup.button.callback('๐Ÿ‘ค Mode Toggle: OFF (/off)', 'admin_cmd_mode_off')], [Markup.button.callback('โ†ฉ Back to User Menu', 'pamenu_back_user')], ]); } /** Header text for the persistent ADMIN MAIN MENU */ -function adminMainMenuText() { +function adminMainMenuText(user) { const dash = buildDashboard(); const uptimeSec = Math.floor(process.uptime()); const uptimeH = Math.floor(uptimeSec / 3600); @@ -1327,9 +1430,11 @@ function adminMainMenuText() { const uptimeStr = uptimeH > 0 ? `${uptimeH}h ${uptimeM}m` : `${uptimeM}m`; const lastPersistAge = lastPersistAt ? Math.floor((Date.now() - lastPersistAt) / 1000) : 99999; const statusLight = lastPersistAge < 300 ? '๐ŸŸข' : lastPersistAge < 600 ? '๐ŸŸก' : '๐Ÿ”ด'; + const modeStatus = user && user.adminModeOn ? 'ON' : 'OFF'; return ( `๐Ÿ›  *Admin Menu*\n\n` - + `${statusLight} Bot: Uptime ${uptimeStr} ยท Users: ${dash.totalUsers} ยท Giveaways: ${dash.runningGiveaways} running` + + `${statusLight} Bot: Uptime ${uptimeStr} ยท Users: ${dash.totalUsers} ยท Giveaways: ${dash.runningGiveaways} running\n` + + `Mode Toggle Status: *${modeStatus}*` ); } @@ -1350,7 +1455,16 @@ async function sendPersistentAdminMenu(ctx, user) { user.adminMenuChatId = null; } - const text = adminMainMenuText(); + // Ensure user menu is not lingering when admin menu is shown + if (user.mainMenuMsgId && user.mainMenuChatId) { + try { + await ctx.telegram.deleteMessage(user.mainMenuChatId, user.mainMenuMsgId); + } catch (_) { /* ignore */ } + user.mainMenuMsgId = null; + user.mainMenuChatId = null; + } + + const text = adminMainMenuText(user); const keyboard = adminMainMenuKeyboard(); const sent = await ctx.telegram.sendMessage(chatId, text, { parse_mode: 'Markdown', ...keyboard }); user.adminMenuMsgId = sent.message_id; @@ -1909,8 +2023,6 @@ async function configureBotSurface() { const adminCommands = [ ...privateCommands, { command: 'admin', description: 'Open full admin dashboard' }, - { command: 'admin_help', description: 'Admin command reference guide (page 6 of help booklet)' }, - { command: 'admin_dashboard', description: 'Live stats: users, promo claims, giveaways, errors' }, { command: 'wager30_admin', description: '30 SC bonus admin panel (approve/deny/sent/add/reset)' }, { command: 'giveaway', description: 'Create / manage SC giveaways' }, { command: 'start_giveaway', description: 'Start giveaway wizard (alias)' }, @@ -1922,9 +2034,8 @@ async function configureBotSurface() { { command: 'bugreports', description: 'View all open bug reports' }, { command: 'exportbugs', description: 'Export all open bug reports as plain text' }, { command: 'resolvebug', description: 'Mark a bug report resolved: /resolvebug ' }, - { command: 'adminmode', description: 'Toggle admin bypass mode: /adminmode on|off' }, - { command: 'on', description: 'Enable admin bypass mode (test as a regular user)' }, - { command: 'off', description: 'Disable admin bypass mode (restore admin checks)' }, + { command: 'on', description: 'Enable admin mode view' }, + { command: 'off', description: 'Disable admin mode view' }, { command: 'testall', description: 'Run full static self-test โ€” shows PASS/FAIL for every feature' }, { command: 'testgiveaway', description: 'Run fake giveaway end-to-end test (no real SC)' }, { command: 'health', description: 'Show live health endpoint data' }, @@ -2126,8 +2237,8 @@ bot.command('settings', async (ctx) => { * Returns array of { text, buttons } objects. */ function buildHelpPages(user) { - // Hide admin page when admin is in user-mode (/on) โ€” treat as regular user - const adminUser = ADMIN_IDS.includes(Number(user.id)) && !user.adminModeOn; + // Admin help page is visible only when admin view is ON + const adminUser = ADMIN_IDS.includes(Number(user.id)) && user.adminModeOn; const tips = user.settings && user.settings.tooltipsEnabled; function navRow(page, total) { @@ -2352,16 +2463,14 @@ function buildHelpPages(user) { }, ]; - // โ”€โ”€ Page 6: Admin Command Reference (admins only โ€” hidden in user-mode) โ”€โ”€ + // โ”€โ”€ Page 6: Admin Command Reference (admins only โ€” shown when admin view is ON) โ”€โ”€ if (adminUser) { pages.push({ text: [ '๐Ÿ“– *Admin Help โ€” Page 6/6: Admin Commands*', '', '๐Ÿ›  DASHBOARD & NAVIGATION', - '/admin โ€” Persistent admin main menu', - '/admin_help โ€” This admin command reference', - '/admin_dashboard โ€” Live stats: users, promos, giveaways, errors', + '/admin โ€” Open the Admin Dashboard', '', '๐ŸŽฏ 30 SC BONUS MANAGEMENT', '/wager30_admin โ€” Full bonus admin panel (keyboard-driven)', @@ -2393,8 +2502,8 @@ function buildHelpPages(user) { '/refreshuser โ€” Migrate user to latest schema', '/broadcast_retry โ€” Retry failed broadcasts', '/broadcast_failed โ€” List users with failed delivery', - '/on โ€” Enter user mode (bot treats you as regular user)', - '/off โ€” Exit user mode (restore full admin access)', + '/on โ€” Enable admin view', + '/off โ€” Disable admin view (permissions stay admin)', '', '๐Ÿ’ฌ GROUP MANAGEMENT', '/approve_group โ€” Whitelist a group chat', @@ -2431,7 +2540,7 @@ function buildHelpPages(user) { '/admin_backup โ€” Snapshot runtime state', '', 'โ„น๏ธ All commands include smart errors and usage hints.', - 'โ„น๏ธ Use /on to test as a user, /off to restore admin.', + 'โ„น๏ธ Use /on to enable admin view, /off to hide admin UI.', ].join('\n'), buttons: [ [Markup.button.callback('๐Ÿ›  Open Admin Dashboard', 'open_admin_dashboard'), Markup.button.callback('๐Ÿงช Run TestAll', 'admin_cmd_testall')], @@ -2452,8 +2561,8 @@ function buildHelpPages(user) { * @param {number|string} pageOrTab - page number (1-based) or legacy tab string ('user'|'admin') */ async function sendHelpMenu(ctx, user, pageOrTab = 1) { - // Admin in user-mode (/on) sees no admin page โ€” treat as regular user - const adminUser = ADMIN_IDS.includes(Number(user.id)) && !user.adminModeOn; + // Admin help page is visible only when admin view is ON + const adminUser = ADMIN_IDS.includes(Number(user.id)) && user.adminModeOn; // Legacy tab string support let page = typeof pageOrTab === 'string' @@ -2581,8 +2690,18 @@ bot.command('walkthrough', async (ctx) => { }); bot.command('admin', async (ctx) => { - if (!requireAdmin(ctx)) return; + if (!isAdmin(ctx)) { + await sendCommandError(ctx, { + command: 'admin', + reason: 'Admin only', + howToUse: 'Only admins may access the dashboard', + example: '/help', + }); + return; + } const user = getUser(ctx); + persistAdminMode(user, true); + await refreshAdminMenuHeader(ctx, user); const isGroup = ctx.chat && (ctx.chat.type === 'group' || ctx.chat.type === 'supergroup'); if (isGroup) { // In groups: send a brief inline button so admin can open dashboard in DM @@ -2592,16 +2711,10 @@ bot.command('admin', async (ctx) => { ); return; } - // In DM: send the persistent admin menu + // In DM: send the persistent admin dashboard await sendPersistentAdminMenu(ctx, user); }); -bot.command('admin_help', async (ctx) => { - if (!requireAdmin(ctx)) return; - const user = getUser(ctx); - await sendHelpMenu(ctx, user, 'admin'); -}); - // ========================= // Admin Announce Command โ€” /A /a /announce // State machine: @@ -2783,6 +2896,9 @@ bot.command('giveaway', async (ctx) => { return; } + const user = getUser(ctx); + if (!user.adminModeOn) persistAdminMode(user, true); + // Admin in a group: offer to start giveaway for this group or list active ones if (isGroup) { const running = Array.from(giveawayStore.running.values()); @@ -2805,7 +2921,6 @@ bot.command('giveaway', async (ctx) => { } // Admin in DM: full wizard - const user = getUser(ctx); await gwizStart(ctx, user, { chatId: ctx.chat.id, chatTitle: 'DM', startedBy: ctx.from.id }); }); @@ -2958,22 +3073,6 @@ bot.command('refreshuser', safeAdminHandler('refreshuser', { usage: '/refreshuse await ctx.reply(`โœ… User ${rawId} refreshed to latest schema.`); })); -bot.command('adminmode', async (ctx) => { - if (!requireAdmin(ctx)) return; - const parts = (ctx.message.text || '').trim().split(/\s+/); - const mode = (parts[1] || '').toLowerCase(); - const user = getUser(ctx); - if (mode === 'on') { - user.adminModeOn = true; - await ctx.reply('๐Ÿ”˜ Admin mode ON โ€” auth bypassed for testing.'); - } else if (mode === 'off') { - user.adminModeOn = false; - await ctx.reply('โšช Admin mode OFF โ€” normal auth restored.'); - } else { - await sendCommandError(ctx, { command: 'adminmode', reason: 'Missing mode argument.', howToUse: '/adminmode on|off', example: '/adminmode on' }); - } -}); - bot.command('health', async (ctx) => { if (!requireAdmin(ctx)) return; const port = Number(process.env.PORT || 3000); @@ -3097,6 +3196,7 @@ bot.command('bonus', async (ctx) => { const rest = parts.slice(3).join(' '); if (sub && isAdmin(ctx)) { + if (!user.adminModeOn) persistAdminMode(user, true); clearPendingAction(user); if (sub === 'pending') { return bonusAdminListPending(ctx); @@ -3182,12 +3282,6 @@ bot.command('leaderboard_weekly', safeStepHandler('leaderboard_weekly', async (c await ctx.reply(`๐Ÿ† *Most Active (7 days)*\n\n${lines.join('\n')}`, { parse_mode: 'Markdown' }); })); -bot.command('admin_dashboard', safeStepHandler('admin_dashboard', async (ctx) => { - if (!requireAdmin(ctx)) return; - const dashboard = buildDashboard(); - await ctx.reply(`Admin Live Dashboard\nUsers: ${dashboard.totalUsers}\nActive: ${dashboard.activeUsers}\nOnboarding complete: ${dashboard.onboardingDone}\nPromo claims: ${dashboard.promoClaims}\nErrors: ${dashboard.errors}\nReferral users: ${dashboard.referralUsers}\nGenerated: ${dashboard.generatedAt}`); -})); - bot.command('boost_referrals', safeStepHandler('boost_referrals', async (ctx) => { if (!requireAdmin(ctx)) return; const parts = String(ctx.message.text || '').split(' '); @@ -3252,39 +3346,21 @@ bot.action('ref_leaderboard', async (ctx) => { await ctx.reply(`๐Ÿ† Referral Leaderboard\n${text}`); }); -bot.command('admin_mode_on', async (ctx) => { - if (!requireAdmin(ctx)) return; - const user = getUser(ctx); - user.adminModeOn = true; - await ctx.reply('๐Ÿ”ง Admin mode ON โ€” admin auth temporarily bypassed. Use /off to restore.'); -}); - -bot.command('admin_mode_off', async (ctx) => { - if (!requireAdmin(ctx)) return; - const user = getUser(ctx); - user.adminModeOn = false; - await ctx.reply('โœ… Admin mode OFF โ€” admin auth restored to normal.'); -}); - -// /on and /off โ€” shorthand admin auth bypass toggles +// /on and /off โ€” admin view visibility toggles (permissions remain identity-based) bot.command('on', async (ctx) => { if (!requireAdmin(ctx)) return; const user = getUser(ctx); - user.adminModeOn = true; - await ctx.reply( - '๐Ÿ”˜ *User Mode ON*\n\nYou are now in user mode. All admin commands are blocked and you see the bot exactly as a regular user would.\n\nโ€ข Admin menu is hidden\nโ€ข Admin commands are blocked\nโ€ข Help booklet shows user pages only\n\nUse /off to restore full admin access.', - { parse_mode: 'Markdown', ...Markup.inlineKeyboard([[Markup.button.callback('โฌ…๏ธ Main Menu', 'to_main_menu')]]) }, - ); + persistAdminMode(user, true); + await refreshAdminMenuHeader(ctx, user); + await ctx.reply('๐Ÿ”ง Admin Mode Enabled'); }); bot.command('off', async (ctx) => { if (!requireAdmin(ctx)) return; const user = getUser(ctx); - user.adminModeOn = false; - await ctx.reply( - 'โœ… Admin mode OFF\n\nAdmin authentication restored. You now have full admin access again.', - Markup.inlineKeyboard([[Markup.button.callback('๐Ÿ”ง Admin Panel', 'menu_admin_tab'), Markup.button.callback('โฌ…๏ธ Main Menu', 'to_main_menu')]]), - ); + persistAdminMode(user, false); + await refreshAdminMenuHeader(ctx, user); + await ctx.reply('๐Ÿ‘ค Admin Mode Disabled'); }); bot.command('bugreport', async (ctx) => { @@ -4280,9 +4356,8 @@ bot.action('help_tab_user', async (ctx) => { }); bot.action('help_tab_admin', async (ctx) => { + if (!requireAdmin(ctx)) return; const user = getUser(ctx); - // Block if not admin, or if admin is in user-mode (/on) - if (!ADMIN_IDS.includes(Number(user.id)) || user.adminModeOn) { await ctx.answerCbQuery('Admin only.'); return; } await ctx.answerCbQuery(); await sendHelpMenu(ctx, user, 6); }); @@ -4294,11 +4369,7 @@ bot.action(/^help_page_(\d+)$/, async (ctx) => { await sendHelpMenu(ctx, user, Number(ctx.match[1])); }); -bot.action('open_help', async (ctx) => { - const user = getUser(ctx); - await ctx.answerCbQuery(); - await sendHelpMenu(ctx, user, 1); -}); + // โ”€โ”€ Menu pagination โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ bot.action('menu_page_1', async (ctx) => { @@ -4381,9 +4452,8 @@ bot.action('menu_bugreport', async (ctx) => { await ctx.reply('๐Ÿ› *Bug Report*\n\nDescribe the bug you encountered. Include steps to reproduce if possible.\n\nOr type /cancel to cancel.', { parse_mode: 'Markdown' }); }); -bot.action('menu_bonus_status', async (ctx) => { - const user = getUser(ctx); - await ctx.answerCbQuery(); +async function replyBonusStatus(ctx, user, answerCallback = false) { + if (answerCallback) await ctx.answerCbQuery(); const wb = user.wagerBonus; const statusEmoji = { none: 'โšช', pending: '๐Ÿ•', approved: 'โœ…', denied: 'โŒ', bonus_sent: '๐ŸŽ‰' }; const text = [ @@ -4399,7 +4469,17 @@ bot.action('menu_bonus_status', async (ctx) => { ...(wb.status === 'none' || wb.status === 'denied' ? [[Markup.button.callback('๐ŸŽฏ Request Bonus', 'w30_request_start')]] : []), [Markup.button.callback('โฌ…๏ธ Menu', 'to_main_menu')], ]; - await ctx.reply(text, { parse_mode: 'Markdown', ...Markup.inlineKeyboard(btns) }); + return ctx.reply(text, { parse_mode: 'Markdown', ...Markup.inlineKeyboard(btns) }); +} + +bot.action('menu_bonus_status', async (ctx) => { + const user = getUser(ctx); + return replyBonusStatus(ctx, user, true); +}); + +bot.action('w30_my_status', async (ctx) => { + const user = getUser(ctx); + return replyBonusStatus(ctx, user, true); }); // โ”€โ”€ Admin dashboard navigation โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ @@ -4410,7 +4490,7 @@ bot.action('open_admin_dashboard', async (ctx) => { await sendPersistentAdminMenu(ctx, user); }); -bot.action('admin_dashboard_action', async (ctx) => { +bot.action('admin_dashboard', async (ctx) => { if (!requireAdmin(ctx)) return; const user = getUser(ctx); await ctx.answerCbQuery(); @@ -4518,16 +4598,18 @@ bot.action('admin_cmd_mode_on', async (ctx) => { if (!requireAdmin(ctx)) return; await ctx.answerCbQuery('Admin mode enabled'); const user = getUser(ctx); - user.adminModeOn = true; - await ctx.reply('๐Ÿ”˜ Admin mode ON โ€” auth bypassed for testing.'); + persistAdminMode(user, true); + await refreshAdminMenuHeader(ctx, user); + await ctx.reply('๐Ÿ”ง Admin Mode Enabled'); }); bot.action('admin_cmd_mode_off', async (ctx) => { if (!requireAdmin(ctx)) return; await ctx.answerCbQuery('Admin mode disabled'); const user = getUser(ctx); - user.adminModeOn = false; - await ctx.reply('โšช Admin mode OFF โ€” normal auth restored.'); + persistAdminMode(user, false); + await refreshAdminMenuHeader(ctx, user); + await ctx.reply('๐Ÿ‘ค Admin Mode Disabled'); }); bot.action('admin_cmd_whois_prompt', async (ctx) => { @@ -4643,39 +4725,40 @@ bot.action('admin_stats_lifetime', async (ctx) => { }); bot.action('menu_admin_tab', async (ctx) => { + if (!requireAdmin(ctx)) return; const user = getUser(ctx); - // Block if not admin, or if admin is in user-mode (/on) - if (!ADMIN_IDS.includes(Number(user.id)) || user.adminModeOn) { await ctx.answerCbQuery('Admin only.'); return; } await ctx.answerCbQuery(); - const modeStatus = user.adminModeOn ? '๐Ÿ”ง User mode: ON (use /off to restore)' : 'โœ… Admin mode: ACTIVE'; + const modeStatus = user.adminModeOn ? '๐Ÿ”ง Admin view: ON' : '๐Ÿ‘ค Admin view: OFF'; await ctx.reply( `๐Ÿ”ง Admin Panel\n\n${modeStatus}\n\nQuick actions:`, Markup.inlineKeyboard([ [Markup.button.callback('๐Ÿ“„ Promo Controls', 'admin_view'), Markup.button.callback('๐ŸŽฏ Bonus Panel', 'w30_admin_pending')], - [Markup.button.callback('๐Ÿ“Š Dashboard', 'admin_dashboard_action'), Markup.button.callback('๐Ÿ“ฃ Broadcast', 'admin_broadcast')], + [Markup.button.callback('๐Ÿ›  Admin Dashboard', 'admin_dashboard'), Markup.button.callback('๐Ÿ“ฃ Broadcast', 'admin_broadcast')], [Markup.button.callback('๐Ÿ”ง Admin Help', 'help_tab_admin')], [user.adminModeOn - ? Markup.button.callback('โœ… Turn Auth Back ON (/off)', 'admin_auth_restore') - : Markup.button.callback('๐Ÿ”“ Bypass Auth to Test (/on)', 'admin_auth_bypass')], + ? Markup.button.callback('๐Ÿ‘ค Disable Admin Mode (/off)', 'admin_auth_restore') + : Markup.button.callback('๐Ÿ”ง Enable Admin Mode (/on)', 'admin_auth_bypass')], [Markup.button.callback('โฌ…๏ธ Main Menu', 'to_main_menu')], ]), ); }); bot.action('admin_auth_bypass', async (ctx) => { + if (!requireAdmin(ctx)) return; const user = getUser(ctx); - if (!ADMIN_IDS.includes(Number(user.id))) { await ctx.answerCbQuery('Admin only.'); return; } - user.adminModeOn = true; - await ctx.answerCbQuery('Admin auth bypassed.'); - await ctx.reply('๐Ÿ”ง Admin mode ON โ€” use /off to restore auth.'); + persistAdminMode(user, true); + await refreshAdminMenuHeader(ctx, user); + await ctx.answerCbQuery('Admin mode enabled.'); + await ctx.reply('๐Ÿ”ง Admin Mode Enabled'); }); bot.action('admin_auth_restore', async (ctx) => { + if (!requireAdmin(ctx)) return; const user = getUser(ctx); - if (!ADMIN_IDS.includes(Number(user.id))) { await ctx.answerCbQuery('Admin only.'); return; } - user.adminModeOn = false; - await ctx.answerCbQuery('Admin auth restored.'); - await ctx.reply('โœ… Admin mode OFF โ€” full admin auth active.'); + persistAdminMode(user, false); + await refreshAdminMenuHeader(ctx, user); + await ctx.answerCbQuery('Admin mode disabled.'); + await ctx.reply('๐Ÿ‘ค Admin Mode Disabled'); }); bot.action('menu_profile_action', async (ctx) => { @@ -5263,6 +5346,19 @@ bot.on('text', async (ctx) => { const text = (ctx.message.text || '').trim(); + if (!user.pendingAction && text.startsWith('/')) { + const cmd = text.slice(1).split(/\s+/)[0].split('@')[0]; + if (!REGISTERED_COMMANDS.has(cmd)) { + await sendCommandError(ctx, { + command: cmd || 'unknown', + reason: 'Unknown command.', + howToUse: 'Use /help to view available commands.', + example: '/help', + }); + return; + } + } + // โ”€โ”€ Feature 25: Support portal โ€” capture step-2 description โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ if (user.supportTicket && user.supportTicket.step === 2 && ctx.chat.type === 'private') { const { issueType } = user.supportTicket.data; @@ -7592,8 +7688,7 @@ bot.command('gw_resume', safeAdminHandler('gw_resume', { usage: '/gw_resume })); bot.action(/^gw_pause_(\d+)$/, async (ctx) => { - const _pauseUser = getUser(ctx); - if (!isAdmin(ctx) || _pauseUser.adminModeOn) { await ctx.answerCbQuery('Admins only.'); return; } + if (!requireAdmin(ctx)) return; const gwId = Number(ctx.match[1]); const gw = giveawayStore.running.get(gwId); await ctx.answerCbQuery(); @@ -7607,8 +7702,7 @@ bot.action(/^gw_pause_(\d+)$/, async (ctx) => { }); bot.action(/^gw_resume_(\d+)$/, async (ctx) => { - const _resumeUser = getUser(ctx); - if (!isAdmin(ctx) || _resumeUser.adminModeOn) { await ctx.answerCbQuery('Admins only.'); return; } + if (!requireAdmin(ctx)) return; const gwId = Number(ctx.match[1]); const gw = giveawayStore.running.get(gwId); await ctx.answerCbQuery(); @@ -8151,6 +8245,15 @@ async function startBot() { } } +// Fallback for unmatched callback_data: always acknowledge and provide recovery path. +bot.action(/.*/, async (ctx) => { + await ctx.answerCbQuery('Action not available anymore.').catch(() => {}); + await ctx.reply( + 'โš ๏ธ That button is no longer active. Please open /menu and try again.', + Markup.inlineKeyboard([[Markup.button.callback('๐Ÿ  Main Menu', 'to_main_menu')]]), + ).catch(() => {}); +}); + // ========================= // Runtime environment guard //