diff --git a/deploy.sh b/deploy.sh index f4a5b36..b974dc3 100755 --- a/deploy.sh +++ b/deploy.sh @@ -89,9 +89,18 @@ trap '_on_error "$LINENO"' ERR REMOTE_HASH=$(git ls-remote origin -h refs/heads/main 2>/dev/null | cut -f1 || true) LOCAL_HASH=$(git rev-parse HEAD 2>/dev/null || true) if [[ -n "$REMOTE_HASH" && "$REMOTE_HASH" = "$LOCAL_HASH" ]]; then - say "Already running latest code ($(git rev-parse --short HEAD)) — skipping deploy." - send_admin "✅ Bot is already running the latest version (commit: $(git rev-parse --short HEAD))." - exit 0 + ACTIVE="unknown" + if command -v systemctl >/dev/null 2>&1; then + ACTIVE="$(systemctl is-active "${APP_NAME}.service" 2>/dev/null || echo unknown)" + fi + if [[ "$ACTIVE" == "active" ]]; then + say "Already running latest code ($(git rev-parse --short HEAD)) — skipping deploy." + send_admin "✅ Bot is already running the latest version (commit: $(git rev-parse --short HEAD))." + exit 0 + else + warn "Code is up to date but service status is: $ACTIVE — continuing deploy to (re)start service." + send_admin "ℹ️ Code is up to date but service status is: $ACTIVE — continuing deploy to (re)start service." + fi fi # Resolve human-readable source label for notifications diff --git a/index.js b/index.js index 037f648..619c439 100644 --- a/index.js +++ b/index.js @@ -101,11 +101,19 @@ async function sendIntroGif(ctx, caption, extra = {}) { const COPY = { intro: - '👋 Welcome to Runewager! This platform uses SC (Sweepstakes Coins) for prize-based play and instant crypto prize redemption. No gambling language, no real-money wagering, and no deposit claims. Worldwide users are welcome.', + '👋 Welcome to Runewager! This platform uses SC (Sweepstakes Coins) for prize-based play and instant crypto prize redemption. No gambling language, no real-money wagering, and no deposit claims. Runewager is 100% FREE to play and accepts players worldwide.', ageGate: - 'Before continuing, please confirm you are 18+ and eligible to participate in sweepstakes-style prize activities.', + '🌍 *Runewager is 100% free to play and accepts players worldwide.*\n\n' + + 'No real-money wagering. No deposits. No gambling. Just sweepstakes-style SC (Sweepstakes Coin) fun with instant crypto prize redemption.\n\n' + + 'Before continuing, please confirm you are 18+ and eligible to participate in sweepstakes-style prize activities in your region.', prizeExplanation: - 'Great! Here\'s how Runewager works:\n• SC = Sweepstakes Coins used for prize redemption\n• No real-money wagering\n• Prize redemption is instant crypto\n• New users can claim a 3.5 SC bonus using the promo code\n• Sign up under GambleCodez to unlock all rewards and giveaways', + 'Great! Here\'s how Runewager works:\n' + + '• SC = Sweepstakes Coins used for prize redemption\n' + + '• 100% free to play — no deposits, no real-money wagering\n' + + '• Accepts players worldwide 🌍\n' + + '• Prize redemption is instant crypto\n' + + '• New users can claim a 3.5 SC bonus using the promo code\n' + + '• Sign up under GambleCodez to unlock all rewards and giveaways', // Discord web-link step is shown here as an EXTERNAL link only. // Runewager's own Discord bot handles the actual code confirmation — our Telegram bot does nothing. accountSetup: @@ -216,12 +224,30 @@ function runCmd(cmd, args, cwd, timeoutMs = 60000) { const walkthroughCatalog = buildWalkthroughCatalog(); const giveawayFeatureList = buildGiveawayFeatureList(); +const AFFILIATE_REMINDER_TEXT = + '🔔 *Affiliate Reminder:* When signing up on Runewager, make sure you enter *GambleCodez* as your affiliate.\n' + + 'If you missed it, you can still continue — just make sure your Runewager username is correct.'; + +// Words that are clearly not usernames — used to filter accidental text during username input +const NON_USERNAME_WORDS = new Set([ + 'help', 'hi', 'hello', 'hey', 'back', 'cancel', 'stop', 'start', 'menu', + 'yes', 'no', 'ok', 'okay', 'done', 'next', 'skip', 'exit', 'quit', '?', 'thanks', + 'thank', 'please', 'go', 'home', 'main', 'more', 'info', 'test', 'bye', 'nope', +]); + const WAGER_BONUS_RULES_TEXT = - '🎯 30 SC Wager Bonus — Eligibility Requirements\n\n' - + '✅ You must be registered under the GambleCodez affiliate\n' - + '✅ You must have wagered 3,000 SC in any rolling 7-day period\n' - + '✅ This bonus is once per account (max 3 requests total)\n' - + '✅ Admins manually credit the 30 SC on Runewager — the bot does not send it\n\n' + '🎯 *GambleCodez 30 SC Bonus — Full Details*\n\n' + + `${AFFILIATE_REMINDER_TEXT}\n\n` + + '*Requirements:*\n' + + '✅ Must be under the *GambleCodez affiliate*\n' + + '✅ Must wager *3,000 SC* in any rolling 7-day period\n' + + '✅ Bonus is *once per account/IP*\n' + + '✅ You may submit *up to 3 verification attempts*\n\n' + + '*Process:*\n' + + '• Bonus amount: *30 SC*\n' + + '• Admin manually reviews and sends the bonus on Runewager\n' + + '• Admin is pinged when you submit a verification request\n' + + '• The bot does NOT send SC — admin credits manually\n\n' + '⚠️ Do not message admins repeatedly. Requests are reviewed in order.'; const WAGER_BONUS_IN_FLIGHT_STATUSES = new Set(['pending', 'approved']); @@ -671,14 +697,21 @@ async function submitBonusRequest(ctx, user) { }); const wb = user.wagerBonus; + const attemptsLeft = 3 - wb.attempts; await ctx.reply( - `✅ Your request has been submitted. Admin will review and manually credit your 30 SC bonus on Runewager.\n\nAttempt ${wb.attempts}/3.`, + `✅ Your request has been submitted!\n\n` + + `${AFFILIATE_REMINDER_TEXT}\n\n` + + `• You must wager 3,000 SC in any rolling 7-day period\n` + + `• Bonus is once per account/IP\n` + + `• Attempt ${wb.attempts}/3 used (${attemptsLeft} remaining)\n\n` + + `Admin has been notified and will manually review and credit your 30 SC bonus on Runewager.`, + { parse_mode: 'Markdown' }, ); const handle = user.tgUsername ? `@${user.tgUsername}` : `user${user.id}`; await notifyAdminsPlain( - `🎯 New 30 SC bonus request\n` - + `From: ${handle} (ID: ${user.id})\n` + `🎯 *New 30 SC bonus request — admin action required*\n` + + `From: ${handle} (TG ID: ${user.id})\n` + `Runewager: ${user.runewagerUsername || '(not linked)'}\n` + `7-day wager declared: ${wb.wager7dayTotal} SC\n` + `Attempts: ${wb.attempts}/3\n` @@ -700,10 +733,13 @@ async function showBonusStatus(ctx, user) { bonus_sent: '🎉 Bonus already received', }; const wagerDisplay = wb.wager7dayTotal > 0 ? `${wb.wager7dayTotal.toLocaleString()} SC` : '(not recorded)'; + const attemptsLeft = 3 - (wb.attempts || 0); const lines = [ '🎯 30 SC Wager Bonus — Your Status', '', - `Attempts used: ${wb.attempts}/3`, + AFFILIATE_REMINDER_TEXT, + '', + `Attempts used: ${wb.attempts}/3 (${attemptsLeft} remaining)`, `Status: ${statusLabels[wb.status] || wb.status}`, `Runewager username: ${user.runewagerUsername || '(not linked)'}`, `Affiliate: ${wb.affiliateSource || 'GambleCodez'}`, @@ -719,10 +755,11 @@ async function showBonusStatus(ctx, user) { lines.push('', 'This is a once-per-account bonus. No further requests are accepted.'); } const canRequest = wb.attempts < 3 && !isWagerBonusRequestLocked(wb.status); - const kb = canRequest - ? Markup.inlineKeyboard([[Markup.button.callback('🎯 Request Bonus', 'w30_request_start')]]) - : undefined; - await ctx.reply(lines.join('\n'), kb); + const kbRows = []; + if (canRequest) kbRows.push([Markup.button.callback('🎯 Request Bonus', 'w30_request_start')]); + kbRows.push([Markup.button.callback('ℹ️ Tell me more about the 30 SC bonus', 'w30_bonus_info')]); + kbRows.push([Markup.button.callback('⬅️ Main Menu', 'to_main_menu')]); + await ctx.reply(lines.join('\n'), { parse_mode: 'Markdown', ...Markup.inlineKeyboard(kbRows) }); } function signupButton() { @@ -832,6 +869,7 @@ function mainMenuKeyboard(isAdminUser = false, page = 1, user = null) { } function ageGateKeyboard() { + // Note: parse_mode: 'Markdown' must be passed with this keyboard since COPY.ageGate uses Markdown return Markup.inlineKeyboard([ [Markup.button.callback('✅ I am 18+', 'age_yes')], [Markup.button.callback('❌ I am under 18', 'age_no')], @@ -865,21 +903,25 @@ function promoText() { function wager30AdminKeyboard() { return Markup.inlineKeyboard([ - [Markup.button.callback('📋 Pending Requests', 'w30_admin_pending')], - [Markup.button.callback('✅ Approve Request', 'w30_admin_approve_pick')], - [Markup.button.callback('📤 Mark Bonus Sent', 'w30_admin_sent_pick')], + [Markup.button.callback('📋 Open (Pending) Requests', 'w30_admin_pending')], + [Markup.button.callback('✅ Completed Requests', 'w30_admin_completed')], + [Markup.button.callback('👍 Approve Request', 'w30_admin_approve_pick')], [Markup.button.callback('❌ Deny Request', 'w30_admin_deny_pick')], + [Markup.button.callback('📤 Mark Tip Sent', 'w30_admin_sent_pick')], + [Markup.button.callback('🔗 Add Username Manually', 'w30_admin_link_username')], [Markup.button.callback('➕ Add Manual Record (bonus_sent)', 'w30_admin_add_pick')], [Markup.button.callback('🔍 Lookup User', 'w30_admin_lookup')], [Markup.button.callback('📊 Stats', 'w30_admin_stats')], [Markup.button.callback('♻️ Reset User', 'w30_admin_reset')], + [Markup.button.callback('⬅️ Return to Admin Menu', 'open_admin_dashboard')], ]); } function wagerReminderKeyboard() { return Markup.inlineKeyboard([ [Markup.button.callback('🎯 Request My 30 SC Bonus', 'w30_request_start')], - [Markup.button.callback('ℹ️ Bonus Rules', 'w30_rules')], + [Markup.button.callback('ℹ️ Tell me more about the 30 SC bonus', 'w30_bonus_info')], + [Markup.button.callback('📋 Bonus Rules', 'w30_rules')], ]); } @@ -1137,11 +1179,12 @@ function userMainMenuText(user) { return ( `👋 Hey ${name}! Here's your Runewager menu.\n\n` + (statusLine ? `${statusLine}\n\n` : '') + + `🌍 *Runewager is 100% FREE to play — worldwide access, no deposits.*\n\n` + `🎮 *Play Now* — Open the Runewager Mini App\n` - + `🎁 *Claim Bonus* — New user SC bonus & 30 SC wager bonus\n` - + `📊 *My Profile* — View your linked account & stats\n` - + `🏆 *Giveaways* — Active SC giveaways\n` - + `❓ *Help / Commands* — Full command reference` + + `🎁 *Claim Bonus* — 3.5 SC new-user bonus & 30 SC wager bonus\n` + + `📊 *My Profile* — Linked account, XP, badges & referrals\n` + + `🏆 *Giveaways* — Active SC giveaways & eligibility\n` + + `❓ *Help / Commands* — Command booklet & bug reports` ); } @@ -1178,6 +1221,8 @@ function adminMainMenuKeyboard() { [Markup.button.callback('🎉 Start Giveaway', 'pamenu_start_giveaway')], [Markup.button.callback('🎁 Active Giveaways', 'pamenu_active_giveaways')], [Markup.button.callback('🛠 Tools', 'pamenu_tools')], + [Markup.button.callback('📖 Admin Commands Reference', 'pamenu_admin_help')], + [Markup.button.callback('🐞 Bug Reports', 'pamenu_bug_reports')], [Markup.button.callback('↩ Back to User Menu', 'pamenu_back_user')], ]); } @@ -1693,84 +1738,94 @@ function persistAnalytics() { } async function configureBotSurface() { + // ── User commands visible in ALL chats (Telegram's global default) ────────── const globalCommands = [ - { command: 'start', description: 'Start / resume onboarding' }, - { command: 'menu', description: 'Open main menu' }, - { command: 'help', description: 'Help and support' }, - { command: 'play', description: 'Launch Runewager Mini App' }, - { command: 'profile', description: 'View profile and badges' }, - { command: 'status', description: 'Quick status summary' }, - { command: 'promo', description: 'View current promo code and bonus' }, - { command: 'signup', description: 'Sign up link for Runewager' }, - { command: 'affiliate', description: 'Open affiliate / promo entry page' }, - { command: 'discord', description: 'Open Runewager Discord' }, - { command: 'referral', description: 'Get your referral link' }, - { command: 'walkthrough', description: 'Guided walkthrough (35 steps)' }, - { command: 'linkrunewager', description: 'Link your Runewager username' }, - { command: 'leaderboard', description: 'Bot activity leaderboard' }, - { command: 'bonus', description: 'View 30 SC bonus status / request bonus' }, - { command: 'bugreport', description: 'Report a bug or issue' }, - { command: 'commands', description: 'Help guide (alias for /help)' }, - { command: 'settings', description: 'Your preferences (play mode, tooltips, quick commands)' }, + { command: 'start', description: 'Start or resume onboarding' }, + { command: 'menu', description: 'Open your main menu' }, + { command: 'help', description: 'Help guide, commands & bug report' }, + { command: 'commands', description: 'Full command reference (alias for /help)' }, + { command: 'play', description: 'Launch Runewager Mini App inside Telegram' }, + { command: 'status', description: 'Quick account status — 18+, username, promo, onboarding' }, + { command: 'profile', description: 'View your profile, XP, badges, referral code' }, + { command: 'linkrunewager', description: 'Link (or update) your Runewager username' }, + { command: 'bonus', description: 'View / request your 30 SC wager bonus' }, + { command: 'promo', description: 'View the current SC promo code (3.5 SC new-user bonus)' }, + { command: 'signup', description: 'Get the Runewager sign-up link under GambleCodez affiliate' }, + { command: 'affiliate', description: 'Open the affiliate / promo code entry page' }, + { command: 'discord', description: 'Runewager Discord join & code-link channel links' }, + { command: 'referral', description: 'Get your personal referral invite link' }, + { command: 'walkthrough', description: '35-step guided account setup tutorial' }, + { command: 'leaderboard', description: 'Referral leaderboard (top referrers)' }, + { command: 'leaderboard_weekly', description: 'Most active users in the last 7 days' }, + { command: 'giveaway', description: 'View active SC giveaways and eligibility' }, + { command: 'settings', description: 'Your preferences: play mode, tooltips, quick commands' }, + { command: 'bugreport', description: 'Report a bug or issue to the admin team' }, ]; + + // ── Additional commands only shown in private DM chats ────────────────────── const privateCommands = globalCommands.concat([ - { command: 'cancel', description: 'Cancel pending input' }, - { command: 'claim_history', description: 'View promo claim history' }, + { command: 'cancel', description: 'Cancel any pending input flow and go back to menu' }, + { command: 'claim_history', description: 'View your promo claim history' }, ]); - // Group commands: all commands users will want to type in a group chat. - // Telegram shows this list when a user types / in the group. + + // ── Commands shown when user types / in a GROUP chat ─────────────────────── const groupCommands = [ + { command: 'start', description: 'Set up your account (DM flow)' }, { command: 'play', description: 'Launch Runewager Mini App' }, - { command: 'start', description: 'Start / link your account (opens in DM)' }, { command: 'signup', description: 'Sign up under GambleCodez affiliate' }, - { command: 'promo', description: 'View current promo code and bonus' }, - { command: 'leaderboard', description: 'Bot activity leaderboard' }, - { command: 'referral', description: 'Get your referral link' }, - { command: 'status', description: 'Quick account status' }, - { command: 'profile', description: 'View your profile' }, + { command: 'promo', description: 'View current promo code' }, { command: 'bonus', description: 'View 30 SC bonus status' }, { command: 'linkrunewager', description: 'Link your Runewager username' }, - { command: 'discord', description: 'Open Runewager Discord' }, - { command: 'help', description: 'Help and commands' }, + { command: 'status', description: 'Quick account status' }, + { command: 'profile', description: 'View your profile' }, + { command: 'leaderboard', description: 'Referral leaderboard' }, + { command: 'referral', description: 'Get your referral link' }, { command: 'giveaway', description: 'View / join active giveaways' }, - { command: 'affiliate', description: 'Open affiliate / promo entry page' }, + { command: 'discord', description: 'Runewager Discord links' }, + { command: 'affiliate', description: 'Affiliate / promo entry page' }, + { command: 'help', description: 'Help guide & commands' }, { command: 'bugreport', description: 'Report a bug or issue' }, ]; + await bot.telegram.setMyCommands(globalCommands, { scope: { type: 'default' } }).catch(() => {}); await bot.telegram.setMyCommands(privateCommands, { scope: { type: 'all_private_chats' } }).catch(() => {}); await bot.telegram.setMyCommands(groupCommands, { scope: { type: 'all_group_chats' } }).catch(() => {}); - // Sequential per-admin command registration — order matters; disable lint rule + + // ── Admin-specific commands (shown in their private DM only) ──────────────── + const adminCommands = [ + ...privateCommands, + { command: 'admin', description: 'Open full admin dashboard' }, + { command: 'admin_help', description: 'Admin command reference guide (page 6 of help booklet)' }, + { command: 'admin_dashboard', description: 'Live stats: users, promo claims, giveaways, errors' }, + { command: 'wager30_admin', description: '30 SC bonus admin panel (approve/deny/sent/add/reset)' }, + { command: 'giveaway', description: 'Create / manage SC giveaways' }, + { command: 'start_giveaway', description: 'Start giveaway wizard (alias)' }, + { command: 'setpromo', description: 'Set or update the active promo display message' }, + { command: 'boost_referrals', description: 'Activate referral boost: /boost_referrals [hours] [multiplier]' }, + { command: 'whois', description: 'Look up any user by TG ID or Runewager username' }, + { command: 'bonusstatus', description: 'View full bonus state for a user' }, + { command: 'refreshuser', description: 'Force-migrate a user to the latest schema' }, + { command: 'bugreports', description: 'View all open bug reports' }, + { command: 'exportbugs', description: 'Export all open bug reports as plain text' }, + { command: 'resolvebug', description: 'Mark a bug report resolved: /resolvebug ' }, + { command: 'adminmode', description: 'Toggle admin bypass mode: /adminmode on|off' }, + { command: 'on', description: 'Enable admin bypass mode (test as a regular user)' }, + { command: 'off', description: 'Disable admin bypass mode (restore admin checks)' }, + { command: 'testall', description: 'Run full static self-test — shows PASS/FAIL for every feature' }, + { command: 'testgiveaway', description: 'Run fake giveaway end-to-end test (no real SC)' }, + { command: 'health', description: 'Show live health endpoint data' }, + { command: 'version', description: 'Bot version, Node.js version, uptime' }, + { command: 'admin_backup', description: 'Snapshot runtime state to a dated backup file' }, + { command: 'deploy', description: 'Trigger a live deploy from the latest git commit' }, + { command: 'deploy_status', description: 'View last deploy result and timing' }, + { command: 'logs', description: 'Show last N lines of the bot log file' }, + { command: 'admin_notify', description: 'Send a direct admin notification message' }, + ]; + // eslint-disable-next-line no-restricted-syntax for (const adminId of ADMIN_IDS) { // eslint-disable-next-line no-await-in-loop - await bot.telegram.setMyCommands([ - ...privateCommands, - { command: 'admin', description: 'Admin dashboard (full)' }, - { command: 'admin_help', description: 'Admin help guide with all commands' }, - { command: 'admin_dashboard', description: 'Live user & promo dashboard' }, - { command: 'giveaway', description: 'Create / manage giveaways' }, - { command: 'start_giveaway', description: 'Start SC giveaway wizard (alias)' }, - { command: 'bonus', description: '30 SC bonus: pending / approve / deny / sent / add' }, - { command: 'wager30_admin', description: '30 SC bonus admin panel (alias)' }, - { command: 'admin_backup', description: 'Create runtime state backup snapshot' }, - { command: 'boost_referrals', description: 'Activate referral boost' }, - { command: 'setpromo', description: 'Set promo display message' }, - { command: 'bugreports', description: 'View open bug reports' }, - { command: 'on', description: 'Enable admin mode (bypass auth to test as user)' }, - { command: 'off', description: 'Disable admin mode (restore normal admin auth)' }, - { command: 'admin_mode_on', description: 'Enable admin bypass mode (alias for /on)' }, - { command: 'admin_mode_off', description: 'Disable admin bypass mode (alias for /off)' }, - { command: 'testall', description: 'Run full static self-test of all bot features (PASS/FAIL)' }, - { command: 'testgiveaway', description: 'Run fake giveaway quick-test with 10 fake participants' }, - { command: 'whois', description: 'Look up user by TG ID or Runewager username' }, - { command: 'bonusstatus', description: 'View bonus state for a user' }, - { command: 'refreshuser', description: 'Force-migrate user data to latest schema' }, - { command: 'adminmode', description: 'Toggle admin bypass mode: /adminmode on|off' }, - { command: 'health', description: 'Show live health endpoint data' }, - { command: 'version', description: 'Bot version, Node version, uptime' }, - { command: 'resolvebug', description: 'Mark a bug report as resolved' }, - { command: 'exportbugs', description: 'Dump all open bug reports as text' }, - ], { scope: { type: 'chat', chat_id: adminId } }).catch(() => {}); + await bot.telegram.setMyCommands(adminCommands, { scope: { type: 'chat', chat_id: adminId } }).catch(() => {}); } await bot.telegram.setChatMenuButton({ menu_button: { type: 'web_app', text: '🎮 Play', web_app: { url: LINKS.miniAppPlay } }, @@ -1854,10 +1909,12 @@ bot.start(safeStepHandler('start', async (ctx) => { + 'This bot helps you join Runewager under the GambleCodez affiliate, ' + 'claim exclusive SC (Sweepstakes Coin) bonuses, and participate in SC prize giveaways — ' + 'all without leaving Telegram.\n\n' + + '🌍 *Runewager is 100% FREE to play and accepts players worldwide.*\n' + '• SC = Sweepstakes Coins — instant crypto prize redemption\n' - + '• No real-money wagering · No gambling language\n' - + '• Worldwide users welcome · 18+ only\n' + + '• No real-money wagering · No deposits · No gambling language\n' + + '• 18+ only — confirmation required before onboarding\n' + '• New users eligible for 3.5 SC sign-up bonus\n\n' + + `${AFFILIATE_REMINDER_TEXT}\n\n` + 'Join the community to get notified of live SC giveaways 👇'; const introButtons = Markup.inlineKeyboard([ [ @@ -1868,9 +1925,10 @@ bot.start(safeStepHandler('start', async (ctx) => { Markup.button.callback('🎉 SC Giveaways', 'menu_giveaways'), Markup.button.callback('❓ Help Booklet', 'open_help'), ], + [Markup.button.callback('ℹ️ Tell me more about the 30 SC bonus', 'w30_bonus_info')], ]); await sendIntroGif(ctx, introCaption, { parse_mode: 'Markdown', ...introButtons }); - await ctx.reply(COPY.ageGate, ageGateKeyboard()); + await ctx.reply(COPY.ageGate, { parse_mode: 'Markdown', ...ageGateKeyboard() }); return; } @@ -1891,11 +1949,10 @@ bot.start(safeStepHandler('start', async (ctx) => { + 'Runewager brings the heat with top‑tier providers like Hacksaw, BGaming, Betsoft, ' + 'live games, and full sports action — all wrapped inside a clean, sweepstakes‑safe experience.\n\n' + 'We even run a massive weekly leaderboard where grinders climb for bragging rights and real prizes.\n\n' - + 'And the best part?\n' - + 'Runewager is 100% FREE to play.\n' - + 'No deposits. No risk.\n' - + 'Just instant‑crypto prize redemptions and worldwide access for everyone.\n\n' - + 'Let\'s get you verified so we can unlock your bonuses and link your account.', + + '🌍 The best part? Runewager is 100% FREE to play and accepts players WORLDWIDE.\n' + + 'No deposits. No real-money wagering. No risk.\n' + + 'Just instant‑crypto prize redemptions for everyone, everywhere.\n\n' + + 'Let\'s get you set up so we can unlock your bonuses and link your account.', ); const iChatId = introRunewagerMsg.chat.id; const iMsgId = introRunewagerMsg.message_id; @@ -1925,7 +1982,7 @@ bot.command('menu', async (ctx) => { const user = getUser(ctx); clearPendingAction(user); if (!user.ageConfirmed) { - await ctx.reply(COPY.ageGate, ageGateKeyboard()); + await ctx.reply(COPY.ageGate, { parse_mode: 'Markdown', ...ageGateKeyboard() }); return; } await sendPersistentUserMenu(ctx, user); @@ -1977,21 +2034,24 @@ function buildHelpPages(user) { '🎯 ABOUT THIS BOT', 'GambleCodez Runewager Bot helps you join Runewager under the GambleCodez affiliate and unlock exclusive SC (Sweepstakes Coin) rewards.', '', - '• SC = Sweepstakes Coins — prize redemption only', - '• No real-money wagering · No gambling language', - '• Worldwide users welcome · 18+ only', + '🌍 *Runewager is 100% FREE to play and accepts players worldwide.*', + '• SC = Sweepstakes Coins — prize redemption only, no real-money wagering', + '• No deposits · No gambling language · 18+ only', + '• Instant crypto prize redemption', '• New users eligible for 3.5 SC sign-up bonus', '', '🚀 QUICK START', - '1️⃣ /start → Confirm 18+ → Create Runewager account', - '2️⃣ Join Runewager Discord (for verification)', - '3️⃣ /linkrunewager → Link your Runewager username', + '1️⃣ /start → Confirm 18+ (age gate)', + '2️⃣ Enter GambleCodez as your affiliate when signing up', + '3️⃣ /linkrunewager → Link your exact Runewager username', '4️⃣ /promo → Claim your 3.5 SC new user bonus', - '5️⃣ Join our Channel & Group for SC giveaways', - tips ? '\nℹ️ Tip: Use /walkthrough for a full 35-step interactive tutorial.' : '', + '5️⃣ /bonus → Request your 30 SC wager bonus when eligible', + '6️⃣ Join our Channel & Group for live SC giveaways', + tips ? '\nℹ️ Tip: Use /walkthrough for a full 35-step interactive setup tutorial.' : '', ].filter(Boolean).join('\n'), buttons: [ [Markup.button.url('🎮 Play Now', LINKS.miniAppPlay), Markup.button.callback('🔗 Link Account', 'menu_link_runewager')], + [Markup.button.callback('ℹ️ 30 SC Bonus Info', 'w30_bonus_info')], navRow(1, total), [Markup.button.callback('⬅️ Menu', 'to_main_menu')], ], @@ -2109,80 +2169,120 @@ function buildHelpPages(user) { ], }, - // ── Page 5: Commands & Quick Reference ──────────────────────────────────── + // ── Page 5: Full User Command Reference ────────────────────────────────── { text: [ - '📖 *Help Guide — Page 5/5: Commands & Quick Reference*', + '📖 *Help Guide — Page 5/5: All Commands*', + '', + '📋 ACCOUNT & ONBOARDING', + '/start — Start or resume onboarding (age gate → affiliate step → menu)', + '/menu — Open your persistent main menu', + '/status — See all flags: 18+ ✅, username, promo, onboarding step', + '/profile — View XP, badges, referral code, linked username', + '/settings — Play mode (Mini App/Browser) · Tooltips · Quick Commands', + '/walkthrough — 35-step guided tutorial covering account setup end-to-end', + '/cancel — Cancel any pending input and return to the menu', + '', + '🔗 RUNEWAGER ACCOUNT', + '/linkrunewager — Link or update your exact Runewager username', + ' ↳ Required for: 30 SC bonus, SC giveaways, eligibility checks', + ' ↳ Always shown a confirmation step — never auto-accepted', + '/signup — Get the Runewager sign-up link with GambleCodez affiliate', + '/affiliate — Open the promo / affiliate code entry page', + '/discord — Runewager Discord server + code-link channel links', + '/play — Launch Runewager Mini App directly inside Telegram', + '', + '🎁 BONUSES & PROMOS', + '/promo — View current promo code (3.5 SC new-user bonus)', + '/bonus — Check your 30 SC wager bonus status or submit a request', + ' ↳ Must wager 3,000 SC in any rolling 7-day period', + ' ↳ Must be under GambleCodez affiliate · once per account/IP', + ' ↳ Up to 3 verification attempts per account', + '/claim_history — View your promo claim history', + '', + '🏆 COMMUNITY & SOCIAL', + '/giveaway — View active SC giveaways and check your eligibility', + '/referral — Get your personal referral invite link', + '/leaderboard — Top referrers (all time)', + '/leaderboard_weekly — Most active bot users in the last 7 days', '', - '📋 USER COMMANDS', - '/start — Start or resume onboarding', - '/menu — Open main menu', - '/help or /commands — This help guide', - '/play — Launch Runewager Mini App', - '/profile — View profile & XP badges', - '/status — Quick account status check', - '/bonus — View or request your 30 SC bonus', - '/promo — View current promo code', - '/linkrunewager — Link your Runewager username', - '/signup — Get sign-up link (GambleCodez affiliate)', - '/affiliate — Open promo/affiliate entry page', - '/discord — Runewager Discord links', - '/referral — Your personal referral link', - '/walkthrough — 35-step guided setup tutorial', - '/leaderboard — Referral leaderboard (group chats)', - '/spin — Daily bonus wheel (cosmetic rewards)', - '/settings — Your preferences (play mode, tooltips, etc.)', - '/bugreport — Report a bug or issue', - '/cancel — Cancel any pending input flow', + '❓ HELP', + '/help or /commands — Open this help booklet', + '/bugreport — Report a bug or issue to the admin team', '', - '🔗 QUICK LINKS', - `• Play: t.me/RuneWager_bot/Play`, - `• Sign Up: runewager.com/r/GambleCodez`, - `• Promo Entry: runewager.com/affiliate`, - `• Channel: t.me/GambleCodezDrops`, - `• Group: t.me/GambleCodezPrizeHub`, - `• Discord: discord.gg/runewagers`, + tips ? 'ℹ️ Tip: Enable tooltips in /settings for extra guidance in each section.' : '', '', '💡 PRO TIPS', - '• Always sign up under GambleCodez — unlocks all rewards', + '• Always enter GambleCodez as affiliate when signing up on Runewager', '• Link your username first — required for bonuses & giveaways', - '• Enable tooltips in /settings for extra guidance here', - ].join('\n'), + '• Runewager is 100% free to play — no deposits, worldwide access 🌍', + ].filter(Boolean).join('\n'), buttons: [ - [Markup.button.url('🎮 Play Now', LINKS.miniAppPlay), Markup.button.url('💬 Discord', LINKS.rwDiscordJoin)], + [Markup.button.url('🎮 Play Now', LINKS.miniAppPlay), Markup.button.callback('🔗 Link Username', 'menu_link_runewager')], + [Markup.button.callback('🎯 30 SC Bonus', 'w30_request_start'), Markup.button.url('💬 Discord', LINKS.rwDiscordJoin)], + [Markup.button.callback('🐞 Report a Bug', 'help_open_bugreport')], navRow(5, total), [Markup.button.callback('⬅️ Menu', 'to_main_menu')], ], }, ]; - // ── Page 6: Admin Tools (admins only) ───────────────────────────────────── + // ── Page 6: Admin Command Reference (admins only) ───────────────────────── if (adminUser) { pages.push({ text: [ - '📖 *Help Guide — Page 6/6: Admin Tools*', + '📖 *Help Guide — Page 6/6: Admin Commands*', + '', + '🛠 DASHBOARD & NAVIGATION', + '/admin — Open the persistent admin main menu', + '/admin_help — Open this admin command reference', + '/admin_dashboard — Live stats: users, promos, giveaways, errors', + '', + '🎯 30 SC BONUS MANAGEMENT', + '/wager30_admin — Full bonus admin panel (keyboard-driven)', + '/bonus pending — List all pending bonus requests', + '/bonus approve — Approve a user\'s bonus request', + '/bonus deny [reason] — Deny a bonus request with optional reason', + '/bonus sent — Mark bonus as paid/sent (after manual Runewager credit)', + '/bonus add — Manually add a bonus_sent record for legacy users', + '/bonusstatus — View a user\'s full bonus state and history', + '', + '🎉 GIVEAWAY MANAGEMENT', + '/giveaway — Start giveaway wizard (in group: wizard for that chat)', + '/start_giveaway — Alias for /giveaway wizard', + '', + '📢 ANNOUNCEMENTS & PROMOS', + '/A or /announce — Compose and broadcast an announcement', + '/setpromo — Update the active promo display message', + '/boost_referrals [hours] [multiplier] — Activate referral 2× boost', + '/admin_notify — Send a targeted admin notification', '', - '🛠 ADMIN DASHBOARD', - '/admin_dashboard — or tap "🛠 Admin Dashboard" in menu', - 'Categories: Giveaway · Promo · User · System · Support', + '👤 USER MANAGEMENT', + '/whois — Look up any user by TG ID or Runewager username', + '/refreshuser — Force-migrate a user object to the latest schema', + '/adminmode on|off — Toggle admin bypass mode (test as regular user)', + '/on — Enable bypass mode · /off — Disable bypass mode', '', - '📋 QUICK REFERENCE', - '/testall — Full diagnostic. Run after every deploy.', - '/testgiveaway — Fake giveaway end-to-end test (no real SC).', - '/whois — Look up any user by TG ID or Runewager username.', - '/bonusstatus — View a user\'s full bonus state.', - '/refreshuser — Force-migrate user to latest schema.', - '/health — Show live health endpoint data.', - '/version — Bot version, Node version, uptime.', - '/adminmode on|off — Toggle admin bypass mode.', - '/exportbugs — Dump all open bug reports as text.', - '/resolvebug — Mark a bug report as resolved.', + '🐞 BUG REPORTS', + '/bugreports — View all open bug reports', + '/exportbugs — Export all open bug reports as plain text', + '/resolvebug — Mark a bug report as resolved', '', - 'ℹ️ All admin commands include smart error messages.', - 'ℹ️ Dashboard has 2 pages with sub-category menus.', + '🔧 SYSTEM', + '/testall — Run full static self-test — every feature PASS/FAIL', + '/testgiveaway — Fake giveaway end-to-end test (no real SC)', + '/health — Live health endpoint data', + '/version — Bot version, Node.js version, uptime', + '/deploy — Trigger live deploy from latest git commit', + '/deploy_status — View last deploy result and timing', + '/logs [lines] — Show last N lines of the bot log', + '/admin_backup — Snapshot runtime state to a dated backup file', + '', + 'ℹ️ All admin commands include smart error messages and usage hints.', ].join('\n'), buttons: [ [Markup.button.callback('🛠 Open Admin Dashboard', 'open_admin_dashboard'), Markup.button.callback('🧪 Run TestAll', 'admin_cmd_testall')], + [Markup.button.callback('🐞 Bug Reports', 'pamenu_bug_reports')], navRow(6, total), [Markup.button.callback('⬅️ Menu', 'to_main_menu')], ], @@ -2223,14 +2323,15 @@ bot.command('linkrunewager', async (ctx) => { async function sendLinkAccountPrompt(ctx, user) { const alreadyLinked = user.runewagerUsername && user.runewagerUsername.trim(); const header = alreadyLinked - ? `🔗 Your Runewager account is currently linked as: *${user.runewagerUsername}*\n\nSend a new username to update it, or tap Cancel to keep the current one.` - : '🔗 *Link Your Runewager Account*\n\nTo request the 30 SC bonus and join SC giveaways you must link your Runewager username.\n\n*Steps:*\n1️⃣ Sign up on Runewager (link below) if you haven\'t already\n2️⃣ Find your exact username in your Runewager profile\n3️⃣ Send it here — exactly as it appears on Runewager'; + ? `🔗 Your Runewager account is currently linked as: *${user.runewagerUsername}*\n\nSend a new username to update it, or tap Cancel to keep the current one.\n\n${AFFILIATE_REMINDER_TEXT}` + : `🔗 *Link Your Runewager Account*\n\n${AFFILIATE_REMINDER_TEXT}\n\n*Steps:*\n1️⃣ Sign up on Runewager (link below) if you haven't already\n2️⃣ Find your exact username in your Runewager profile\n3️⃣ Send it here — exactly as it appears on Runewager\n\n⚠️ Your message will not be accepted until you confirm it is correct.`; user.pendingAction = { type: 'await_runewager_username' }; await ctx.reply(header, { parse_mode: 'Markdown', ...Markup.inlineKeyboard([ [Markup.button.url('🔗 Sign Up on Runewager', LINKS.runewagerSignup)], [Markup.button.url('👤 View My Runewager Profile', LINKS.runewagerProfile)], + [Markup.button.callback('ℹ️ Tell me more about the 30 SC bonus', 'w30_bonus_info')], [Markup.button.callback('❌ Cancel', 'cancel_link')], ]), }); @@ -3153,11 +3254,43 @@ bot.action('pmenu_giveaways', async (ctx) => { }); bot.action('pmenu_help', async (ctx) => { + const user = getUser(ctx); + await ctx.answerCbQuery(); + // Show help sub-menu: Command Help Booklet + Bug Report + await ctx.reply( + '❓ *Help & Support*\n\nChoose what you need:', + { + parse_mode: 'Markdown', + ...Markup.inlineKeyboard([ + [Markup.button.callback('📖 Command Help Booklet', 'help_open_booklet')], + [Markup.button.callback('🐞 Report a Bug', 'help_open_bugreport')], + [Markup.button.callback('ℹ️ Tell me more about the 30 SC bonus', 'w30_bonus_info')], + [Markup.button.callback('⬅️ Main Menu', 'to_main_menu')], + ]), + }, + ); +}); + +bot.action('help_open_booklet', async (ctx) => { const user = getUser(ctx); await ctx.answerCbQuery(); await sendHelpMenu(ctx, user, 1); }); +bot.action('help_open_bugreport', async (ctx) => { + const user = getUser(ctx); + await ctx.answerCbQuery(); + clearPendingAction(user); + user.pendingAction = { type: 'await_bugreport' }; + await ctx.reply( + '🐞 *Report a Bug*\n\nDescribe the issue as clearly as possible.\nInclude what you were doing and what went wrong.\n\nType your report now:', + { + parse_mode: 'Markdown', + ...Markup.inlineKeyboard([[Markup.button.callback('❌ Cancel', 'to_main_menu')]]), + }, + ); +}); + bot.action('pmenu_admin', async (ctx) => { const user = getUser(ctx); await ctx.answerCbQuery(); @@ -3258,6 +3391,34 @@ bot.action('pamenu_tools', async (ctx) => { ); }); +bot.action('pamenu_admin_help', async (ctx) => { + const user = getUser(ctx); + await ctx.answerCbQuery(); + if (!requireAdmin(ctx)) return; + await sendHelpMenu(ctx, user, 6); +}); + +bot.action('pamenu_bug_reports', async (ctx) => { + await ctx.answerCbQuery(); + if (!requireAdmin(ctx)) return; + const reports = (promoStore.bugreports || []).filter((r) => r.status === 'open'); + if (!reports.length) { + await ctx.reply('✅ No open bug reports.', Markup.inlineKeyboard([[Markup.button.callback('↩ Admin Menu', 'pamenu_back_admin')]])); + return; + } + const lines = ['🐞 *Open Bug Reports*\n']; + reports.slice(0, 20).forEach((r) => { + lines.push(`#${r.id} — @${r.tgUsername || r.userId} — ${new Date(r.timestamp).toUTCString().slice(0, 16)}\n${r.message.slice(0, 120)}`); + }); + if (reports.length > 20) lines.push(`…and ${reports.length - 20} more. Use /exportbugs to see all.`); + await ctx.reply(lines.join('\n\n'), { + parse_mode: 'Markdown', + ...Markup.inlineKeyboard([ + [Markup.button.callback('↩ Admin Menu', 'pamenu_back_admin')], + ]), + }); +}); + bot.action('pamenu_back_user', async (ctx) => { const user = getUser(ctx); await ctx.answerCbQuery(); @@ -3458,12 +3619,15 @@ bot.action('onboard_skip_to_link', async (ctx) => { user.verified = true; await ctx.answerCbQuery('Skipping to account link.'); await ctx.reply( - '✅ Got it! Let\'s link your existing Runewager account.\n\n' - + 'Please send your exact Runewager username so the bot can track your wager bonus and giveaway eligibility.', - Markup.inlineKeyboard([ - [Markup.button.url('👤 Find My Username on Runewager', LINKS.runewagerProfile)], - [Markup.button.callback('❌ Cancel', 'cancel_link')], - ]), + `✅ Got it! Let's link your existing Runewager account.\n\n${AFFILIATE_REMINDER_TEXT}\n\nPlease send your exact Runewager username so the bot can track your wager bonus and giveaway eligibility.\n\n⚠️ You will be asked to confirm your username before it is saved.`, + { + parse_mode: 'Markdown', + ...Markup.inlineKeyboard([ + [Markup.button.url('👤 Find My Username on Runewager', LINKS.runewagerProfile)], + [Markup.button.callback('ℹ️ Tell me more about the 30 SC bonus', 'w30_bonus_info')], + [Markup.button.callback('❌ Cancel', 'cancel_link')], + ]), + }, ); user.pendingAction = { type: 'await_runewager_username' }; trackOnboardingProgress(user); @@ -3478,7 +3642,7 @@ bot.action('menu_verify_account', async (ctx) => { const user = getUser(ctx); clearPendingAction(user); if (!user.ageConfirmed) { - await ctx.reply(COPY.ageGate, ageGateKeyboard()); + await ctx.reply(COPY.ageGate, { parse_mode: 'Markdown', ...ageGateKeyboard() }); return; } await ctx.answerCbQuery(); @@ -3540,38 +3704,90 @@ bot.action('cancel_link', async (ctx) => { await ctx.reply('Link cancelled.', Markup.inlineKeyboard([[Markup.button.callback('⬅️ Main Menu', 'to_main_menu')]])); }); -// Confirm a username that was detected from a free-text message (smart detection path) -bot.action('confirm_smart_username', async (ctx) => { - const user = getUser(ctx); - const action = user.pendingAction; - if (!action || action.type !== 'await_username_confirm') { - await ctx.answerCbQuery('Nothing to confirm.'); - return; - } - const normalized = action.data.username; +// Shared handler: finalise username link after user confirmation +async function finaliseUsernameLink(ctx, user, normalized, returnTo) { const wasLinked = Boolean(user.runewagerUsername && user.runewagerUsername.trim()); user.runewagerUsername = normalized; user.pendingAction = null; addBadge(user, 'linked_profile'); if (!wasLinked) user.profileXP += 10; trackOnboardingProgress(user); - await ctx.answerCbQuery('Username linked!'); await reactToMessage(ctx, '🔗'); + + // If triggered mid-flow (e.g. bonus request), resume that flow + if (returnTo === 'w30_bonus') { + await ctx.reply(`✅ Username linked: *${normalized}*\n\n${AFFILIATE_REMINDER_TEXT}\n\nContinuing your bonus request…`, { parse_mode: 'Markdown' }); + const check = checkBonusEligibility(user); + if (check.ok) { + await submitBonusRequest(ctx, user); + } else if (check.needsWager) { + user.pendingAction = { type: 'w30_await_wager_total' }; + await ctx.reply( + 'What is your total SC wagered in the past 7 days?\n\n' + + 'Enter the number only (e.g. 3500). No screenshots needed — admin verifies this on Runewager.', + ); + } else { + await ctx.reply(check.reason); + } + return; + } + const nextStep = getNextOnboardingStep(user); const nextStepText = nextStep < 5 ? `\n\n➡️ Next step: ${onboardingStepLabel(nextStep)}` : '\n\n🎉 Onboarding complete! You can now request the 30 SC bonus.'; await ctx.reply( - `✅ Runewager account linked!\n\n👤 Username: *${normalized}*\n\nYou can now:\n• Request the 30 SC wager bonus (/bonus)\n• Join SC giveaways\n• View full profile (/profile)${nextStepText}`, + `✅ Runewager account linked!\n\n👤 Username: *${normalized}*\n\n${AFFILIATE_REMINDER_TEXT}\n\nYou can now:\n• Request the 30 SC wager bonus (/bonus)\n• Join SC giveaways\n• View full profile (/profile)${nextStepText}`, { parse_mode: 'Markdown', ...Markup.inlineKeyboard([ [Markup.button.callback('🎯 Request 30 SC Bonus', 'w30_request_start')], + [Markup.button.callback('ℹ️ Tell me more about the 30 SC bonus', 'w30_bonus_info')], [Markup.button.callback('👤 View Profile', 'menu_profile_action')], [Markup.button.callback('⬅️ Main Menu', 'to_main_menu')], ]), }, ); +} + +// "Yes, Continue" — user confirms the displayed username is correct +bot.action('confirm_yes_username', async (ctx) => { + const user = getUser(ctx); + const action = user.pendingAction; + if (!action || action.type !== 'await_username_confirm') { + await ctx.answerCbQuery('Nothing to confirm.'); + return; + } + await ctx.answerCbQuery('Username confirmed!'); + await finaliseUsernameLink(ctx, user, action.data.username, action.data.returnTo || null); +}); + +// "No, Edit" — return user to WAITING_FOR_USERNAME +bot.action('confirm_no_username', async (ctx) => { + const user = getUser(ctx); + const action = user.pendingAction; + const returnTo = (action && action.data && action.data.returnTo) || null; + user.pendingAction = { type: 'await_runewager_username', returnTo }; + await ctx.answerCbQuery('OK, let\'s try again.'); + await ctx.reply( + 'Please type your exact Runewager username as it appears on Runewager:', + Markup.inlineKeyboard([ + [Markup.button.url('👤 Find My Username', LINKS.runewagerProfile)], + [Markup.button.callback('❌ Cancel', 'cancel_link')], + ]), + ); +}); + +// Legacy alias kept for backward compat with any existing keyboard buttons +bot.action('confirm_smart_username', async (ctx) => { + const user = getUser(ctx); + const action = user.pendingAction; + if (!action || action.type !== 'await_username_confirm') { + await ctx.answerCbQuery('Nothing to confirm.'); + return; + } + await ctx.answerCbQuery('Username linked!'); + await finaliseUsernameLink(ctx, user, action.data.username, action.data.returnTo || null); }); bot.action('menu_join_channel', async (ctx) => { @@ -3601,7 +3817,7 @@ bot.action('menu_claim_bonus', async (ctx) => { clearPendingAction(user); await ctx.answerCbQuery(); if (!user.ageConfirmed) { - await ctx.reply(COPY.ageGate, ageGateKeyboard()); + await ctx.reply(COPY.ageGate, { parse_mode: 'Markdown', ...ageGateKeyboard() }); return; } if (needsLinkedUsername(user)) { @@ -3618,12 +3834,16 @@ bot.action('menu_claim_bonus', async (ctx) => { } await ctx.reply( - `${promoText()}\n\nSteps:\n1) Sign up on Runewager under GambleCodez\n2) Open the affiliate/promo page\n3) Enter the promo code shown above`, - Markup.inlineKeyboard([ - [signupButton()], - [promoButton()], - [Markup.button.callback('✅ I Entered Promo Code', 'promo_confirm_claimed')], - ]), + `${promoText()}\n\n${AFFILIATE_REMINDER_TEXT}\n\nSteps:\n1) Sign up on Runewager under GambleCodez\n2) Open the affiliate/promo page\n3) Enter the promo code shown above`, + { + parse_mode: 'Markdown', + ...Markup.inlineKeyboard([ + [signupButton()], + [promoButton()], + [Markup.button.callback('ℹ️ Tell me more about the 30 SC bonus', 'w30_bonus_info')], + [Markup.button.callback('✅ I Entered Promo Code', 'promo_confirm_claimed')], + ]), + }, ); // Show promo entry screenshot so users know exactly where to type the code await sendPhoto(ctx, IMG_PROMO_ENTRY, '📸 Step 3: Enter the promo code here (Menu → Refer a Friend → Promo entry box)'); @@ -3664,7 +3884,48 @@ bot.action('promo_confirm_claimed', async (ctx) => { bot.action('w30_rules', async (ctx) => { await ctx.answerCbQuery(); - await ctx.reply(WAGER_BONUS_RULES_TEXT, Markup.inlineKeyboard([[Markup.button.callback('🎯 Request My 30 SC Bonus', 'w30_request_start')]])); + await ctx.reply(WAGER_BONUS_RULES_TEXT, { + parse_mode: 'Markdown', + ...Markup.inlineKeyboard([ + [Markup.button.callback('🎯 Request My 30 SC Bonus', 'w30_request_start')], + [Markup.button.callback('⬅️ Back', 'to_main_menu')], + ]), + }); +}); + +// "Tell me more about the 30 SC bonus" — full explanation with affiliate reminder +bot.action('w30_bonus_info', async (ctx) => { + await ctx.answerCbQuery(); + const user = getUser(ctx); + const wb = user.wagerBonus || {}; + const attemptsLeft = 3 - (wb.attempts || 0); + const infoText = [ + '🎯 *GambleCodez 30 SC Bonus — Full Information*', + '', + AFFILIATE_REMINDER_TEXT, + '', + '*Requirements:*', + '• Must be under the *GambleCodez* affiliate', + '• Must wager *3,000 SC* in any rolling 7-day period', + '• Bonus is *once per account/IP*', + `• You may submit *up to 3 verification attempts* (you have *${attemptsLeft}* remaining)`, + '', + '*Process:*', + '• Bonus: *30 SC*', + '• Admin manually reviews and approves', + '• Admin is pinged the moment you request verification', + '• Admin sends the bonus directly on Runewager — the bot does NOT send SC', + '', + '⚠️ Do not message admins repeatedly. Requests are reviewed in order.', + ].join('\n'); + await ctx.reply(infoText, { + parse_mode: 'Markdown', + ...Markup.inlineKeyboard([ + [Markup.button.callback('🎯 Request My 30 SC Bonus', 'w30_request_start')], + [Markup.button.callback('🔗 Link Runewager Username', 'menu_link_runewager')], + [Markup.button.callback('⬅️ Back to Menu', 'to_main_menu')], + ]), + }); }); bot.action('w30_request_start', async (ctx) => { @@ -3687,11 +3948,14 @@ bot.action('w30_request_start', async (ctx) => { user.pendingAction = { type: 'await_runewager_username', returnTo: 'w30_bonus' }; await ctx.reply( '🔗 *One quick step first — link your Runewager username*\n\n' - + 'Send your exact Runewager username and we\'ll pick up your bonus request right after.', + + `${AFFILIATE_REMINDER_TEXT}\n\n` + + 'Send your exact Runewager username and we\'ll pick up your bonus request right after.\n\n' + + '⚠️ You will be asked to confirm your username before it is saved.', { parse_mode: 'Markdown', ...Markup.inlineKeyboard([ [Markup.button.url('👤 Find My Username', LINKS.runewagerProfile)], + [Markup.button.callback('ℹ️ Tell me more about the 30 SC bonus', 'w30_bonus_info')], [Markup.button.callback('❌ Cancel', 'cancel_link')], ]), }, @@ -4402,6 +4666,44 @@ bot.action('w30_admin_reset', async (ctx) => { await ctx.reply('Enter Telegram user ID or Runewager username to reset (returns to none, allows new request):'); }); +// View all completed bonus requests (approved + bonus_sent) +bot.action('w30_admin_completed', async (ctx) => { + if (!requireAdmin(ctx)) return; + await ctx.answerCbQuery(); + const completed = Array.from(userStore.values()).filter( + (u) => u.wagerBonus && (u.wagerBonus.status === 'bonus_sent' || u.wagerBonus.status === 'approved'), + ); + if (completed.length === 0) { + await ctx.reply('✅ No completed requests yet.', Markup.inlineKeyboard([[Markup.button.callback('⬅️ Back', 'open_admin_dashboard')]])); + return; + } + const lines = ['✅ *Completed Bonus Requests*\n']; + for (const u of completed) { + const wb = u.wagerBonus; + const name = u.tgUsername ? `@${u.tgUsername}` : `user${u.id}`; + lines.push(`${name} (${u.id}) — ${u.runewagerUsername || 'no username'} — *${wb.status}* — ${wb.attempts}/3 attempts`); + } + await ctx.reply(lines.join('\n'), { + parse_mode: 'Markdown', + ...Markup.inlineKeyboard([[Markup.button.callback('⬅️ Back to Bonus Panel', 'open_admin_dashboard')]]), + }); +}); + +// Add Runewager username manually (for users who already had an account) +bot.action('w30_admin_link_username', async (ctx) => { + if (!requireAdmin(ctx)) return; + const user = getUser(ctx); + clearPendingAction(user); + user.pendingAction = { type: 'w30_admin_link_username' }; + await ctx.answerCbQuery(); + await ctx.reply( + '🔗 *Add Runewager Username Manually*\n\n' + + 'Enter: ` `\n' + + 'Example: `6668510825 johnplayer`', + { parse_mode: 'Markdown' }, + ); +}); + // Inline approve button from pending list bot.action(/^w30_admin_approve_user_(\d+)$/, async (ctx) => { if (!requireAdmin(ctx)) return; @@ -4684,25 +4986,24 @@ bot.on('text', async (ctx) => { // Smart username detection — fires when there is NO pending action, the message looks // like a Runewager username (3-30 chars, alphanumeric/underscore/hyphen/dot, no spaces), - // and the user has not yet linked an account. Lets users type their username naturally - // at any point without needing an explicit prompt first. + // and the user has not yet linked an account. Always requires confirmation — never auto-accepts. if (!user.pendingAction) { if ( text && !text.startsWith('/') && /^[A-Za-z0-9_.-]{3,30}$/.test(text) && - !user.runewagerUsername + !user.runewagerUsername && + !NON_USERNAME_WORDS.has(text.toLowerCase()) ) { const normalized = normalizeRunewagerUsername(text); - user.pendingAction = { type: 'await_username_confirm', data: { username: normalized } }; + user.pendingAction = { type: 'await_username_confirm', data: { username: normalized, returnTo: null } }; await ctx.reply( - `👤 Is *${normalized}* your Runewager username?`, + `👤 You entered: *${normalized}*\n\nIs this your exact Runewager username?`, { parse_mode: 'Markdown', ...Markup.inlineKeyboard([ - [Markup.button.callback('✅ Yes, link it', 'confirm_smart_username')], - [Markup.button.callback('🔗 Enter a different username', 'menu_link_runewager')], - [Markup.button.callback('❌ Cancel', 'cancel_link')], + [Markup.button.callback('✅ Yes, Continue', 'confirm_yes_username')], + [Markup.button.callback('✏️ No, Edit', 'confirm_no_username')], ]), }, ); @@ -4812,50 +5113,38 @@ bot.on('text', async (ctx) => { return; } - // User linking flow + // User linking flow — NEVER auto-accept; always require explicit confirmation if (action.type === 'await_runewager_username') { if (!text) { - await ctx.reply('Username cannot be empty. Please send your exact Runewager username.'); + await ctx.reply('Username cannot be empty. Please type your exact Runewager username as it appears on Runewager.'); return; } - const normalizedUsername = normalizeRunewagerUsername(text); - const wasLinked = Boolean(user.runewagerUsername && user.runewagerUsername.trim()); - const returnTo = action.returnTo || null; - user.runewagerUsername = normalizedUsername; - user.pendingAction = null; - addBadge(user, 'linked_profile'); - if (!wasLinked) user.profileXP += 10; - trackOnboardingProgress(user); - await reactToMessage(ctx, '🔗'); - - // If triggered mid-flow (e.g. bonus request), resume that flow after linking - if (returnTo === 'w30_bonus') { - await ctx.reply(`✅ Username linked: *${normalizedUsername}*\n\nContinuing your bonus request…`, { parse_mode: 'Markdown' }); - const check = checkBonusEligibility(user); - if (check.ok) { - await submitBonusRequest(ctx, user); - } else if (check.needsWager) { - user.pendingAction = { type: 'w30_await_wager_total' }; - await ctx.reply( - 'What is your total SC wagered in the past 7 days?\n\n' - + 'Enter the number only (e.g. 3500). No screenshots needed — admin verifies this on Runewager.', - ); - } else { - await ctx.reply(check.reason); - } + // Reject obvious non-username inputs + const lowerText = text.toLowerCase(); + if ( + NON_USERNAME_WORDS.has(lowerText) || + text.includes(' ') || + text.startsWith('/') || + text.length < 2 + ) { + await ctx.reply( + "⚠️ That doesn't look like a Runewager username.\nPlease type your exact username as it appears on Runewager.", + Markup.inlineKeyboard([ + [Markup.button.callback('❌ Cancel', 'cancel_link')], + ]), + ); return; } - - const nextStep = getNextOnboardingStep(user); - const nextStepText = nextStep < 5 ? `\n\n➡️ Next step: ${onboardingStepLabel(nextStep)}` : '\n\n🎉 Onboarding complete! You can now request the 30 SC bonus.'; + // Stage for confirmation — never auto-accept + const normalizedUsername = normalizeRunewagerUsername(text); + user.pendingAction = { type: 'await_username_confirm', data: { username: normalizedUsername, returnTo: action.returnTo || null } }; await ctx.reply( - `✅ Runewager account linked!\n\n👤 Username: *${normalizedUsername}*\n\nYou can now:\n• Request the 30 SC wager bonus (/bonus)\n• Join SC giveaways\n• View full profile (/profile)${nextStepText}`, + `👤 You entered: *${normalizedUsername}*\n\nIs this your exact Runewager username?`, { parse_mode: 'Markdown', ...Markup.inlineKeyboard([ - [Markup.button.callback('🎯 Request 30 SC Bonus', 'w30_request_start')], - [Markup.button.callback('👤 View Profile', 'menu_profile_action')], - [Markup.button.callback('⬅️ Main Menu', 'to_main_menu')], + [Markup.button.callback('✅ Yes, Continue', 'confirm_yes_username')], + [Markup.button.callback('✏️ No, Edit', 'confirm_no_username')], ]), }, ); @@ -4967,6 +5256,34 @@ bot.on('text', async (ctx) => { return; } + // Admin: manually link a Runewager username to a user + if (action.type === 'w30_admin_link_username') { + if (!requireAdmin(ctx)) return; + user.pendingAction = null; + const parts = text.trim().split(/\s+/); + if (parts.length < 2) { + await ctx.reply('Format: ` `\nExample: `6668510825 johnplayer`', { parse_mode: 'Markdown' }); + return; + } + const [rawId, rwUsername] = parts; + const target = findUserByTelegramIdOrRunewager(rawId); + if (!target) { await ctx.reply(`User not found: ${rawId}`); return; } + const normalized = normalizeRunewagerUsername(rwUsername); + target.runewagerUsername = normalized; + target.wagerBonus.affiliateSource = 'GambleCodez'; + trackOnboardingProgress(target); + await ctx.reply(`✅ Runewager username *${normalized}* linked to TG ID ${target.id} (@${target.tgUsername || 'unknown'}).`, { parse_mode: 'Markdown' }); + // Notify the user + try { + await bot.telegram.sendMessage( + target.id, + `✅ An admin has linked your Runewager username: *${normalized}*\n\n${AFFILIATE_REMINDER_TEXT}`, + { parse_mode: 'Markdown', ...Markup.inlineKeyboard([[Markup.button.callback('🎯 Request 30 SC Bonus', 'w30_request_start')]]) }, + ); + } catch (_) { /* user may have blocked bot */ } + return; + } + // ── Inline giveaway wizard custom-text inputs ────────────────────────────── if (action.type === 'gwiz_await_title') { if (!requireAdmin(ctx)) return; @@ -6265,8 +6582,8 @@ async function runWeeklyWagerReminder() { // eslint-disable-next-line no-await-in-loop await bot.telegram.sendMessage( user.id, - `👋 Friendly reminder:\nIf you have wagered 3,000 SC in any 7-day period under the GambleCodez affiliate, you can request a one-time 30 SC bonus.\nAdmins manually credit the bonus on Runewager — no screenshots needed.`, - wagerReminderKeyboard(), + `👋 Friendly reminder:\n\n${AFFILIATE_REMINDER_TEXT}\n\nIf you have wagered 3,000 SC in any 7-day period under the GambleCodez affiliate, you can request a one-time 30 SC bonus.\nAdmins manually credit the bonus on Runewager — no screenshots needed.`, + { parse_mode: 'Markdown', ...wagerReminderKeyboard() }, ); } catch (e) { // ignore blocked users diff --git a/prod-run.sh b/prod-run.sh index 6f29904..035041b 100755 --- a/prod-run.sh +++ b/prod-run.sh @@ -265,6 +265,21 @@ LRCONF ensure_logrotate +# --------------------------------------------------------- +# 9b) Weekly disk-space protection cron job +# --------------------------------------------------------- +DISK_PROTECT_SCRIPT="${PROJECT_DIR}/scripts/disk-protect.sh" +if [[ -f "$DISK_PROTECT_SCRIPT" ]] && command -v crontab >/dev/null 2>&1; then + disk_cron_line="0 3 * * 0 $DISK_PROTECT_SCRIPT >> ${PROJECT_DIR}/logs/disk-protect.log 2>&1 # ${APP_NAME}_disk_protect" + current_cron="$(crontab -l 2>/dev/null || true)" + if ! printf '%s\n' "$current_cron" | grep -Fq "${APP_NAME}_disk_protect"; then + { printf '%s\n' "$current_cron"; printf '%s\n' "$disk_cron_line"; } | crontab - + say "Weekly disk-protect cron job installed" + else + say "Weekly disk-protect cron already present — skipping" + fi +fi + # --------------------------------------------------------- # 10) Structured diagnostics # --------------------------------------------------------- diff --git a/scripts/disk-protect.sh b/scripts/disk-protect.sh new file mode 100755 index 0000000..cadfa19 --- /dev/null +++ b/scripts/disk-protect.sh @@ -0,0 +1,127 @@ +#!/usr/bin/env bash +# ============================================================= +# disk-protect.sh — Runewager Bot Disk Space Protection +# +# Runs automatically (weekly cron / systemd timer) or manually. +# Safe: never deletes .env, state files, or anything critical. +# +# Schedule (add to crontab or run via systemd timer): +# 0 3 * * 0 /path/to/Runewager/scripts/disk-protect.sh +# ============================================================= + +set -euo pipefail + +APP_DIR="${APP_DIR:-/var/www/html/Runewager}" +LOG_DIR="${APP_DIR}/logs" +DATA_DIR="${APP_DIR}/data" +BACKUP_DIR="${DATA_DIR}/backups" + +# Max ages (days) +LOG_MAX_DAYS=7 +SNAPSHOT_MAX_DAYS=30 +TEMP_MAX_DAYS=3 + +# Journal vacuum target size +JOURNAL_VACUUM_MB=300 + +# ── Logging ──────────────────────────────────────────────────────────────────── + +log() { echo "[$(date -u +"%Y-%m-%dT%H:%M:%SZ")] [disk-protect] $*"; } +warn() { echo "[$(date -u +"%Y-%m-%dT%H:%M:%SZ")] [disk-protect] WARN: $*" >&2; } + +# ── 1. Log rotation (via logrotate if available) ─────────────────────────────── +log "Step 1: Log rotation" +if command -v logrotate >/dev/null 2>&1; then + LOGROTATE_CONF="${APP_DIR}/runewager.logrotate" + if [[ -f "$LOGROTATE_CONF" ]]; then + logrotate -f "$LOGROTATE_CONF" 2>/dev/null && log " logrotate ran OK" || warn " logrotate failed (non-fatal)" + else + log " No logrotate config found at $LOGROTATE_CONF — skipping" + fi +else + log " logrotate not available — using manual rotation" +fi + +# ── 2. Compress logs older than 1 day ───────────────────────────────────────── +log "Step 2: Compress logs older than 1 day" +if [[ -d "$LOG_DIR" ]]; then + find "$LOG_DIR" -type f \( -name "*.log" -o -name "*.out" \) -mtime +1 ! -name "*.gz" | while read -r f; do + gzip -f "$f" && log " Compressed: $f" + done +else + log " Log directory not found: $LOG_DIR — skipping" +fi + +# ── 3. Delete logs older than LOG_MAX_DAYS days ──────────────────────────────── +log "Step 3: Delete logs older than ${LOG_MAX_DAYS} days" +if [[ -d "$LOG_DIR" ]]; then + find "$LOG_DIR" -type f -mtime "+${LOG_MAX_DAYS}" -delete -print | while read -r f; do + log " Deleted old log: $f" + done +fi + +# ── 4. journalctl vacuum ─────────────────────────────────────────────────────── +log "Step 4: journalctl vacuum (keep up to ${JOURNAL_VACUUM_MB}MB)" +if command -v journalctl >/dev/null 2>&1; then + journalctl --vacuum-size="${JOURNAL_VACUUM_MB}M" 2>&1 | grep -v "^$" | while read -r line; do + log " [journal] $line" + done +else + log " journalctl not available — skipping" +fi + +# ── 5. Clear npm cache ───────────────────────────────────────────────────────── +log "Step 5: Clear npm cache" +if command -v npm >/dev/null 2>&1; then + npm cache clean --force 2>&1 | tail -1 | while read -r line; do log " [npm] $line"; done + log " npm cache cleared" +else + log " npm not found — skipping" +fi + +# ── 6. Clear temp files ──────────────────────────────────────────────────────── +log "Step 6: Clear temp files older than ${TEMP_MAX_DAYS} days" +# Clear any .tmp files left by atomic writes inside the app +if [[ -d "$DATA_DIR" ]]; then + find "$DATA_DIR" -type f -name "*.tmp.*" -mtime "+${TEMP_MAX_DAYS}" -delete -print | while read -r f; do + log " Deleted temp file: $f" + done +fi +# Clear /tmp files with our app name +find /tmp -type f -name "runewager-*" -mtime "+${TEMP_MAX_DAYS}" -delete 2>/dev/null | true + +# ── 7. Delete old runtime state snapshots older than SNAPSHOT_MAX_DAYS ───────── +log "Step 7: Delete runtime state snapshots older than ${SNAPSHOT_MAX_DAYS} days" +if [[ -d "$BACKUP_DIR" ]]; then + find "$BACKUP_DIR" -type f -name "runtime-state-*.json" -mtime "+${SNAPSHOT_MAX_DAYS}" -delete -print | while read -r f; do + log " Deleted old snapshot: $f" + done +else + log " Backup dir not found: $BACKUP_DIR — skipping" +fi + +# ── 8. NEVER delete critical files — safety check ───────────────────────────── +log "Step 8: Safety check — verifying critical files are intact" +CRITICAL_OK=true +for f in "${APP_DIR}/.env" "${DATA_DIR}/runtime-state.json"; do + if [[ -f "$f" ]]; then + log " OK: $f" + else + log " (not present, not required): $f" + fi +done +if [[ "$CRITICAL_OK" != "true" ]]; then + warn " ⚠️ Critical file check failed — review above output" +fi + +# ── 9. Disk usage summary ────────────────────────────────────────────────────── +log "Step 9: Disk usage summary" +df -h / 2>/dev/null | tail -1 | while read -r line; do log " / : $line"; done +if [[ -d "$APP_DIR" ]]; then + du -sh "$APP_DIR" 2>/dev/null | while read -r line; do log " App dir: $line"; done +fi +if [[ -d "$LOG_DIR" ]]; then + du -sh "$LOG_DIR" 2>/dev/null | while read -r line; do log " Logs: $line"; done +fi + +log "Disk protection complete."