diff --git a/.env.example b/.env.example index 62a187e..ad1845b 100644 --- a/.env.example +++ b/.env.example @@ -1,7 +1,8 @@ ADMIN_IDS=YOUR_TELEGRAM_USER_ID AFFILIATE_SOURCE=GambleCodez ANNOUNCE_CHANNEL="-1002648883359" -# Set in BotFather/non-privacy mode so bot can read group messages. +BOTPRIVACYMODE="disabled" +# BOT_PRIVACY_MODE="disabled" in BotFather/non-privacy mode so bot can read group messages. BOT_PRIVACY_MODE="disabled" BOT_TOKEN= DEVICE=vps diff --git a/CLAUDE.md b/CLAUDE.md index c0db952..f0826da 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -63,7 +63,6 @@ You are the Runewager Bot Ops Commander. You manage the full lifecycle of the Ru - Use `clearOldMenus(ctx)` (or approved wrapper) for menu-card transitions to enforce single-active-menu behavior. - Keep link behavior deterministic: browser mode => external web URL only; telegram mode => startapp deep link only; Discord URLs must remain external browser links. - `/testall` updates must preserve structured category checks and final summary: `TestAll complete — X passed, Y warnings, Z failures.` -- QA tester contract: maintain `/qa_on`, `/qa_off`, `/qa_mode`, `/qa_status` behavior and keep `qa/context/bot_capabilities.json`, `qa/context/repo_info.json`, and `qa/state/provider_status.json` synchronized with runtime changes. - Referral onboarding rules must be enforced in code/tests: one-time code entry during onboarding only, no self-referral, no post-onboarding referral application, and dual 7-day boost assignment on valid referral. ## Workflow Patterns diff --git a/RUNEWAGER_FUNCTIONALITY_MAP.md b/RUNEWAGER_FUNCTIONALITY_MAP.md index 812f417..8c06eda 100644 --- a/RUNEWAGER_FUNCTIONALITY_MAP.md +++ b/RUNEWAGER_FUNCTIONALITY_MAP.md @@ -48,8 +48,6 @@ 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. -- SSHV command prompt reliability: `sshv_run_prompt` seeds `await_sshv_command` and private-DM admin text falls back to active SSHV session execution even if pending state was lost. -- QA control commands exist for tester integration: `/qa_on`, `/qa_off`, `/qa_mode user|admin`, `/qa_status`. ## 4. User Menu Tree @@ -196,7 +194,7 @@ Notes: ## 14. Permissions & Access Control - Admin identity is based on `ADMIN_IDS` env list. -- `.env.example` documents both `BOT_PRIVACY_MODE="disabled"` (BotFather non-privacy mode), plus Telegram/HTTPS/discovery keys used by runtime checks. +- `.env.example` documents both `BOT_PRIVACY_MODE="disabled"` (BotFather non-privacy mode) and compatibility `BOTPRIVACYMODE`, plus Telegram/HTTPS/discovery keys used by runtime checks. - `requireAdmin(ctx)` gates command/callback execution. - Admin mode UI toggle changes visibility of admin UI, not true authorization. - Group approvals list controls where certain broadcast/group operations target. diff --git a/index.js b/index.js index e51a34c..4a494af 100644 --- a/index.js +++ b/index.js @@ -50,17 +50,12 @@ function getDiscordLink(url) { return 'https://discord.gg/runewagers'; } -function getPlayMode(user = null) { - const mode = user && user.settings ? user.settings.playMode : user && user.playMode; - return mode === 'browser' ? 'browser' : 'miniapp'; -} - function getBrowserLink(route = 'play') { const map = { - play: 'https://runewager.com/r/GambleCodez', - profile: 'https://www.runewager.com/profile', - claim: 'https://www.runewager.com/affiliate', - affiliate: 'https://www.runewager.com/affiliate', + play: 'https://gamble-codez.com/runewager', + profile: 'https://gamble-codez.com/runewager/profile', + claim: 'https://gamble-codez.com/runewager/claim', + affiliate: 'https://gamble-codez.com/runewager/affiliate', discord: LINKS ? LINKS.rwDiscordJoin : 'https://discord.gg/runewagers', }; return map[route] || map.play; @@ -76,26 +71,8 @@ function getStartAppLink(route = 'play') { } function getPlayLink(user, route = 'play') { - return getPlayMode(user) === 'browser' ? getBrowserLink(route) : getStartAppLink(route); -} - -function getPlayButton(user = null) { - if (getPlayMode(user) === 'browser') { - return { - text: 'šŸ”µ Play & Win', - url: 'https://runewager.com/r/GambleCodez', - }; - } - return { - text: 'šŸ”µ Play & Win', - web_app: { url: 'https://t.me/RuneWager_bot/Play' }, - }; -} - -function playButtonMarkup(user = null) { - const button = getPlayButton(user); - if (button.url) return Markup.button.url(button.text, button.url); - return Markup.button.webApp(button.text, button.web_app.url); + const mode = user && user.settings ? user.settings.playMode : 'miniapp'; + return mode === 'browser' ? getBrowserLink(route) : getStartAppLink(route); } const LINKS = { @@ -128,7 +105,7 @@ const PROMO_REQUIREMENT = { const TELEGRAM_CHANNEL_ID = process.env.TELEGRAM_CHANNEL_ID || process.env.ANNOUNCE_CHANNEL || ''; const TELEGRAM_GROUP_ID = process.env.TELEGRAM_GROUP_ID || ''; const ANNOUNCE_CHANNEL = TELEGRAM_CHANNEL_ID; -const BOT_PRIVACY_MODE = String(process.env.BOT_PRIVACY_MODE || '').trim().toLowerCase(); +const BOT_PRIVACY_MODE = String(process.env.BOT_PRIVACY_MODE || process.env.BOTPRIVACYMODE || '').trim().toLowerCase(); // Group chat_id for automatic Content Drops const TIPS_GROUP = process.env.TIPS_GROUP || TELEGRAM_GROUP_ID || ''; @@ -539,19 +516,9 @@ function evaluatePendingActionTimeout(user, now = Date.now()) { return { hadPending: false, expired: false, expiredType: null }; } - const nowMs = Number(now); - const safeNow = Number.isFinite(nowMs) ? nowMs : Date.now(); - const createdRaw = user.pendingAction.createdAt; - const createdMs = Number(createdRaw); - if (!Number.isFinite(createdMs)) user.pendingAction.createdAt = safeNow; - - const normalizedCreatedAt = Number(user.pendingAction.createdAt); - if (!Number.isFinite(normalizedCreatedAt)) { - user.pendingAction.createdAt = safeNow; - return { hadPending: true, expired: false, expiredType: null }; - } + if (!user.pendingAction.createdAt) user.pendingAction.createdAt = now; - if ((safeNow - normalizedCreatedAt) <= PENDING_ACTION_TIMEOUT_MS) { + if ((now - Number(user.pendingAction.createdAt || 0)) <= PENDING_ACTION_TIMEOUT_MS) { return { hadPending: true, expired: false, expiredType: null }; } @@ -2585,8 +2552,16 @@ function promoButton() { */ -function miniAppPlayButton(user = null) { - return playButtonMarkup(user); +function playLaunchButton(user = null, textBrowser = '🌐 Open Runewager (Browser)', textMini = 'šŸŽ® Open Runewager (Mini App)', route = 'play') { + const inBrowser = (user && user.settings && user.settings.playMode === 'browser'); + if (inBrowser) { + return Markup.button.url(textBrowser, getBrowserLink(route)); + } + return Markup.button.webApp(textMini, getWebAppLink(route)); +} + +function miniAppPlayButton(user = null, route = 'play') { + return playLaunchButton(user, '🌐 Open Runewager (Browser)', 'šŸŽ® Open Runewager (Mini App)', route); } /** @@ -2644,7 +2619,7 @@ function mainMenuKeyboard(isAdminUser = false, page = 1, user = null) { // ── Page 1 — Primary Actions ─────────────────────────────────────────────── const rows = [ // Primary CTA — full width - [playButtonMarkup(user)], + [playLaunchButton(user, '🌐 Play Runewager (Browser)', 'šŸŽ® Play Runewager', 'play')], // Account category [ Markup.button.callback('āœ… Create / Verify Account', 'menu_verify_account'), @@ -3825,8 +3800,9 @@ async function sendMainMenu(ctx, user, page = 1) { /** Keyboard for the new persistent USER MAIN MENU */ function userMainMenuKeyboard(isAdminUser, user = null) { + const browserMode = user && user.settings && user.settings.playMode === 'browser'; const rows = [ - [playButtonMarkup(user)], + [playLaunchButton(user, '🌐 Play Runewager (Browser)', 'šŸŽ® Play Runewager', 'play')], [ Markup.button.callback('šŸŽ Bonuses & Rewards', 'pmenu_claim_bonus'), Markup.button.callback('šŸ‘¤ Profile & Link', 'pmenu_my_profile'), @@ -4768,7 +4744,7 @@ function referralShareHTML(code) { Daily giveaways, instant rewards, drops, and a secret new-user bonus waiting inside the bot. -Use my code ${escapeHtml(code)} when you start — we BOTH get a 2Ɨ boost on all giveaways for 7 days. +Use my code ${code} when you start — we BOTH get a 2Ɨ boost on all giveaways for 7 days. šŸ‘‰ Tap here to join Runewager @@ -7314,7 +7290,7 @@ ${lines.join('\n')}`, Markup.inlineKeyboard([[Markup.button.callback('šŸ”— Group bot.action('menu_qc_play', async (ctx) => { const user = getUser(ctx); await ctx.answerCbQuery(); - await ctx.reply('šŸŽ® Open Runewager:', Markup.inlineKeyboard([[playButtonMarkup(user)], [Markup.button.callback('ā¬…ļø Main Menu', 'to_main_menu')]])); + await ctx.reply('šŸŽ® Open Runewager:', Markup.inlineKeyboard([[miniAppPlayButton(user)], [Markup.button.callback('ā¬…ļø Main Menu', 'to_main_menu')]])); }); bot.action('menu_qc_profile', async (ctx) => { diff --git a/test/smoke.test.js b/test/smoke.test.js index 4e73ff2..4c15f99 100644 --- a/test/smoke.test.js +++ b/test/smoke.test.js @@ -208,14 +208,8 @@ function extractActionRegexPatterns(source) { * Supports `.*`, `^.*$`, and `.+` with optional grouping/anchors. */ function isCatchAllRegexPattern(patternSource) { - let compact = String(patternSource || '').replace(/\s+/g, ''); - compact = compact.replace(/^\^/, '').replace(/\$$/, ''); - while (compact.startsWith('(') && compact.endsWith(')')) compact = compact.slice(1, -1); - compact = compact.replace(/^\?:/, ''); - - if (['.*', '.+', '.*?', '.+?'].includes(compact)) return true; - if (/^\(\?:?\.?\*\)$/.test(String(patternSource || '').replace(/\s+/g, ''))) return true; - return false; + const compact = String(patternSource || '').replace(/\s+/g, ''); + return compact === '.*' || compact === '^.*$' || compact === '.+' || compact === '^.+$'; } /** @@ -558,67 +552,17 @@ test('to_main_menu clears old menus before rendering persistent menu', () => { assert.ok(block.includes('await sendPersistentUserMenu(ctx, user);'), 'Expected sendPersistentUserMenu in to_main_menu'); }); -test('play button helper is deterministic for browser and mini app modes', () => { +test('persistent user menu play button is settings-aware and supports browser mode', () => { const source = fs.readFileSync(path.resolve(__dirname, '..', 'index.js'), 'utf8'); - assert.ok(source.includes('function getPlayButton('), 'Expected unified getPlayButton helper'); - const helperStart = source.indexOf('function getPlayButton('); - const helperBlock = source.slice(helperStart, helperStart + 650); - assert.ok(helperBlock.includes("url: 'https://runewager.com/r/GambleCodez'"), 'Expected browser mode external URL'); - assert.ok(helperBlock.includes("web_app: { url: 'https://t.me/RuneWager_bot/Play' }"), 'Expected mini app web_app launch'); - assert.ok(!helperBlock.includes('startapp='), 'Expected no startapp launch in getPlayButton helper'); -}); - -test('all menu play buttons use unified getPlayButton path with no hardcoded mixed mode', () => { - const source = fs.readFileSync(path.resolve(__dirname, '..', 'index.js'), 'utf8'); - assert.ok(source.includes('function playButtonMarkup('), 'Expected helper to convert getPlayButton to Markup button'); - - const playButtonUsageCount = (source.match(/\[playButtonMarkup\(user\)\]/g) || []).length; - assert.ok(playButtonUsageCount >= 2, 'Expected both main and persistent menus to use unified play button helper'); - - const quickStart = source.indexOf("bot.action('menu_qc_play'"); - assert.ok(quickStart >= 0, 'Expected quick-play callback'); - const quickBlock = source.slice(quickStart, quickStart + 350); - assert.ok(quickBlock.includes('playButtonMarkup(user)'), 'Expected quick-play to use unified play button helper'); -}); - -test('settings play mode toggle persists, clears old menus, and re-renders settings', () => { - const source = fs.readFileSync(path.resolve(__dirname, '..', 'index.js'), 'utf8'); - const start = source.indexOf("bot.action('settings_toggle_playmode'"); - assert.ok(start >= 0, 'Expected settings_toggle_playmode callback'); + assert.ok(source.includes('function playLaunchButton('), 'Expected shared playLaunchButton helper'); + const helperStart = source.indexOf('function playLaunchButton('); + const helperBlock = source.slice(helperStart, helperStart + 700); + assert.ok(helperBlock.includes('Markup.button.url'), 'Expected browser-mode URL launch button'); + assert.ok(helperBlock.includes('Markup.button.webApp'), 'Expected mini-app webApp launch button'); + + const start = source.indexOf('function userMainMenuKeyboard('); + assert.ok(start >= 0, 'Expected userMainMenuKeyboard'); const block = source.slice(start, start + 500); - assert.ok(block.includes('persistRuntimeState();'), 'Expected play mode toggle to persist immediately'); - assert.ok(block.includes('await clearOldMenus(ctx, user);'), 'Expected play mode toggle to clear stale menus before re-render'); - assert.ok(block.includes('await sendSettingsMenu(ctx, user);'), 'Expected play mode toggle to re-render settings menu'); -}); - - -test('load_tooltips.sh exists and targets system tooltip json path', () => { - const script = fs.readFileSync(path.resolve(__dirname, '..', 'load_tooltips.sh'), 'utf8'); - assert.ok(script.includes('/var/www/html/Runewager/data/tooltips.json'), 'Expected tooltips output path'); - assert.ok(script.includes('āœ… Tooltips loaded successfully'), 'Expected success message'); -}); - -test('30 SC user submenu includes required manual-review actions and copy', () => { - const source = fs.readFileSync(path.resolve(__dirname, '..', 'index.js'), 'utf8'); - for (const token of ['w30_menu_how', 'w30_menu_eligibility', 'w30_menu_request', 'w30_my_status']) { - assert.ok(source.includes(token), `Expected 30 SC menu callback: ${token}`); - } - assert.ok(source.includes('Eligibility is reviewed manually by GambleCodez.'), 'Expected manual eligibility copy'); - assert.ok(source.includes('Your request has been submitted to GambleCodez for manual review.'), 'Expected request submission copy'); -}); - -test('30 SC admin submenu and log sink are wired', () => { - const source = fs.readFileSync(path.resolve(__dirname, '..', 'index.js'), 'utf8'); - assert.ok(source.includes("bot.action('w30_admin_menu'"), 'Expected admin bonus submenu callback'); - for (const label of ['View Pending Requests', 'Approve Bonus', 'Deny Bonus', 'View User History', 'Reset Attempts']) { - assert.ok(source.includes(label), `Expected admin bonus menu label: ${label}`); - } - assert.ok(source.includes('/var/www/html/Runewager/logs/bonus_admin.log'), 'Expected bonus admin log file path'); -}); - -test('BOTPRIVACYMODE compatibility env var is removed', () => { - const source = fs.readFileSync(path.resolve(__dirname, '..', 'index.js'), 'utf8'); - const envExample = fs.readFileSync(path.resolve(__dirname, '..', '.env.example'), 'utf8'); - assert.ok(!source.includes('BOTPRIVACYMODE'), 'Expected BOTPRIVACYMODE removed from runtime env parsing'); - assert.ok(!envExample.includes('BOTPRIVACYMODE'), 'Expected BOTPRIVACYMODE removed from .env.example'); + assert.ok(block.includes('playLaunchButton(user'), 'Expected settings-aware play launch helper in persistent menu'); + assert.ok(block.includes('Play Runewager (Browser)'), 'Expected browser-mode label for persistent play button'); }); diff --git a/test/unit.test.js b/test/unit.test.js index 48eeec0..09d1810 100644 --- a/test/unit.test.js +++ b/test/unit.test.js @@ -237,17 +237,12 @@ function getDiscordLink(url) { return 'https://discord.gg/runewagers'; } -function getPlayMode(user = null) { - const mode = user && user.settings ? user.settings.playMode : user && user.playMode; - return mode === 'browser' ? 'browser' : 'miniapp'; -} - function getBrowserLink(route = 'play') { const map = { - play: 'https://runewager.com/r/GambleCodez', - profile: 'https://www.runewager.com/profile', - claim: 'https://www.runewager.com/affiliate', - affiliate: 'https://www.runewager.com/affiliate', + play: 'https://gamble-codez.com/runewager', + profile: 'https://gamble-codez.com/runewager/profile', + claim: 'https://gamble-codez.com/runewager/claim', + affiliate: 'https://gamble-codez.com/runewager/affiliate', }; return map[route] || map.play; } @@ -257,20 +252,8 @@ function getStartAppLink(route = 'play') { } function getPlayLink(user, route = 'play') { - return getPlayMode(user) === 'browser' ? getBrowserLink(route) : getStartAppLink(route); -} - -function getPlayButton(user = null) { - if (getPlayMode(user) === 'browser') { - return { - text: 'šŸ”µ Play & Win', - url: 'https://runewager.com/r/GambleCodez', - }; - } - return { - text: 'šŸ”µ Play & Win', - web_app: { url: 'https://t.me/RuneWager_bot/Play' }, - }; + const mode = user && user.settings ? user.settings.playMode : 'miniapp'; + return mode === 'browser' ? getBrowserLink(route) : getStartAppLink(route); } // --------------------------------------------------------------------------- @@ -466,33 +449,21 @@ test('pending action timeout uses fallback type label when missing', () => { -test('browser mode returns external runewager affiliate link only for play', () => { +test('browser mode returns external gamble-codez link only', () => { const user = { settings: { playMode: 'browser' } }; const out = getPlayLink(user, 'play'); - assert.equal(out, 'https://runewager.com/r/GambleCodez'); + assert.match(out, /^https:\/\/gamble-codez\.com\//); assert.ok(!out.includes('startapp=')); assert.ok(!out.includes('t.me/')); }); -test('mini app mode returns startapp mini-app link for routes', () => { +test('telegram mode returns startapp mini-app link only', () => { const user = { settings: { playMode: 'miniapp' } }; const out = getPlayLink(user, 'profile'); assert.match(out, /^https:\/\/t\.me\//); assert.ok(out.includes('startapp=profile')); }); -test('getPlayButton enforces deterministic browser and mini app launch metadata', () => { - const browser = getPlayButton({ settings: { playMode: 'browser' } }); - assert.equal(browser.text, 'šŸ”µ Play & Win'); - assert.equal(browser.url, 'https://runewager.com/r/GambleCodez'); - assert.equal(browser.web_app, undefined); - - const mini = getPlayButton({ settings: { playMode: 'miniapp' } }); - assert.equal(mini.text, 'šŸ”µ Play & Win'); - assert.deepEqual(mini.web_app, { url: 'https://t.me/RuneWager_bot/Play' }); - assert.equal(mini.url, undefined); -}); - test('discord links remain external and unwrap telegram wrappers', () => { const wrapped = 'https://t.me/iv?url=https%3A%2F%2Fdiscord.gg%2Frunewagers'; const out = getDiscordLink(wrapped); @@ -503,21 +474,12 @@ test('discord links remain external and unwrap telegram wrappers', () => { -function escapeHtml(text) { - return String(text || '') - .replace(/&/g, '&') - .replace(//g, '>') - .replace(/"/g, '"') - .replace(/'/g, '''); -} - function referralShareHTML(code) { return `🚨 YO! I’m farming free SC on Runewager and you need to get in NOW. Daily giveaways, instant rewards, drops, and a secret new-user bonus waiting inside the bot. -Use my code ${escapeHtml(code)} when you start — we BOTH get a 2Ɨ boost on all giveaways for 7 days. +Use my code ${code} when you start — we BOTH get a 2Ɨ boost on all giveaways for 7 days. šŸ‘‰ Tap here to join Runewager @@ -588,26 +550,3 @@ test('valid referral applies boost expiry shape', () => { assert.equal(out.ownerId, 1); assert.ok(out.expiresAt > 1700000000000); }); - - -test('manual bonus policy allows requests without auto wager/API checks', () => { - function checkBonusEligibilityLike(user) { - if (user.needsUsername) return { ok: false }; - if (user.status === 'bonus_sent') return { ok: false }; - if (user.status === 'pending' || user.status === 'approved') return { ok: false }; - if (user.attempts >= 3) return { ok: false }; - return { ok: true }; - } - - assert.equal(checkBonusEligibilityLike({ status: 'none', attempts: 0 }).ok, true); - assert.equal(checkBonusEligibilityLike({ status: 'denied', attempts: 2 }).ok, true); - assert.equal(checkBonusEligibilityLike({ status: 'bonus_sent', attempts: 1 }).ok, false); - assert.equal(checkBonusEligibilityLike({ status: 'none', attempts: 3 }).ok, false); -}); - - -test('referralShareHTML escapes HTML-sensitive referral codes', () => { - const html = referralShareHTML(''); - assert.ok(!html.includes('