diff --git a/RUNEWAGER_FUNCTIONALITY_MAP.md b/RUNEWAGER_FUNCTIONALITY_MAP.md index eb48946..bab8abc 100644 --- a/RUNEWAGER_FUNCTIONALITY_MAP.md +++ b/RUNEWAGER_FUNCTIONALITY_MAP.md @@ -1,6 +1,6 @@ # RUNEWAGER_FUNCTIONALITY_MAP.md -_Last audited: 2026-02-28_ +_Last audited: 2026-02-28 (v3.1 pass)_ _Source of truth files: `index.js`, `test/*.test.js`, scripts under `scripts/`, deployment/runtime docs in repo root._ --- @@ -48,6 +48,8 @@ Navigation is driven by inline menus plus command aliases. Persistent user/admin - Many menu-style responses use `replaceCallbackPanel(...)` to avoid stale stacked cards. - Single active menu rule: `clearOldMenus(ctx)` + menu send helpers keep only one active transient menu card per user/chat. - `to_main_menu` clears transient menu cards via `clearOldMenus(...)` before rendering persistent menu headers. +- **Group command guard middleware** intercepts commands sent in group/supergroup chats. Commands that have their own group-specific handling (`link`, `linkrunewager`, `giveaway`, `start_giveaway`, `admin`) pass through. All other commands receive a "๐Ÿ’ฌ This command works in DM" response with a deep-link button; the command handler is suppressed. This prevents onboarding, settings, promo, and other DM flows from executing in group chats. +- **`GROUP_PASSTHROUGH_COMMANDS`** set defines which commands bypass the group guard; it does NOT restrict admin access or callback queries. ## 4. User Menu Tree @@ -86,7 +88,8 @@ Navigation is driven by inline menus plus command aliases. Persistent user/admin - TestAll engine (`/testall`) runs structured diagnostics across environment, data/stores, callbacks/commands, navigation helpers, giveaway/promo/helpful-tooltips, SSHV, pendingAction timeout/label rules; summary line: `TestAll complete โ€” X passed, Y warnings, Z failures.` - `admin_cat_giveaway`: start/test/status + persistent navigation row (`Admin Dashboard`, `Main Menu`, `Cancel`). - `admin_cat_promo`: full promo manager actions + guide + persistent navigation row (`Admin Dashboard`, `Main Menu`, `Cancel`). -- `admin_cat_system`: health/version/verify/setup/backup/admin mode/testall/sshv + persistent navigation row (`Admin Dashboard`, `Main Menu`, `Cancel`). +- `admin_cat_system`: health/version/verify/setup/backup/admin mode/testall/sshv + **๐Ÿ”— Group Linking** (v3.1 โ€” opens group linking tools with return to System Tools) + persistent navigation row (`Admin Dashboard`, `Main Menu`, `Cancel`). + - `admin_sys_group_linking` callback renders the group linking panel (`renderGroupLinkingTools`) with `admin_cat_system` as the back target. - `admin_cat_tests`: bug tools + test tools + sshv shortcut + return navigation controls. - `admin_cat_support`: bug report management shortcuts + persistent navigation row (`Admin Dashboard`, `Main Menu`, `Cancel`). @@ -150,13 +153,27 @@ The bot uses `user.pendingAction.type` as its input state machine. Key families: 5. Community join prompts (channel/group flags). 6. Walkthrough progression tracking (`user.walkthrough`, onboarding milestones). +**Progress Indicator (v3.1+):** +- `onboardingProgressBar(step)` renders a visual dot bar: `โ—โ—โ—‹โ—‹โ—‹ Step 2 of 5 โ€” Link Runewager`. +- `showOnboardingPrompt(ctx, user, step)` sends the progress bar as a Markdown message (auto-deletes after 8s) before each step-specific prompt. +- `user.onboarding.completionCardShown` โ€” boolean, defaults `false`. Set to `true` on first main-menu arrival after completion. +- **Completion card** is shown once, the first time the user reaches the main menu after completing all 5 steps: friendly welcome, feature summary, and a "๐ŸŽฎ Open Menu" button. + Recovery: - `confirm_no_username` returns to username entry. - `/stuck` and `/fixaccount` provide guided recovery paths. ## 10. Group Commands -Group-aware commands include giveaway interaction and linking shortcuts: +**Group command guard (v3.1+):** All bot commands sent in a group/supergroup are intercepted by middleware and redirected to DM. Passthrough exceptions (have their own group logic): +- `/link` / `/linkrunewager` โ€” accepts inline username argument, acknowledges in group, continues in DM. +- `/giveaway` โ€” shows active giveaways for that group; admin can start a new one. +- `/start_giveaway` โ€” admin-only wizard in the group context. +- `/admin` โ€” sends brief DM-launch button in group. + +All other commands (menu, settings, promo, bonus, profile, help, status, etc.) receive: "๐Ÿ’ฌ This command works in DM. Tap below..." with a direct DM deep-link. The command handler is suppressed (next() not called). + +Group-aware commands also include giveaway interaction and linking shortcuts: - `/giveaway` (admin wizard in group context). - `/join` and `gw_join_` for participant entry. - `/eligible [id]` checks eligibility. @@ -384,3 +401,5 @@ Mandatory rules for any AI agent touching this repo: - 2026-02-26: Added deterministic 30 SC user submenu (`How It Works`, `Check My Eligibility`, `Request My Bonus`, `Check Bonus Status`) and Admin submenu (`View Pending Requests`, `Approve Bonus`, `Deny Bonus`, `View User History`, `Reset Attempts`) with manual-review copy and admin action logging to `/var/www/html/Runewager/logs/bonus_admin.log`. - 2026-02-27: Added QA tester scaffolding (`qa/context/bot_capabilities.json`, `qa/context/repo_info.json`, `qa/state/provider_status.json`, `qa/README_QA.md`), with runtime refresh via `/qa_*` commands and 10-minute provider cooldown reset. - 2026-02-27: Hardened SSHV Run prompt flow so admin text in private DM executes against active SSHV sessions if pending state desynchronizes. +- 2026-02-28: v3.1 โ€” added group command guard middleware (`GROUP_PASSTHROUGH_COMMANDS` + `bot.use` interceptor); added `onboardingProgressBar()` and progress header on each onboarding step prompt (auto-deletes after 8s); added one-time onboarding completion card (tracked via `user.onboarding.completionCardShown`); added `๐Ÿ”— Group Linking` to Admin System Tools keyboard (`admin_sys_group_linking` callback with back-to-system-tools navigation). +- 2026-02-28: PR #112 review + audit pass โ€” fixed 10 issues: (R1) `await_tip_import_batch` dedicated pending type with JSON-array router; (R2) `generate_tooltips.sh` command-substitution pollution fixed via `RUNEWAGER_APP` env var; (R3) `add_tooltip.sh` shell-injection fixed via `TOOLTIP_TEXT_ENV`/`TOOLTIP_TMP_FILE` env vars and `<<'EOF'`; (R4) `catchAllCases` test extended with multiline patterns + `CATCH_ALL_CORES` updated; (R5) `extractCommandHandlerNames` test extended with `let`/`var`/no-semicolon fixtures; (R6) typo "auto-deletes 8s" โ†’ "auto-deletes after 8s"; (A1) dead `buildGiveawayAnnouncementText(giveaway,remainingStr)` removed; (A2) simplified `buildGiveawayAnnouncementKeyboard` with wrong callback removed; (A3+A4) duplicate `bot.action('admin_cat_system')` and `bot.action('admin_cat_support')` first registrations removed. All 60 tests pass. diff --git a/add_tooltip.sh b/add_tooltip.sh index e1d5066..9360ecc 100755 --- a/add_tooltip.sh +++ b/add_tooltip.sh @@ -42,14 +42,17 @@ node -e "JSON.parse(require('fs').readFileSync('$TOOLTIPS_FILE','utf8'))" 2>/dev TOOLTIP_TEXT="${CUSTOM_TEXT:-New tooltip โ€” edit in admin panel via /tips.}" # Append new entry and get new ID using Node.js -NEW_ID=$(node - "$TOOLTIPS_FILE" < Math.max(m, Number(t.id) || 0), 0); const newId = maxId + 1; -list.push({ id: newId, text: $(node -e "process.stdout.write(JSON.stringify('$TOOLTIP_TEXT'))"), enabled: true }); -fs.writeFileSync('${TMP_FILE}', JSON.stringify(list, null, 2)); +list.push({ id: newId, text, enabled: true }); +fs.writeFileSync(tmpFile, JSON.stringify(list, null, 2)); console.log(newId); EOF ) diff --git a/generate_tooltips.sh b/generate_tooltips.sh index 9d03261..33658a1 100755 --- a/generate_tooltips.sh +++ b/generate_tooltips.sh @@ -34,9 +34,9 @@ if [[ ! -f "$APP_DIR/index.js" ]]; then fi info "Extracting DEFAULT_TIPS_LIST from index.js..." -TOOLTIP_JSON=$(node - <<'EOF' +TOOLTIP_JSON=$(RUNEWAGER_APP="$APP_DIR/index.js" node - <<'EOF' const fs = require('fs'); -const src = fs.readFileSync(process.argv[1] || 'index.js', 'utf8'); +const src = fs.readFileSync(process.env.RUNEWAGER_APP || 'index.js', 'utf8'); // Execute just the DEFAULT_TIPS_LIST block and print it as JSON const m = src.match(/const DEFAULT_TIPS_LIST\s*=\s*(\[[\s\S]+?\]);/); if (!m) { process.stderr.write('DEFAULT_TIPS_LIST not found\n'); process.exit(1); } @@ -46,7 +46,6 @@ try { console.log(JSON.stringify(list, null, 2)); } catch (e) { process.stderr.write('Parse error: ' + e.message + '\n'); process.exit(1); } EOF -node "$APP_DIR/index.js" --version 2>/dev/null || true ) || { # Fallback: emit a minimal valid tooltips.json with a placeholder warn "Could not extract tooltips from index.js โ€” writing placeholder." diff --git a/index.js b/index.js index 46798cb..62c1be6 100644 --- a/index.js +++ b/index.js @@ -259,6 +259,49 @@ bot.catch((err, ctx) => { logEvent('error', 'Unhandled bot handler error', { userId, updateType, error: err && err.message ? err.message : String(err) }); }); +// ========================= +// Group Command Guard Middleware +// Commands sent in group/supergroup chats are intercepted and redirected to DM, +// unless the command has its own group-specific handling (link, giveaway, admin, etc.) +// ========================= + +/** + * Commands that have explicit group-aware logic and should NOT be intercepted. + * All other commands sent in a group get a "DM redirect" response. + */ +const GROUP_PASSTHROUGH_COMMANDS = new Set([ + 'link', 'linkrunewager', // handled: group username inline confirm + 'giveaway', // handled: shows active giveaways for the group + 'start_giveaway', // handled: admin can start giveaway from group context + 'admin', // handled: brief DM dashboard link +]); + +bot.use(async (ctx, next) => { + const chatType = ctx.chat && ctx.chat.type; + if (chatType !== 'group' && chatType !== 'supergroup') return next(); + if (!ctx.message || !ctx.message.text) return next(); + + const text = ctx.message.text; + if (!text.startsWith('/')) return next(); + + // Extract command name, stripping bot @mention and arguments + const rawCmd = text.slice(1).split(/[\s@]/)[0].toLowerCase(); + if (GROUP_PASSTHROUGH_COMMANDS.has(rawCmd)) return next(); + + // Redirect all other commands to DM + const botUsername = ctx.botInfo && ctx.botInfo.username ? ctx.botInfo.username : ''; + const dmUrl = botUsername ? `https://t.me/${botUsername}` : null; + const keyboard = dmUrl + ? Markup.inlineKeyboard([[Markup.button.url('๐Ÿ’ฌ Open DM', dmUrl)]]) + : Markup.inlineKeyboard([]); + + await ctx.reply( + '๐Ÿ’ฌ This command works in DM. Tap below to open a private chat with me!', + { ...keyboard, reply_to_message_id: ctx.message.message_id }, + ).catch(() => {}); + // Do not call next() โ€” suppress the group command from running +}); + // ========================= // State (DB-ready interfaces) // ========================= @@ -528,6 +571,7 @@ let menuStaleRecoveries = 0; // incremented when stale menu IDs are cleared o // Human-friendly labels for pending action keys shown in timeout/error UI. const ACTION_LABELS = { await_tip_add_text: 'add tip text', + await_tip_import_batch: 'batch tooltip import', await_tip_edit_text: 'edit tip text', await_tip_settings_interval: 'tip schedule interval', await_tip_amount: 'tip amount', @@ -1420,7 +1464,7 @@ function createDefaultUser(user) { giveawayJoinedIds: new Set(), referralTag: '', badges: new Set(), - onboarding: { currentStep: 0, startedAt: Date.now(), completedAt: 0, stepTimestamps: [] }, + onboarding: { currentStep: 0, startedAt: Date.now(), completedAt: 0, stepTimestamps: [], completionCardShown: false }, miniAppLastSyncAt: 0, profileXP: 0, settings: { @@ -1542,7 +1586,8 @@ function getUser(ctx) { delete wb.optOut; } if (!user.badges) user.badges = new Set(); - if (!user.onboarding) user.onboarding = { currentStep: 0, startedAt: Date.now(), completedAt: 0, stepTimestamps: [] }; + if (!user.onboarding) user.onboarding = { currentStep: 0, startedAt: Date.now(), completedAt: 0, stepTimestamps: [], completionCardShown: false }; + if (user.onboarding.completionCardShown === undefined) user.onboarding.completionCardShown = false; if (!user.walkthrough) user.walkthrough = { currentStep: 0, completed: new Set(), started: false }; if (!user.giveawayJoinedIds) user.giveawayJoinedIds = new Set(); if (!user.profileXP) user.profileXP = 0; @@ -3634,6 +3679,7 @@ function adminSystemToolsKeyboard(user = null) { [Markup.button.callback('๐Ÿค– Verify Bot Setup', 'admin_cmd_verify_setup')], [Markup.button.callback('๐Ÿงช Drop Test (4h broadcast)', 'admin_cmd_tiptest')], [Markup.button.callback('๐Ÿ’พ Backup State', 'admin_backup_action')], + [Markup.button.callback('๐Ÿ”— Group Linking', 'admin_sys_group_linking')], [Markup.button.callback(toggleLabel, 'admin_cmd_mode_toggle')], [Markup.button.callback('โฌ…๏ธ Admin Dashboard', 'open_admin_dashboard')], [Markup.button.callback('๐Ÿ  Main Menu', 'to_main_menu'), Markup.button.callback('โŒ Cancel', 'to_main_menu')], @@ -4509,14 +4555,41 @@ function clearPendingAction(user) { user.pendingAction = null; } +/** + * Returns a compact visual progress bar string for the current onboarding step. + * Total steps: 5 (step 1โ€“5); dots filled up to the current step. + * Example: step 2 โ†’ "โ—โ—โ—‹โ—‹โ—‹ Step 2 of 5 โ€” Link Runewager" + * @param {number} step - current step number (1โ€“5) + * @returns {string} progress bar label + */ +function onboardingProgressBar(step) { + const total = 5; + const filled = Math.max(0, Math.min(step, total)); + const dots = 'โ—'.repeat(filled) + 'โ—‹'.repeat(total - filled); + const label = onboardingStepLabel(step); + return `${dots} Step ${step} of ${total} โ€” ${label}`; +} + /** * Show the appropriate onboarding step prompt instead of the main menu. * Called when a user has confirmed their age but has not yet completed onboarding. + * Prepends a progress bar header before the step-specific prompt. * @param {object} ctx - Telegraf context * @param {object} user - user state object * @param {number} step - current onboarding step (1โ€“4) */ async function showOnboardingPrompt(ctx, user, step) { + // Send progress indicator (auto-deletes after 8s to keep chat clean) + const progressText = `๐Ÿš€ *Onboarding Progress*\n${onboardingProgressBar(step)}`; + const progressMsg = await ctx.reply(progressText, { parse_mode: 'Markdown' }).catch(() => null); + if (progressMsg) { + const pChatId = progressMsg.chat.id; + const pMsgId = progressMsg.message_id; + setTimeout(async () => { + try { await ctx.telegram.deleteMessage(pChatId, pMsgId); } catch (_) { /* ignore */ } + }, 8000); + } + switch (step) { case 1: // Account setup โ€” leads through GambleCodez VIP / Discord signup flow @@ -5713,6 +5786,29 @@ bot.start(safeStepHandler('start', async (ctx) => { } // โ”€โ”€ Onboarding complete โ€” show persistent user main menu โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + + // Show a one-time completion card the first time the user reaches the main menu + if (!user.onboarding.completionCardShown) { + user.onboarding.completionCardShown = true; + const rwName = user.runewagerUsername ? `*${user.runewagerUsername}*` : 'your account'; + await ctx.reply( + `๐ŸŽ‰ *You're all set!*\n\n` + + `โ—โ—โ—โ—โ— Onboarding complete!\n\n` + + `Welcome to Runewager, ${rwName}! Here's what you can do:\n` + + `โ€ข ๐ŸŽฎ Play โ€” launch the Runewager Mini App\n` + + `โ€ข ๐ŸŽ Promos โ€” claim exclusive SC bonuses\n` + + `โ€ข ๐ŸŽ‰ Giveaways โ€” enter free SC giveaways\n` + + `โ€ข ๐Ÿ‘ฅ Referrals โ€” invite friends and earn boosts\n\n` + + `Use the menu below to explore everything. Good luck! ๐Ÿ€`, + { + parse_mode: 'Markdown', + ...Markup.inlineKeyboard([ + [Markup.button.callback('๐ŸŽฎ Open Menu', 'to_main_menu')], + ]), + }, + ).catch(() => {}); + } + await sendPersistentUserMenu(ctx, user); })); @@ -8626,50 +8722,6 @@ bot.action('admin_cat_support', async (ctx) => { }); -/** - - - * adminTestsToolsKeyboard executes its scoped Runewager logic and participates in menu/command or utility flow composition. - - - * Parameters: See the function signature for exact argument names and accepted values. - - - * Returns: Returns the computed value or a Promise resolving to the operation result; may return void for side-effect handlers. - - - * Side effects: May mutate runtime stores, pendingAction state, menu state, persistence files, logs, and callback progression. - - - * Validation/safety: Uses existing guard utilities (admin checks, input checks, path checks, cooldown checks) where applicable. - - - * Timeouts/fallbacks: Timeout and fallback behavior are controlled by the calling flow and global handler/state machine conventions. - - - * Errors: Surfaces user-facing error replies and/or logs when inputs, permissions, or dependencies are invalid. - - - * System fit: This function is part of the Runewager command/callback/state orchestration pipeline. - - - */ - - -bot.action('admin_cat_system', async (ctx) => { - if (!requireAdmin(ctx)) return; - await ctx.answerCbQuery(); - const user = getUser(ctx); - await replaceCallbackPanel(ctx, 'โš™๏ธ *System Tools*', { parse_mode: 'Markdown', ...adminSystemToolsKeyboard(user) }); -}); - -bot.action('admin_cat_support', async (ctx) => { - if (!requireAdmin(ctx)) return; - await ctx.answerCbQuery(); - await replaceCallbackPanel(ctx, '๐Ÿ›Ÿ *Support Tools*', { parse_mode: 'Markdown', ...adminSupportToolsKeyboard() }); -}); - - /** @@ -8847,6 +8899,12 @@ bot.action('admin_gw_group_linking', async (ctx) => { await renderGroupLinkingTools(ctx, 'admin_cat_giveaway'); }); +bot.action('admin_sys_group_linking', async (ctx) => { + if (!requireAdmin(ctx)) return; + await ctx.answerCbQuery(); + await renderGroupLinkingTools(ctx, 'admin_cat_system'); +}); + bot.action('admin_gw_payout_manager', async (ctx) => { if (!requireAdmin(ctx)) return; await ctx.answerCbQuery(); @@ -10910,6 +10968,40 @@ C = Existing User With Wager Requirement`); return; } + // Tips system: batch JSON import + if (action.type === 'await_tip_import_batch') { + if (!requireAdmin(ctx)) return; + if (!text) { + await ctx.reply('โŒ Input cannot be empty. Paste a JSON array of tooltip objects.'); + return; + } + let batch; + try { + batch = JSON.parse(text); + if (!Array.isArray(batch)) throw new Error('Expected a JSON array'); + } catch (e) { + await ctx.reply(`โŒ Invalid JSON: ${e.message}\n\nMust be a JSON array like:\n\`[{"text":"My tip"}]\``); + return; + } + const added = []; + for (const item of batch) { + if (!item || typeof item.text !== 'string' || !item.text.trim()) continue; + const newTip = { id: tipsStore.nextTipId, text: item.text.trim(), enabled: item.enabled !== false }; + tipsStore.tips.push(newTip); + tipsStore.nextTipId += 1; + added.push(newTip.id); + } + user.pendingAction = null; + if (added.length === 0) { + await ctx.reply('โš ๏ธ No valid tooltip entries found. Each entry needs a non-empty `text` field.'); + return; + } + persistRuntimeState(); + saveHelpfulMessages(); + await ctx.reply(`โœ… Imported ${added.length} tooltip(s). IDs: ${added.join(', ')}.`); + return; + } + // Tips system: await edited tip text if (action.type === 'await_tip_edit_text') { if (!requireAdmin(ctx)) return; @@ -11638,8 +11730,13 @@ bot.action('tips_cmd_import_batch', async (ctx) => { const user = getUser(ctx); await ctx.answerCbQuery(); clearPendingAction(user); - user.pendingAction = { type: 'await_tip_add_text', createdAt: Date.now() }; - await ctx.reply('Paste a JSON array to append tooltips (supports plain text or HTML):\n\n`/tipadd [{"text":"My Tooltip","enabled":true}]`', { parse_mode: 'Markdown' }); + user.pendingAction = { type: 'await_tip_import_batch', createdAt: Date.now() }; + await ctx.reply( + '๐Ÿ“ฅ *Batch Import Tooltips*\n\nPaste a JSON array of tooltip objects:\n\n' + + '```json\n[{"text":"Tip one","enabled":true},{"text":"Tip two"}]\n```\n\n' + + 'Each object must have a `text` field\\. `enabled` defaults to `true` if omitted\\.', + { parse_mode: 'MarkdownV2' }, + ); }); /** Build the Helpful Tooltips Settings keyboard */ @@ -12428,34 +12525,6 @@ function scheduleGiveawayRefresh(giveaway) { } } -/** Build the announcement text for a running giveaway (used for initial post and refreshes). */ -function buildGiveawayAnnouncementText(giveaway, remainingStr) { - const titleLine = giveaway.title ? `\n๐Ÿ“Œ *${escapeMarkdownV2(giveaway.title)}*\n` : ''; - const minPartLine = giveaway.minParticipants > 0 ? `โ€ข Min participants: ${giveaway.minParticipants}\n` : ''; - const reqLines = [ - giveaway.requireLinked ? 'โ€ข Linked Runewager username โœ…' : null, - giveaway.requireChannel ? 'โ€ข Joined GambleCodez channel โœ…' : null, - giveaway.requireGroup ? 'โ€ข Joined GambleCodez group โœ…' : null, - giveaway.requireAge ? 'โ€ข Age confirmed (18+) โœ…' : null, - giveaway.requireVerified ? 'โ€ข Account confirmed โœ…' : null, - giveaway.requirePromo ? 'โ€ข Claimed promo โœ…' : null, - giveaway.requireWalkthrough ? 'โ€ข Full walkthrough โœ…' : null, - ].filter(Boolean); - const reqBlock = reqLines.length > 0 ? `\n*Requirements:*\n${reqLines.join('\n')}\n` : ''; - const timeDisplay = remainingStr || `${giveaway.durationMinutes} min`; - return [ - `๐ŸŽ‰ *SC Giveaway!*${titleLine}`, - `๐Ÿ† Winners: ${giveaway.maxWinners}`, - `๐Ÿ’ฐ SC per winner: ${giveaway.scPerWinner}`, - `โฑ ${remainingStr ? 'Time remaining: ' + remainingStr : 'Duration: ' + timeDisplay}`, - `โ„น๏ธ Referrals give 2ร— boost for 7 days`, - minPartLine.trim() || null, - reqBlock.trim() || null, - `๐Ÿ‘ฅ Joined: ${giveaway.participants.size}`, - `\n๐Ÿ‘‡ Tap below to join before countdown ends!`, - ].filter(Boolean).join('\n'); -} - /** Build the inline keyboard for a running giveaway announcement. */ function buildGiveawayAnnouncementKeyboard(giveaway, botUsername) { const joinRows = []; @@ -13713,15 +13782,6 @@ function buildGiveawayAnnouncementText(giveaway) { ].join('\n'); } -function buildGiveawayAnnouncementKeyboard(giveaway, botUsername) { - const openBotUrl = botUsername ? `https://t.me/${botUsername}` : LINKS.miniAppPlay; - return Markup.inlineKeyboard([ - [Markup.button.callback('โœ… Join Giveaway', `gw_join_${giveaway.id}`)], - [Markup.button.url('๐Ÿค– Open Bot', openBotUrl)], - [Markup.button.callback('๐Ÿ‘ฅ View Participants', `tgw_participants_${giveaway.id}`)], - ]); -} - async function refreshGiveawayAnnouncement(giveaway, botUsername) { if (!giveaway || !giveaway.announcementMsgId) return; try { diff --git a/test/smoke.test.js b/test/smoke.test.js index 04902fa..dee6f28 100644 --- a/test/smoke.test.js +++ b/test/smoke.test.js @@ -223,7 +223,8 @@ function isCatchAllRegexPattern(patternSource) { const CATCH_ALL_CORES = new Set([ '.*', '.+', '(?:.*)', '(?:.+)', '(.*)', '(.+)', '(.+)?', - '(.|\n)*', '(.|\n)+', + '(.|\n)*', '(.|\n)+', // literal newline form (pre-strip) + '(.|)*', '(.|)+', // post-whitespace-strip form of the above '(\\.|[\\s\\S])*', ]); if (CATCH_ALL_CORES.has(compact) || CATCH_ALL_CORES.has(stripped)) return true; @@ -445,7 +446,10 @@ test('extractActionRegexPatterns filters all catch-all regex forms', () => { }); test('catch-all detection recognizes supported regex forms', () => { - const catchAllCases = ['.*', '^.*$', '.+', '^.+$', '(.*)', '(.+)', '(?:.*)', '(?:.+)', '^(?:.*)$', '(.+)?']; + const catchAllCases = [ + '.*', '^.*$', '.+', '^.+$', '(.*)', '(.+)', '(?:.*)', '(?:.+)', '^(?:.*)$', '(.+)?', + '(.|\n)*', '(.|\n)+', '(\\.|[\\s\\S])*', + ]; for (const c of catchAllCases) assert.equal(isCatchAllRegexPattern(c), true, `expected "${c}" to be catch-all`); // Specific patterns with capture groups are NOT catch-all for (const c of ['gw_join_(\\d+)', 'help_page_(\\d+)', 'promo_claim_(.+)', 'user_giveaways_page_(\\d+)']) { @@ -486,17 +490,21 @@ test('command handler detection supports single/double/backtick and const-driven "const CMD_ONE = 'alpha';", 'const CMD_TWO = "beta";', 'const CMD_THREE = `gamma`;', + "let CMD_FOUR = 'eta'", // no semicolon, let declaration + 'var CMD_FIVE = "theta";', // var declaration "bot.command('delta', fn);", 'bot.command("epsilon", fn);', 'bot.command(`zeta`, fn);', 'registerCommand(CMD_ONE, fn);', 'bot.command(CMD_TWO, fn);', 'registerCommand(CMD_THREE, fn);', + 'registerCommand(CMD_FOUR, fn);', + 'bot.command(CMD_FIVE, fn);', 'bot.command(`skip_${x}`, fn);', ].join('\n'); const found = extractCommandHandlerNames(fixture); - for (const expected of ['alpha', 'beta', 'gamma', 'delta', 'epsilon', 'zeta']) { + for (const expected of ['alpha', 'beta', 'gamma', 'delta', 'epsilon', 'zeta', 'eta', 'theta']) { assert.ok(found.has(expected), `Expected to find command: ${expected}`); } assert.ok(!found.has('skip_${x}'), 'Interpolated template literal should not be treated as a concrete command'); diff --git a/todolist.md b/todolist.md index 5076f0a..7120619 100644 --- a/todolist.md +++ b/todolist.md @@ -1,6 +1,6 @@ # Runewager Bot โ€” Improvement Task Board -_Last updated: 2026-02-27 โ€” v3.0 upgrade fully implemented and verified_ +_Last updated: 2026-02-28 โ€” PR #112 review fixes + codebase audit pass_ --- @@ -235,9 +235,43 @@ _Last updated: 2026-02-27 โ€” v3.0 upgrade fully implemented and verified_ - [x] **Bump version to 3.0.0** `package.json` ### Deferred to v3.1 -- [ ] Group command guard middleware (redirect non-/link group messages to DM) -- [ ] Onboarding: step progress indicator, skip options, completion card -- [ ] Content Drops rebrand: rename all "tips" copy to "Content Drops" consistently -- [ ] Move group linking to Admin Panel top-level section +- [x] Group command guard middleware โ€” `bot.use()` intercepts group commands; passthrough list: link/giveaway/admin/start_giveaway; all others redirect to DM with deep-link button. +- [x] Onboarding: step progress indicator (`onboardingProgressBar()`, auto-delete 8s) + one-time completion card (`completionCardShown` flag in onboarding schema). +- [ ] Content Drops rebrand: rename all "tips" copy to "Content Drops" consistently (low priority โ€” branding is stable as "Helpful Tooltips") +- [x] Move group linking to Admin Panel top-level section โ€” added `๐Ÿ”— Group Linking` to `adminSystemToolsKeyboard()` + `admin_sys_group_linking` action handler. - [ ] Memory eviction for inactive users (>90 days) from `userStore` when count > 10k - [ ] Modularize `index.js` into `src/` directory (requires >80% test coverage first) + +--- + +## PR #112 + AUDIT PASS (2026-02-28) + +### PR #112 Review Fixes (sourcery-ai[bot]) +- [x] **R1: `tips_cmd_import_batch` used wrong pending type** `index.js` + - Was: `await_tip_add_text` (plain text router handled it). Fixed: dedicated `await_tip_import_batch` type with JSON-array parser; proper batch reply with MarkdownV2 format prompt; router branch validates array, adds each entry, replies with IDs. +- [x] **R2: `generate_tooltips.sh` command substitution pollution** `generate_tooltips.sh` + - Was: `node "$APP_DIR/index.js" --version` inside `$(...)` block โ€” version string polluted `TOOLTIP_JSON`. Also: `process.argv[1]` undefined in `node -` stdin mode. Fixed: use `RUNEWAGER_APP="$APP_DIR/index.js"` env var; `process.env.RUNEWAGER_APP` in Node; removed stray `--version` call. +- [x] **R3: `add_tooltip.sh` shell injection via `$TOOLTIP_TEXT`** `add_tooltip.sh` + - Was: `$(node -e "...JSON.stringify('$TOOLTIP_TEXT')...")` inside heredoc โ€” single-quotes/backticks/`$` in tooltip text could break parsing or inject shell commands. Fixed: `TOOLTIP_TEXT_ENV="$TOOLTIP_TEXT"` + `TOOLTIP_TMP_FILE="$TMP_FILE"` passed as env vars; heredoc single-quoted `<<'EOF'`; Node reads from `process.env` only; `process.argv[2]` for file path (was incorrectly `argv[1]`). +- [x] **R4: `catchAllCases` test array missing multi-line patterns** `test/smoke.test.js` + - Added `'(.|\n)*'`, `'(.|\n)+'`, `'(\\.|[\\s\\S])*'` to test array. Also added `'(.|)*'`/`'(.|)+'` to `CATCH_ALL_CORES` set in `isCatchAllRegexPattern` to handle post-whitespace-strip form of literal-newline patterns. +- [x] **R5: `extractCommandHandlerNames` test missing `let`/`var` fixtures** `test/smoke.test.js` + - Added `let CMD_FOUR = 'eta'` (no semicolon) and `var CMD_FIVE = "theta"` to fixture; added `registerCommand(CMD_FOUR, fn)` and `bot.command(CMD_FIVE, fn)` usage; added `'eta'`, `'theta'` to expected-commands assertion list. +- [x] **R6: Typo in `RUNEWAGER_FUNCTIONALITY_MAP.md`** `RUNEWAGER_FUNCTIONALITY_MAP.md` + - "auto-deletes 8s" โ†’ "auto-deletes after 8s" (line ~404, v3.1 audit log entry). + +### Codebase Audit Fixes +- [x] **A1: Dead `buildGiveawayAnnouncementText` at ~12533 removed** `index.js` + - First definition `(giveaway, remainingStr)` with stale parameter API deleted. Active definition at ~13795 dynamically calculates `remainingSec` from `giveaway.endTime` and includes test-mode warning banner. +- [x] **A2: Simplified `buildGiveawayAnnouncementKeyboard` at ~13817 removed** `index.js` + - Second (simplified) definition with wrong `tgw_participants_` callback and no joinSurface logic deleted. Active definition at ~12533 (renumbered) has proper joinSurface check, DM deep-link, Details/Eligibility buttons, and Admin buttons. +- [x] **A3: Duplicate `bot.action('admin_cat_system')` at ~8710 removed** `index.js` + - First of two identical registrations deleted. Single canonical handler at ~8710 (post-removal numbering). +- [x] **A4: Duplicate `bot.action('admin_cat_support')` at ~8717 removed** `index.js` + - First of two identical registrations deleted. Single canonical handler at ~8717 (post-removal numbering). + +### Test Results +- All 60 tests pass (60/60, 0 failures) +- `node --check index.js` clean +- `bash -n generate_tooltips.sh` clean +- `bash -n add_tooltip.sh` clean