From 1930d1b9aff4567a3a8bdd352d6f8298fad4ec30 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 22 Feb 2026 15:33:08 +0000 Subject: [PATCH] feat: admin menu fix, onboarding gate, intro message, /announce command MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Fix: pamenu_back_user now explicitly deletes the admin menu message and clears its keyboard before rendering the user menu, so admin UI fully vanishes when tapping Back to User Menu - Fix: /start no longer skips onboarding — main menu is gated behind onboarding completion (getNextOnboardingStep >= 5). Returns the appropriate step prompt (account setup, username link, promo, community) instead of jumping straight to the main menu - Add: sexy Runewager intro message sent on /start after the greeting, auto-deletes after 15 seconds - Add: showOnboardingPrompt() helper routes to the correct step UI - Add: /A, /a, /announce commands (admin-only) with a 3-state machine: await_announcement_text → preview → await_announcement_action announce_edit → back to text input announce_send_all → broadcast to all users + @GambleCodezDrops channel announce_send_channel → channel only - Add: ANNOUNCE_CHANNEL constant (env ANNOUNCE_CHANNEL or @GambleCodezDrops) https://claude.ai/code/session_01X3PxGFF5zzKptQwVkjYzzN --- index.js | 203 ++++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 200 insertions(+), 3 deletions(-) diff --git a/index.js b/index.js index b7fc7cd..25f8a6c 100644 --- a/index.js +++ b/index.js @@ -44,6 +44,9 @@ const LINKS = { const DEFAULT_BONUS_RULE = `Wager 3,000 SC in any rolling 7-day period → 30 SC bonus (once per account). Need help? Contact @GambleCodez on Telegram or open a support ticket: ${LINKS.rwDiscordSupport}`; +// Channel username (or chat_id) for admin announcements via /announce +const ANNOUNCE_CHANNEL = process.env.ANNOUNCE_CHANNEL || '@GambleCodezDrops'; + // ── Images ───────────────────────────────────────────────────────────────── // Images live in /images/ next to index.js. // If HOSTED_*_URL env vars are set they take priority (CDN / public URL). @@ -1289,6 +1292,58 @@ function clearPendingAction(user) { user.pendingAction = null; } +/** + * Show the appropriate onboarding step prompt instead of the main menu. + * Called when a user has confirmed their age but has not yet completed onboarding. + * @param {object} ctx - Telegraf context + * @param {object} user - user state object + * @param {number} step - current onboarding step (1–4) + */ +async function showOnboardingPrompt(ctx, user, step) { + switch (step) { + case 1: + // Account setup — leads through GambleCodez VIP / Discord signup flow + await sendGambleCodezVIPStep(ctx, user); + break; + case 2: + // Username linking + await sendLinkAccountPrompt(ctx, user); + break; + case 3: + // Promo claim + await ctx.reply( + `🎁 *Claim Your New User Bonus*\n\nEnter promo code *${promoStore.code}* on the Runewager affiliate page to claim your ${promoStore.amountSC} SC new user bonus!`, + { + parse_mode: 'Markdown', + ...Markup.inlineKeyboard([ + [Markup.button.url('🎁 Enter Promo Code (Mini App)', LINKS.miniAppClaim)], + [Markup.button.callback('⬅️ Main Menu', 'to_main_menu')], + ]), + }, + ); + break; + case 4: + // Join community channel + group + await ctx.reply( + '📢 *Join the GambleCodez Community*\n\nJoin both the channel and group to complete your onboarding and unlock SC giveaways!', + { + parse_mode: 'Markdown', + ...Markup.inlineKeyboard([ + [ + Markup.button.callback('📢 Join Channel', 'menu_join_channel'), + Markup.button.callback('💬 Join Group', 'menu_join_group'), + ], + [Markup.button.callback('⬅️ Main Menu', 'to_main_menu')], + ]), + }, + ); + break; + default: + // Fallback — should not happen, but show main menu to be safe + await sendPersistentUserMenu(ctx, user); + } +} + function isValidHttpUrl(value) { try { const parsed = new URL(value); @@ -1775,16 +1830,39 @@ bot.start(safeStepHandler('start', async (ctx) => { try { await ctx.telegram.deleteMessage(greetChatId, greetMsgId); } catch (_) { /* ignore */ } }, 15000); - if (user.onboarding.currentStep < 5) { - const stepMsg = await ctx.reply(`⏳ Your next step: ${onboardingStepLabel(user.onboarding.currentStep)}.`); + // ── Sexy Runewager intro message (auto-delete after 15 seconds) ───────────── + const introRunewagerMsg = await ctx.reply( + 'Before we jump in, let me give you the quick Runewager rundown…\n\n' + + '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.', + ); + const iChatId = introRunewagerMsg.chat.id; + const iMsgId = introRunewagerMsg.message_id; + setTimeout(async () => { + try { await ctx.telegram.deleteMessage(iChatId, iMsgId); } catch (_) { /* ignore */ } + }, 15000); + + // ── Gate main menu behind onboarding completion ────────────────────────────── + const nextOnboardStep = getNextOnboardingStep(user); + if (nextOnboardStep < 5) { + // Show current step label (auto-deletes after 12s), then show step prompt + const stepMsg = await ctx.reply(`⏳ Your next step: ${onboardingStepLabel(nextOnboardStep)}.`); const sChatId = stepMsg.chat.id; const sMsgId = stepMsg.message_id; setTimeout(async () => { try { await ctx.telegram.deleteMessage(sChatId, sMsgId); } catch (_) { /* ignore */ } }, 12000); + await showOnboardingPrompt(ctx, user, nextOnboardStep); + return; } - // ── Persistent user main menu ─────────────────────────────────────────────── + // ── Onboarding complete — show persistent user main menu ───────────────────── await sendPersistentUserMenu(ctx, user); })); @@ -2133,6 +2211,28 @@ bot.command('admin_help', async (ctx) => { await sendHelpMenu(ctx, user, 'admin'); }); +// ========================= +// Admin Announce Command — /A /a /announce +// State machine: +// state.none → /A|/a|/announce → state.await_announcement_text +// state.await_announcement_text → admin sends text → preview + buttons → state.await_announcement_action +// state.await_announcement_action → Edit → state.await_announcement_text +// → Send All → broadcast + channel → state.none +// → Send Channel Only → channel → state.none +// ========================= + +async function handleAnnounceCommand(ctx) { + if (!requireAdmin(ctx)) return; + const user = getUser(ctx); + clearPendingAction(user); + user.pendingAction = { type: 'await_announcement_text' }; + await ctx.reply('Okay! What would you like to announce?\nYou can send normal text or HTML.'); +} + +bot.command('A', handleAnnounceCommand); +bot.command('a', handleAnnounceCommand); +bot.command('announce', handleAnnounceCommand); + // ========================= // Giveaway Wizard — Inline keyboard-driven creation flow // Triggered by /giveaway command (admin) or "Start Giveaway" button. @@ -3157,6 +3257,16 @@ bot.action('pamenu_tools', async (ctx) => { bot.action('pamenu_back_user', async (ctx) => { const user = getUser(ctx); await ctx.answerCbQuery(); + // Delete the persistent admin menu message so it fully vanishes + if (user.adminMenuMsgId && user.adminMenuChatId) { + try { + await ctx.telegram.deleteMessage(user.adminMenuChatId, user.adminMenuMsgId); + } catch (_) { /* ignore — may already be gone */ } + user.adminMenuMsgId = null; + user.adminMenuChatId = null; + } + // Strip keyboard from the callback message in case the delete didn't fire + try { await ctx.editMessageReplyMarkup({ inline_keyboard: [] }); } catch (_) { /* ignore */ } await sendPersistentUserMenu(ctx, user); }); @@ -5009,6 +5119,27 @@ bot.on('text', async (ctx) => { return; } + // Admin announce: await announcement text + if (action.type === 'await_announcement_text') { + if (!requireAdmin(ctx)) return; + if (!text) { + await ctx.reply('Announcement text cannot be empty. Please send the message you want to announce.'); + return; + } + user.pendingAction = { type: 'await_announcement_action', data: { announcementText: text } }; + await ctx.reply( + `Hey!\n\nOkay, it will look like this:\n\n${text}\n\nWould you like to edit or send?`, + Markup.inlineKeyboard([ + [ + Markup.button.callback('✏️ Edit', 'announce_edit'), + Markup.button.callback('📣 Send All', 'announce_send_all'), + Markup.button.callback('📢 Send Channel Only', 'announce_send_channel'), + ], + ]), + ); + return; + } + // Giveaway live edits if (action.type === 'gw_extend_minutes') { const giveaway = giveawayStore.running.get(action.data.gwId); @@ -5092,6 +5223,72 @@ bot.action('gw_create_no', async (ctx) => { await ctx.reply('Giveaway setup cancelled.'); }); +// ========================= +// Announce command callback actions +// ========================= + +bot.action('announce_edit', async (ctx) => { + if (!requireAdmin(ctx)) return; + const user = getUser(ctx); + await ctx.answerCbQuery(); + user.pendingAction = { type: 'await_announcement_text' }; + await ctx.reply('Sure — send the updated announcement text.'); +}); + +bot.action('announce_send_all', async (ctx) => { + if (!requireAdmin(ctx)) return; + const user = getUser(ctx); + await ctx.answerCbQuery('Sending...'); + const action = user.pendingAction; + if (!action || action.type !== 'await_announcement_action' || !action.data) { + await ctx.reply('No announcement pending. Use /announce to start a new one.'); + return; + } + const announcementText = action.data.announcementText; + user.pendingAction = null; + + // Broadcast to all non-opted-out users + let sent = 0; + for (const [, u] of userStore) { + if (u.optOutBroadcasts || u.muted) continue; + try { + await ctx.telegram.sendMessage(u.id, announcementText); + sent += 1; + } catch (_) { /* ignore blocked/deactivated users */ } + } + + // Send to the GambleCodezDrops channel + try { + await ctx.telegram.sendMessage(ANNOUNCE_CHANNEL, announcementText); + } catch (e) { + await ctx.reply(`⚠️ Channel send failed: ${e.message}`); + } + + adminLog('announce_all', { by: ctx.from.id, sent }); + await ctx.reply(`📣 Announcement sent to ${sent} users and the GambleCodezDrops channel.`); +}); + +bot.action('announce_send_channel', async (ctx) => { + if (!requireAdmin(ctx)) return; + const user = getUser(ctx); + await ctx.answerCbQuery('Sending to channel...'); + const action = user.pendingAction; + if (!action || action.type !== 'await_announcement_action' || !action.data) { + await ctx.reply('No announcement pending. Use /announce to start a new one.'); + return; + } + const announcementText = action.data.announcementText; + user.pendingAction = null; + + try { + await ctx.telegram.sendMessage(ANNOUNCE_CHANNEL, announcementText); + adminLog('announce_channel', { by: ctx.from.id }); + await ctx.reply('📣 Announcement sent to the GambleCodezDrops channel.'); + } catch (e) { + await ctx.reply(`❌ Failed to send to channel: ${e.message}`); + } +}); + // ========================= // Giveaway Wizard action handlers // =========================