diff --git a/index.js b/index.js index f6077ec..3d0378d 100644 --- a/index.js +++ b/index.js @@ -1851,10 +1851,10 @@ function adminDashboardKeyboard(page = 1) { ], [ Markup.button.callback('๐Ÿ›Ÿ Support Tools', 'admin_cat_support'), - Markup.button.callback('๐Ÿ“ˆ Stats', 'admin_stats_menu'), + Markup.button.callback('๐Ÿงช Tests & Bugs', 'admin_cat_tests'), ], [ - Markup.button.callback('๐Ÿ“„ Admin Help', 'help_tab_admin'), + Markup.button.callback('๐Ÿ“ˆ Stats', 'admin_stats_menu'), Markup.button.callback('โ–ถ๏ธ Quick Actions', 'admin_dash_page_2'), ], [Markup.button.callback('โฌ…๏ธ Back to User Menu', 'to_main_menu')], @@ -1888,6 +1888,7 @@ function adminGiveawayToolsKeyboard() { function adminPromoToolsKeyboard() { return Markup.inlineKeyboard([ [Markup.button.callback('๐ŸŽ› Promo Manager', 'admin_promo_manager')], + [Markup.button.callback('โ„น๏ธ Promo Step Guide', 'admin_pm_help')], [Markup.button.callback('โž• Create Promo', 'admin_pm_create')], [Markup.button.callback('โœ๏ธ Edit Promo', 'admin_pm_edit')], [Markup.button.callback('โธ Pause/Unpause', 'admin_pm_pause_toggle')], @@ -2159,7 +2160,11 @@ function adminMainMenuKeyboard(user) { ], [ Markup.button.callback('โš™๏ธ System & Health', 'admin_cat_system'), - Markup.button.callback('๐Ÿ“„ Admin Help', 'help_tab_admin'), + Markup.button.callback('๐Ÿงช Tests & Bugs', 'admin_cat_tests'), + ], + [ + Markup.button.callback('๐Ÿ“Ÿ VPS Console (/sshv)', 'sshv_open'), + Markup.button.callback('๐Ÿ’ก Content Drops', 'admin_cmd_tips_dashboard'), ], [Markup.button.callback(toggleLabel, 'admin_cmd_mode_toggle')], [Markup.button.callback('โ†ฉ Back to User Menu', 'pamenu_back_user')], @@ -3320,32 +3325,18 @@ function buildHelpPages(user) { '/tiptoggle โ€” Enable/disable tips feature', '/tipsettings โ€” Configure content drop settings', '', - '๐Ÿž BUG REPORTS', - '/bugreports โ€” View all open bug reports', - '/exportbugs โ€” Export all reports as plain text', - '/resolvebug โ€” Mark a report resolved', - '', '๐Ÿ“Š ANALYTICS', '/funnel โ€” Conversion funnel stats', '/discord_stats โ€” Discord verification stats', '', - '๐Ÿ”ง SYSTEM', - '/testall โ€” Full static self-test (every feature PASS/FAIL)', - '/testgiveaway โ€” Fake giveaway end-to-end test', - '/health โ€” Live health endpoint data', - '/version โ€” Bot version, Node.js version, uptime', - '/deploy โ€” Trigger live deploy from latest git commit', - '/deploy_status โ€” Last deploy result and timing', - '/logs [lines] โ€” Last N lines of bot log', - '/admin_log [lines] โ€” Admin event log (NDJSON)', - '/admin_backup โ€” Snapshot runtime state', + '๐Ÿงช BUGS + TESTS + SYSTEM COMMANDS', + 'Moved to Admin Dashboard โ†’ Tests & Bugs / System Tools menus for easier use.', '', 'โ„น๏ธ All commands include smart errors and usage hints.', 'โ„น๏ธ 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')], - [Markup.button.callback('๐Ÿž Bug Reports', 'pamenu_bug_reports')], + [Markup.button.callback('๐Ÿ›  Open Admin Dashboard', 'open_admin_dashboard'), Markup.button.callback('๐Ÿงช Tests & Bugs', 'admin_cat_tests')], navRow(6, total), [Markup.button.callback('โฌ…๏ธ Menu', 'to_main_menu')], ], @@ -3608,7 +3599,7 @@ async function handleAnnounceCommand(ctx) { const user = getUser(ctx); clearPendingAction(user); user.pendingAction = { type: 'await_announcement_text' }; - await ctx.reply('Okay! What would you like to announce?\nYou can send normal text or HTML. After sending text, choose targets (DM/channel/group).'); + await ctx.reply('Okay! What would you like to announce?\nYou can send normal text or HTML. After sending text, choose targets (DM/channel/group).', Markup.inlineKeyboard([[Markup.button.callback('โฌ…๏ธ Return to Admin Menu', 'open_admin_dashboard')], [Markup.button.callback('โŒ Cancel', 'admin_cancel')]])); } bot.command('A', handleAnnounceCommand); @@ -4607,7 +4598,21 @@ bot.action('admin_cmd_announce_start', async (ctx) => { bot.action('admin_cmd_tiptest', async (ctx) => { if (!requireAdmin(ctx)) return; await ctx.answerCbQuery(); - await ctx.reply('Use /tiptest to send a test tip to the configured group.'); + const enabled = tipsStore.tips.filter((t) => t.enabled); + if (!enabled.length) { await ctx.reply('No enabled content drops to test.'); return; } + const pool = enabled.length > 1 && tipsStore.lastSentTipId != null + ? enabled.filter((t) => t.id !== tipsStore.lastSentTipId) + : enabled; + const tip = pool[Math.floor(Math.random() * pool.length)]; + const sendResult = await postTipToConfiguredTarget(tip, bot.telegram); + if (sendResult.ok) { + tipsStore.lastSentTipId = tip.id; + persistRuntimeState(); + saveHelpfulMessages(); + await ctx.reply(`โœ… Test content drop posted to ${sendResult.target} (silent).`, Markup.inlineKeyboard([[Markup.button.callback('๐Ÿ’ก Open Content Drops Manager', 'admin_cmd_tips_dashboard')], [Markup.button.callback('โฌ…๏ธ Admin Dashboard', 'open_admin_dashboard')]])); + } else { + await ctx.reply(`โŒ Failed to post test content drop. ${sendResult.error ? sendResult.error.message : 'Unknown error'}\nTip: use /register_chat or forward any message from the target group/channel here to auto-register.`); + } }); bot.action('admin_broadcast', async (ctx) => { @@ -4711,7 +4716,7 @@ bot.action('pamenu_active_giveaways', async (ctx) => { const running = Array.from(giveawayStore.running.values()); const text = buildActiveGiveawaysText(); const keyboard = activeGiveawaysKeyboard(running); - await ctx.reply(text, { parse_mode: 'Markdown', ...keyboard }); + await replaceCallbackPanel(ctx, text, { parse_mode: 'Markdown', ...keyboard }); }); bot.action('pamenu_tools', async (ctx) => { @@ -4772,6 +4777,11 @@ bot.action('pamenu_back_admin', async (ctx) => { const user = getUser(ctx); await ctx.answerCbQuery(); if (!requireAdmin(ctx)) return; + try { + await ctx.deleteMessage(); + } catch (_) { + try { await ctx.editMessageReplyMarkup({ inline_keyboard: [] }); } catch (_) { /* ignore */ } + } await sendPersistentAdminMenu(ctx, user); }); @@ -5403,6 +5413,33 @@ bot.action('admin_cat_support', async (ctx) => { await replaceCallbackPanel(ctx, '๐Ÿ›Ÿ *Support Tools*', { parse_mode: 'Markdown', ...adminSupportToolsKeyboard() }); }); + +function adminTestsToolsKeyboard() { + return Markup.inlineKeyboard([ + [Markup.button.callback('๐Ÿงช Run TestAll', 'admin_cmd_testall'), Markup.button.callback('๐ŸŽ Test Giveaway', 'admin_cmd_testgiveaway')], + [Markup.button.callback('๐Ÿ› View Bug Reports', 'admin_cmd_viewbugs'), Markup.button.callback('๐Ÿ“ค Export Bugs', 'admin_cmd_exportbugs')], + [Markup.button.callback('โœ… Resolve Bug', 'admin_cmd_resolvebug_prompt'), Markup.button.callback('๐Ÿงช Test Content Drop', 'admin_cmd_tiptest')], + [Markup.button.callback('๐Ÿ“Ÿ Open SSHV Console', 'sshv_open')], + [Markup.button.callback('โฌ…๏ธ Admin Dashboard', 'open_admin_dashboard')], + ]); +} + +bot.action('admin_cat_tests', async (ctx) => { + if (!requireAdmin(ctx)) return; + await ctx.answerCbQuery(); + await replaceCallbackPanel(ctx, + `๐Ÿงช *Tests & Bug Tools* + +Use this menu for diagnostics, bug workflows, and test sends. + +โ€ข TestAll = full sanity check +โ€ข Test Giveaway = simulated giveaway flow +โ€ข Test Content Drop = sends one random drop to current target +โ€ข SSHV = VPS console tools`, + { parse_mode: 'Markdown', ...adminTestsToolsKeyboard() }, + ); +}); + // Inline triggers for admin dashboard quick-action buttons bot.action('admin_cmd_start_giveaway', async (ctx) => { if (!requireAdmin(ctx)) return; @@ -5415,9 +5452,29 @@ bot.action('admin_cmd_giveaway_status', async (ctx) => { if (!requireAdmin(ctx)) return; await ctx.answerCbQuery(); const running = Array.from(giveawayStore.running.values()); - if (!running.length) { await ctx.reply('No giveaways currently running.'); return; } - const lines = running.map((g) => `#${g.id} โ€” ${g.participants.size} entries โ€” ends ${new Date(g.endTime).toUTCString()}`); - await ctx.reply(`๐Ÿ“Š Running Giveaways (${running.length}):\n\n${lines.join('\n')}`); + if (!running.length) { + await replaceCallbackPanel(ctx, `๐ŸŽ *Active Giveaways*\n\nNo giveaways are currently running.`, { + parse_mode: 'Markdown', + ...Markup.inlineKeyboard([[Markup.button.callback('โฌ…๏ธ Return to Admin Menu', 'pamenu_back_admin')]]), + }); + return; + } + const lines = running.map((g) => { + const remainSec = Math.max(0, Math.floor((g.endTime - Date.now()) / 1000)); + const remain = remainSec > 3600 + ? `${Math.floor(remainSec / 3600)}h ${Math.floor((remainSec % 3600) / 60)}m` + : `${Math.floor(remainSec / 60)}m ${remainSec % 60}s`; + return `#${g.id} โ€” ${g.participants.size} entries โ€” โฑ ${remain} remaining`; + }); + const actionRows = running.slice(0, 5).map((g) => ([ + Markup.button.callback(`๐ŸŽ‰ Join #${g.id}`, `gw_join_${g.id}`), + Markup.button.callback(`๐Ÿ“‹ Details #${g.id}`, `gw_details_${g.id}`), + ])); + actionRows.push([Markup.button.callback('โฌ…๏ธ Return to Admin Menu', 'pamenu_back_admin')]); + await replaceCallbackPanel(ctx, `๐Ÿ“Š *Running Giveaways* (${running.length})\n\n${lines.join('\\n')}`, { + parse_mode: 'Markdown', + ...Markup.inlineKeyboard(actionRows), + }); }); bot.action('admin_cmd_testgiveaway', async (ctx) => { @@ -5441,7 +5498,29 @@ bot.action('admin_cmd_health', async (ctx) => { bot.action('admin_cmd_version', async (ctx) => { if (!requireAdmin(ctx)) return; await ctx.answerCbQuery(); - await ctx.reply('Use /version to view bot version info.'); + const commitHash = process.env.COMMIT_HASH || 'local'; + const deployTime = process.env.DEPLOY_TIME || 'unknown'; + const uptime = Math.floor(process.uptime()); + const uptimeStr = `${Math.floor(uptime / 3600)}h ${Math.floor((uptime % 3600) / 60)}m ${uptime % 60}s`; + await replaceCallbackPanel(ctx, + `๐Ÿ“ฆ *Bot Version Info* + +Version: \`${pkgVersion}\` +Commit: \`${commitHash}\` +Deployed: ${deployTime} +Node: \`${process.version}\` +Uptime: ${uptimeStr}`, + { parse_mode: 'Markdown', ...Markup.inlineKeyboard([ + [Markup.button.callback('โš™๏ธ System Tools', 'admin_cat_system')], + [Markup.button.callback('โฌ…๏ธ Admin Dashboard', 'open_admin_dashboard')], + ]) }, + ); +}); + +bot.action('admin_cmd_verify_setup', async (ctx) => { + if (!requireAdmin(ctx)) return; + await ctx.answerCbQuery(); + await ctx.reply('Use /verify_bot_setup to validate BotFather setup, permissions, and targets.'); }); bot.action('admin_cmd_verify_setup', async (ctx) => { @@ -5908,6 +5987,33 @@ bot.action('admin_promo_manager', async (ctx) => { await replaceCallbackPanel(ctx, promoManagerSummaryText(), { parse_mode: 'Markdown', ...adminPromoToolsKeyboard() }); }); + +bot.action('admin_pm_help', async (ctx) => { + if (!requireAdmin(ctx)) return; + await ctx.answerCbQuery(); + const guide = [ + 'โ„น๏ธ *Promo Manager Step Guide*', + '', + 'Create Promo is a 9-step wizard. Example values:', + '1) Name: `New User Bonus`', + '2) Code/Link: `WELCOME35` or `https://casino.example/promo`', + '3) Casino base URL: `casino.example`', + '4) Description: `Claim 3.5 SC for first-time users`', + '5) Image URL (optional): `https://.../promo.png` or `skip`', + '6) Requirement type: `A` New user, `B` Existing user, `C` Existing + wager', + ' If C: enter wager SC amount, then rolling days (e.g. `3000`, `7`)', + '7) Claim limit: number or `none`', + '8) Cooldown hours: number or `none`', + '9) Approval mode: `auto` or `admin`', + '', + 'Tips:', + 'โ€ข Use base domain only in step 3 (no path/query).', + 'โ€ข Use clear names so users know who the promo is for.', + 'โ€ข You can preview, pause, and edit anytime from this menu.', + ].join('\n'); + await replaceCallbackPanel(ctx, guide, { parse_mode: 'Markdown', ...adminPromoToolsKeyboard() }); +}); + bot.action('admin_view', async (ctx) => { if (!requireAdmin(ctx)) return; await ctx.answerCbQuery(); @@ -6389,6 +6495,30 @@ bot.on('inline_query', safeStepHandler('inline_query', async (ctx) => { await ctx.answerInlineQuery(filtered, { cache_time: 5, is_personal: true }); })); + +function extractForwardedChat(msg = {}) { + return msg.forward_from_chat + || (msg.forward_origin && msg.forward_origin.chat) + || (msg.sender_chat ? msg.sender_chat : null) + || null; +} + +async function autoRegisterForwardedChatIfPresent(ctx, user) { + if (!isAdmin(ctx)) return false; + const fwdChat = extractForwardedChat(ctx.message || {}); + if (!fwdChat || !fwdChat.id) return false; + const chatId = Number(fwdChat.id); + approvedGroupsStore.add(chatId); + tipsStore.targetGroup = String(chatId); + broadcastConfigStore.targetGroup = String(chatId); + persistRuntimeState(); + await ctx.reply(`โœ… Auto-registered target chat from forwarded message: +โ€ข ${fwdChat.title || chatId} (${chatId}) + +Content Drops + group broadcasts now use this target.`); + return true; +} + // ========================= // Text message handler for pending actions // ========================= @@ -6429,6 +6559,8 @@ bot.on('text', async (ctx) => { return; } + if (!user.pendingAction && await autoRegisterForwardedChatIfPresent(ctx, user)) return; + // Smart username detection โ€” fires when there is NO pending action, the message looks // like a Runewager username (3-30 chars, alphanumeric/underscore/hyphen/dot, no spaces), // and the user has not yet linked an account. Always requires confirmation โ€” never auto-accepts. @@ -6485,10 +6617,7 @@ bot.on('text', async (ctx) => { if (action.type === 'await_register_chat_forward') { if (!requireAdmin(ctx)) return; - const msg = ctx.message || {}; - const fwdChat = msg.forward_from_chat - || (msg.forward_origin && msg.forward_origin.chat) - || (msg.sender_chat ? msg.sender_chat : null); + const fwdChat = extractForwardedChat(ctx.message || {}); if (!fwdChat || !fwdChat.id) { await ctx.reply('Please forward a message from the target channel/group.'); return; @@ -6499,10 +6628,10 @@ bot.on('text', async (ctx) => { broadcastConfigStore.targetGroup = String(chatId); user.pendingAction = null; persistRuntimeState(); - await ctx.reply(`โœ… Registered chat for broadcasts/tips: + await ctx.reply(`โœ… Registered chat for broadcasts/content drops: โ€ข ${fwdChat.title || chatId} (${chatId}) -Tips and group broadcasts will now use this target.`); +Content Drops and group broadcasts will now use this target.`); return; } diff --git a/test/smoke.test.js b/test/smoke.test.js index e34e69b..6cf88af 100644 --- a/test/smoke.test.js +++ b/test/smoke.test.js @@ -26,7 +26,15 @@ function extractLiteralIds(source, kind) { const regex = kind === 'callback' ? /Markup\.button\.callback\([^,]+,\s*(["'`])((?:\\.|(?!\1).)*)\1\)/g : /bot\.action\(\s*(["'`])((?:\\.|(?!\1).)*)\1/g; - return new Set(Array.from(source.matchAll(regex), (m) => m[2])); + const ids = []; + for (const m of source.matchAll(regex)) { + const quote = m[1]; + const id = m[2]; + // Skip templated callback ids (e.g. `help_page_${page + 1}`), they are dynamic. + if (kind === 'callback' && quote === '`' && id.includes('${')) continue; + ids.push(id); + } + return new Set(ids); } function extractActionRegexPatterns(source) {