-
Notifications
You must be signed in to change notification settings - Fork 0
Add menu lifecycle, pending-action timeout, referral onboarding, group-linking tools, Discord/link helpers, and tests #102
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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'; | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. question (bug_risk): Consider preserving the previous The previous implementation used
Comment on lines
73
to
+74
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. issue: Revised play-mode resolution ignores legacy/user-level Previously const mode = user && user.settings
? (user.settings.playMode ?? user.playMode)
: (user && user.playMode) || 'miniapp';If |
||
| 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) { | ||
|
Comment on lines
+519
to
+521
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. issue (bug_risk): The simplified timeout calculation can misbehave when Previously we normalized
Comment on lines
+519
to
+521
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. issue (bug_risk): The simplified timeout logic assumes Previously, both
If const nowMs = Number(now);
const safeNow = Number.isFinite(nowMs) ? nowMs : Date.now();
if (!user.pendingAction.createdAt) {
user.pendingAction.createdAt = safeNow;
}
const createdMs = Number(user.pendingAction.createdAt);
if (!Number.isFinite(createdMs)) {
user.pendingAction.createdAt = safeNow;
return { hadPending: true, expired: false, expiredType: null };
}
if ((safeNow - createdMs) <= 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'; | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nitpick:
|
||
| 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 <b>${escapeHtml(code)}</b> when you start — we BOTH get a <b>2× boost on all giveaways for 7 days</b>. | ||
| Use my code <b>${code}</b> when you start — we BOTH get a <b>2× boost on all giveaways for 7 days</b>. | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🚨 issue (security): Removing The previous |
||
|
|
||
| 👉 <a href="https://t.me/RunewagerBot">Tap here to join Runewager</a> | ||
|
|
||
|
|
@@ -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) => { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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(/\$$/, ''); | ||
|
Comment on lines
210
to
-212
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. issue: Catch-all regex detection in the smoke test is now too narrow and may miss real catch-all patterns. The previous Consider either restoring the normalization (strip |
||
| 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'); | ||
|
Comment on lines
+555
to
+564
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. suggestion (testing): Play button smoke test doesn’t fully exercise all callsites and routes of the new playLaunchButton helper The new helper is also used by the quick‑play callback and on other routes that aren’t covered here. To make this a stronger guard on the new behavior, please:
Suggested implementation: test('persistent user menu play button is settings-aware and supports browser mode', () => {
const source = fs.readFileSync(path.resolve(__dirname, '..', 'index.js'), 'utf8');
// Ensure quick-play callback is wired through the new helper(s)
assert.match(
source,
/menu_qc_play[\s\S]*?(playLaunchButton|miniAppPlayButton)/,
'menu_qc_play should delegate to playLaunchButton or miniAppPlayButton'
);
// Ensure profile route is wired through the new helper(s)
assert.match(
source,
/['"`]profile['"`][\s\S]*?(playLaunchButton|miniAppPlayButton)/,
'profile route should delegate to playLaunchButton or miniAppPlayButton'
);
// Ensure claim route is wired through the new helper(s)
assert.match(
source,
/['"`]claim['"`][\s\S]*?(playLaunchButton|miniAppPlayButton)/,
'claim route should delegate to playLaunchButton or miniAppPlayButton'
);
|
||
| 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'); | ||
| }); | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nitpick (typo): Consider clarifying the phrase "and compatibility
BOTPRIVACYMODE" by adding a noun such as "alias".Here “compatibility” is used without a noun, which reads a bit incomplete. Please rephrase to something like “and compatibility alias
BOTPRIVACYMODE” or “and its compatibility aliasBOTPRIVACYMODE” to improve the grammar while keeping the meaning.