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('