diff --git a/index.js b/index.js index 1472f84..1e923fc 100644 --- a/index.js +++ b/index.js @@ -169,21 +169,21 @@ const giveawayStore = { // Tips Store โ€” rotating silent group posts // ========================= const DEFAULT_TIPS_LIST = [ - { id: 1, text: '๐Ÿ’ก *Did you know?* Runewager gives *instant crypto redemptions* on all sweepstakes prizes. No delays, no stress โ€” just claim and flex.', enabled: true }, - { id: 2, text: '๐Ÿ‘‹ New here? Tap /start in the bot to unlock your *FREE SC bonuses*, link your account, and join our giveaways.', enabled: true }, - { id: 3, text: '๐Ÿ† Runewager\'s weekly leaderboard is *LIVE* โ€” climb the ranks, earn bragging rights, and win real prizes. All 100% free to play.', enabled: true }, - { id: 4, text: '๐Ÿ”— *Pro tip:* Linking your Runewager username in the bot unlocks bonuses, giveaways, and faster support.', enabled: true }, + { id: 1, text: '๐Ÿ’ก Did you know? Runewager gives instant crypto redemptions on all sweepstakes prizes. No delays, no stress โ€” just claim and flex.', enabled: true }, + { id: 2, text: '๐Ÿ‘‹ New here? Tap /start in the bot to unlock your FREE SC bonuses, link your account, and join our giveaways.', enabled: true }, + { id: 3, text: '๐Ÿ† Runewager\'s weekly leaderboard is LIVE โ€” climb the ranks, earn bragging rights, and win real prizes. All 100% free to play.', enabled: true }, + { id: 4, text: '๐Ÿ”— Pro tip: Linking your Runewager username in the bot unlocks bonuses, giveaways, and faster support.', enabled: true }, { id: 5, text: '๐ŸŽ Want more SC? Join our Telegram channel for surprise drops, flash bonuses, and exclusive codes.', enabled: true }, - { id: 6, text: '๐ŸŽฎ Runewager features top providers like *Hacksaw*, *BGaming*, *Betsoft*, and more โ€” all sweepstakesโ€‘safe and free to play.', enabled: true }, + { id: 6, text: '๐ŸŽฎ Runewager features top providers like Hacksaw, BGaming, Betsoft, and more โ€” all sweepstakesโ€‘safe and free to play.', enabled: true }, { id: 7, text: '๐Ÿ”’ Make sure you\'re Discordโ€‘verified in the bot โ€” it unlocks extra rewards and keeps your account secure.', enabled: true }, - { id: 8, text: '๐ŸŽ‰ Tap *Giveaways* in the bot menu to join our SC giveaways with automatic eligibility checks.', enabled: true }, + { id: 8, text: '๐ŸŽ‰ Tap Giveaways in the bot menu to join our SC giveaways with automatic eligibility checks.', enabled: true }, { id: 9, text: '๐ŸŒ Runewager accepts players worldwide โ€” no region blocks, no deposit requirements, just pure sweepstakes fun.', enabled: true }, { id: 10, text: '๐Ÿ’ฐ Your bonuses refresh often โ€” check the bot for new user bonuses, 30 SC rewards, and seasonal promos.', enabled: true }, { id: 11, text: '๐Ÿค Want to help the community grow? Share the bot link with a friend โ€” more players means bigger giveaways.', enabled: true }, - { id: 12, text: 'โ“ If you ever get stuck, tap *Help / Commands* in the bot for a full guide, tooltips, and troubleshooting.', enabled: true }, - { id: 13, text: '๐Ÿ“ฑ Runewager\'s miniโ€‘app loads instantly inside Telegram โ€” or switch to browser mode in *Settings* if you prefer.', enabled: true }, + { id: 12, text: 'โ“ If you ever get stuck, tap Help / Commands in the bot for a full guide, tooltips, and troubleshooting.', enabled: true }, + { id: 13, text: '๐Ÿ“ฑ Runewager\'s miniโ€‘app loads instantly inside Telegram โ€” or switch to browser mode in Settings if you prefer.', enabled: true }, { id: 14, text: '๐Ÿ‘€ Stay active in the group โ€” admins drop surprise SC codes and leaderboard boosts at random times.', enabled: true }, - { id: 15, text: '๐Ÿ†“ Reminder: Runewager is *100% free to play*. No deposits, no wagering, no risk โ€” just sweepstakes fun and instant crypto prizes.', enabled: true }, + { id: 15, text: '๐Ÿ†“ Reminder: Runewager is 100% free to play. No deposits, no wagering, no risk โ€” just sweepstakes fun and instant crypto prizes.', enabled: true }, ]; const tipsStore = { @@ -1737,7 +1737,8 @@ function adminUserToolsKeyboard() { ]); } -function adminSystemToolsKeyboard() { +function adminSystemToolsKeyboard(user = null) { + const toggleLabel = user && user.adminModeOn ? '๐ŸŸข Admin Mode: ON (tap to toggle)' : 'โšช Admin Mode: OFF (tap to toggle)'; return Markup.inlineKeyboard([ [Markup.button.callback('๐Ÿ›  Run TestAll', 'admin_cmd_testall')], [Markup.button.callback('๐Ÿ“Ÿ VPS Console (/sshv)', 'sshv_open')], @@ -1745,7 +1746,7 @@ function adminSystemToolsKeyboard() { [Markup.button.callback('๐Ÿ“ฆ Bot Version', 'admin_cmd_version')], [Markup.button.callback('๐Ÿงช Tip Test (4h broadcast)', 'admin_cmd_tiptest')], [Markup.button.callback('๐Ÿ’พ Backup State', 'admin_backup_action')], - [Markup.button.callback('๐Ÿ”˜ Admin Mode On', 'admin_cmd_mode_on'), Markup.button.callback('โšช Admin Mode Off', 'admin_cmd_mode_off')], + [Markup.button.callback(toggleLabel, 'admin_cmd_mode_toggle')], [Markup.button.callback('โฌ…๏ธ Admin Dashboard', 'open_admin_dashboard')], ]); } @@ -1833,6 +1834,36 @@ async function replaceCallbackPanel(ctx, text, extra = {}) { await ctx.reply(text, extra); } +function extractCallbackDataFromKeyboard(markup) { + const rows = (markup && markup.reply_markup && Array.isArray(markup.reply_markup.inline_keyboard)) + ? markup.reply_markup.inline_keyboard + : []; + const out = []; + for (const row of rows) { + for (const btn of row || []) { + if (btn && typeof btn.callback_data === 'string') out.push(btn.callback_data); + } + } + return out; +} + +function escapeHtml(text) { + return String(text) + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, '''); +} + +function formatTipForHtml(text) { + const t = String(text || '').trim(); + if (!t) return ''; + // If admin intentionally provided HTML tags, preserve them. + if (/[<][a-zA-Z/!][^>]*>/.test(t)) return t; + return escapeHtml(t); +} + async function sendSettingsMenu(ctx, user) { const s = user.settings; const lines = [ @@ -1937,13 +1968,14 @@ async function sendPersistentUserMenu(ctx, user) { } /** Keyboard for the persistent ADMIN MAIN MENU */ -function adminMainMenuKeyboard() { +function adminMainMenuKeyboard(user) { + const toggleLabel = user && user.adminModeOn ? '๐ŸŸข Admin Mode: ON (tap to toggle)' : 'โšช Admin Mode: OFF (tap to toggle)'; return Markup.inlineKeyboard([ [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(toggleLabel, 'admin_cmd_mode_toggle')], [Markup.button.callback('โ†ฉ Back to User Menu', 'pamenu_back_user')], ]); } @@ -1992,7 +2024,7 @@ async function sendPersistentAdminMenu(ctx, user) { } const text = adminMainMenuText(user); - const keyboard = adminMainMenuKeyboard(); + const keyboard = adminMainMenuKeyboard(user); const sent = await ctx.telegram.sendMessage(chatId, text, { parse_mode: 'Markdown', ...keyboard }); user.adminMenuMsgId = sent.message_id; user.adminMenuChatId = sent.chat.id; @@ -5164,7 +5196,8 @@ bot.action('admin_cat_user', async (ctx) => { bot.action('admin_cat_system', async (ctx) => { if (!requireAdmin(ctx)) return; await ctx.answerCbQuery(); - await replaceCallbackPanel(ctx, 'โš™๏ธ *System Tools*', { parse_mode: 'Markdown', ...adminSystemToolsKeyboard() }); + const user = getUser(ctx); + await replaceCallbackPanel(ctx, 'โš™๏ธ *System Tools*', { parse_mode: 'Markdown', ...adminSystemToolsKeyboard(user) }); }); bot.action('admin_cat_support', async (ctx) => { @@ -5226,22 +5259,33 @@ bot.action('admin_backup_action', async (ctx) => { } }); +bot.action('admin_cmd_mode_toggle', async (ctx) => { + if (!requireAdmin(ctx)) return; + const user = getUser(ctx); + const next = !Boolean(user.adminModeOn); + persistAdminMode(user, next); + await ctx.answerCbQuery(next ? 'Admin mode enabled' : 'Admin mode disabled'); + await refreshAdminMenuHeader(ctx, user); + await sendPersistentAdminMenu(ctx, user); +}); + +// Backward-compatible aliases bot.action('admin_cmd_mode_on', async (ctx) => { if (!requireAdmin(ctx)) return; - await ctx.answerCbQuery('Admin mode enabled'); const user = getUser(ctx); persistAdminMode(user, true); + await ctx.answerCbQuery('Admin mode enabled'); await refreshAdminMenuHeader(ctx, user); - await ctx.reply('๐Ÿ”ง Admin Mode Enabled'); + await sendPersistentAdminMenu(ctx, user); }); bot.action('admin_cmd_mode_off', async (ctx) => { if (!requireAdmin(ctx)) return; - await ctx.answerCbQuery('Admin mode disabled'); const user = getUser(ctx); persistAdminMode(user, false); + await ctx.answerCbQuery('Admin mode disabled'); await refreshAdminMenuHeader(ctx, user); - await ctx.reply('๐Ÿ‘ค Admin Mode Disabled'); + await sendPersistentAdminMenu(ctx, user); }); bot.action('admin_cmd_whois_prompt', async (ctx) => { @@ -6916,8 +6960,8 @@ function startTipsScheduler() { logEvent('warn', 'Skipping helpful message: missing permission in target group', { target: tipsStore.targetGroup }); return; } - await bot.telegram.sendMessage(tipsStore.targetGroup, tip.text, { - parse_mode: 'Markdown', + await bot.telegram.sendMessage(tipsStore.targetGroup, formatTipForHtml(tip.text), { + parse_mode: 'HTML', disable_notification: true, }); tipsStore.lastSentTipId = tip.id; @@ -6935,6 +6979,7 @@ function tipsDashboardKeyboard() { [Markup.button.callback('โŒ Remove Tip', 'tips_cmd_remove'), Markup.button.callback('๐Ÿ” Toggle System', 'tips_cmd_toggle')], [Markup.button.callback('๐Ÿ“‹ View All Tips', 'tips_cmd_list'), Markup.button.callback('๐Ÿงช Test Random Tip', 'tips_cmd_test')], [Markup.button.callback('โš™๏ธ Settings', 'tips_cmd_settings')], + [Markup.button.callback('๐Ÿ“ฅ Import Batch JSON', 'tips_cmd_import_batch')], [Markup.button.callback('โ†ฉ Admin Menu', 'pamenu_back_admin')], ]); } @@ -6958,7 +7003,7 @@ async function sendTipsDashboard(ctx) { const enabled = tipsStore.tips.filter((t) => t.enabled).length; const status = tipsStore.systemEnabled ? '๐ŸŸข Enabled' : '๐Ÿ”ด Disabled'; const text = `๐Ÿ“ *Tips Manager*\n\nStatus: ${status}\nTotal Tips: ${total} (${enabled} active)\nInterval: every ${tipsStore.intervalHours}h\nTarget: ${tipsStore.targetGroup}`; - await ctx.reply(text, { parse_mode: 'Markdown', ...tipsDashboardKeyboard() }); + await replaceCallbackPanel(ctx, text, { parse_mode: 'Markdown', ...tipsDashboardKeyboard() }); } // โ”€โ”€ /tips /tiplist /tipadd /tipremove /tipedit /tiptoggle /tiptest /tipsettings โ”€โ”€ @@ -7058,9 +7103,23 @@ bot.command('tiptoggle', async (ctx) => { bot.command('tiptest', async (ctx) => { if (!requireAdmin(ctx)) return; const enabled = tipsStore.tips.filter((t) => t.enabled); - if (!enabled.length) { await ctx.reply('No enabled tips to preview.'); return; } - const tip = enabled[Math.floor(Math.random() * enabled.length)]; - await ctx.reply(`Here is a random tip preview:\n\n${tip.text}`, { parse_mode: 'Markdown' }); + if (!enabled.length) { await ctx.reply('No enabled tips 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)]; + try { + await bot.telegram.sendMessage(tipsStore.targetGroup, formatTipForHtml(tip.text), { + parse_mode: 'HTML', + disable_notification: true, + }); + tipsStore.lastSentTipId = tip.id; + persistRuntimeState(); + saveHelpfulMessages(); + await ctx.reply(`โœ… Test tip posted to ${tipsStore.targetGroup} (silent).`); + } catch (e) { + await ctx.reply(`โŒ Failed to post test tip to ${tipsStore.targetGroup}: ${e.message}`); + } }); bot.command('tipsettings', async (ctx) => { @@ -7123,9 +7182,33 @@ bot.action('tips_cmd_test', async (ctx) => { if (!requireAdmin(ctx)) return; await ctx.answerCbQuery(); const enabled = tipsStore.tips.filter((t) => t.enabled); - if (!enabled.length) { await ctx.reply('No enabled tips to preview.'); return; } - const tip = enabled[Math.floor(Math.random() * enabled.length)]; - await ctx.reply(`Here is a random tip preview:\n\n${tip.text}`, { parse_mode: 'Markdown' }); + if (!enabled.length) { await ctx.reply('No enabled tips 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)]; + try { + await bot.telegram.sendMessage(tipsStore.targetGroup, formatTipForHtml(tip.text), { + parse_mode: 'HTML', + disable_notification: true, + }); + tipsStore.lastSentTipId = tip.id; + persistRuntimeState(); + saveHelpfulMessages(); + await ctx.reply(`โœ… Test tip posted to ${tipsStore.targetGroup} (silent).`); + } catch (e) { + await ctx.reply(`โŒ Failed to post test tip to ${tipsStore.targetGroup}: ${e.message}`); + } + await sendTipsDashboard(ctx); +}); + +bot.action('tips_cmd_import_batch', async (ctx) => { + if (!requireAdmin(ctx)) return; + const user = getUser(ctx); + await ctx.answerCbQuery(); + clearPendingAction(user); + user.pendingAction = { type: 'await_tip_add_text' }; + await ctx.reply('Paste a JSON array to append tips (supports plain text or HTML):\n\n/tipadd [{"text":"My Tip","enabled":true}]'); }); bot.action('tips_cmd_settings', async (ctx) => { @@ -8158,6 +8241,46 @@ bot.command('testall', async (ctx) => { else throw new Error('not a function'); } catch (e) { fail('Admin Dashboard', 'sendCommandError', e.message); } + // โ”€โ”€ 6b. Menu & Callback Audit โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + try { + const source = fs.readFileSync(__filename, 'utf8'); + const callbackPattern = /bot\.action\((?:'([^']+)'|"([^"]+)")/g; + const registered = new Set(); + let m; + while ((m = callbackPattern.exec(source)) !== null) { + registered.add((m[1] || m[2] || '').trim()); + } + + const sampleUser = createDefaultUser({ id: -101, username: 'menuaudit', first_name: 'Menu' }); + sampleUser.adminModeOn = true; + const menuKeyboards = [ + ['adminMainMenuKeyboard', adminMainMenuKeyboard(sampleUser)], + ['adminDashboardKeyboard1', adminDashboardKeyboard(1)], + ['adminDashboardKeyboard2', adminDashboardKeyboard(2)], + ['adminStatsKeyboard', adminStatsKeyboard()], + ['adminGiveawayToolsKeyboard', adminGiveawayToolsKeyboard()], + ['adminPromoToolsKeyboard', adminPromoToolsKeyboard()], + ['adminUserToolsKeyboard', adminUserToolsKeyboard()], + ['adminSystemToolsKeyboard', adminSystemToolsKeyboard(sampleUser)], + ['adminSupportToolsKeyboard', adminSupportToolsKeyboard()], + ['tipsDashboardKeyboard', tipsDashboardKeyboard()], + ['userMainMenuKeyboard', userMainMenuKeyboard(true)], + ]; + + for (const [name, kb] of menuKeyboards) { + const ids = extractCallbackDataFromKeyboard(kb); + const duplicates = ids.filter((id, idx) => ids.indexOf(id) !== idx); + if (duplicates.length === 0) pass('Menu & Callback Audit', `${name}.no_duplicate_buttons`); + else fail('Menu & Callback Audit', `${name}.no_duplicate_buttons`, `Duplicates: ${Array.from(new Set(duplicates)).join(', ')}`); + + const missing = ids.filter((id) => !registered.has(id) && !id.startsWith('tip_') && !id.startsWith('page_') && !id.startsWith('walk_') && !id.startsWith('gw_') && !id.startsWith('tgw_dur_')); + if (missing.length === 0) pass('Menu & Callback Audit', `${name}.callbacks_registered`); + else fail('Menu & Callback Audit', `${name}.callbacks_registered`, `Missing: ${Array.from(new Set(missing)).join(', ')}`); + } + } catch (e) { + fail('Menu & Callback Audit', 'menu_callback_audit', e.message); + } + // โ”€โ”€ 7. Settings System โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ try { const freshUser = createDefaultUser({ id: -2, username: 'y', first_name: 'Y' }); diff --git a/prod-run.sh b/prod-run.sh index f6a378f..f51f692 100755 --- a/prod-run.sh +++ b/prod-run.sh @@ -453,6 +453,13 @@ if is_port_listening "$PRECHECK_PORT"; then free_port_if_conflicted "$PRECHECK_PORT" "$PID" || true fi +PRECHECK_HEALTH_URL="$(resolve_health_url)" +PRECHECK_PORT="${PRECHECK_HEALTH_URL#*://127.0.0.1:}" +PRECHECK_PORT="${PRECHECK_PORT%%/*}" +if is_port_listening "$PRECHECK_PORT"; then + free_port_if_conflicted "$PRECHECK_PORT" "$PID" || true +fi + if command -v systemctl >/dev/null 2>&1 && [[ -f "$SERVICE_FILE" ]]; then if systemctl restart "${APP_NAME}.service" 2>&1; then sleep 3