Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
203 changes: 200 additions & 3 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand Down Expand Up @@ -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);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: The new onboarding gate can be bypassed for users whose next step is "Account Setup" because showOnboardingPrompt calls sendGambleCodezVIPStep, which immediately shows the main menu when user.sawGambleCodezStep is true, so users who have seen that screen once can still reach the main menu even if other onboarding steps (verification, linking, promo, community) are incomplete. Resetting sawGambleCodezStep in the gated path ensures they see the intended step UI instead of skipping straight to the menu. [logic error]

Severity Level: Major ⚠️
- ⚠️ /start skips account-setup copy for partially-onboarded users.
- ⚠️ Onboarding step label disagrees with actual UI shown.
- ⚠️ Users may miss GambleCodez VIP signup instructions.
Suggested change
await sendGambleCodezVIPStep(ctx, user);
// In the gated onboarding flow we must not skip straight to the main menu,
// so ensure the VIP step is actually shown even if it was seen before.
user.sawGambleCodezStep = false;
Steps of Reproduction ✅
1. In `index.js:1661-1760` trigger the age gate and VIP step: send `/start`, tap "✅ I am
18+" (`age_yes` handler at `index.js:2115`), then tap "➡️ Continue — Sign Up with
GambleCodez" (`onboard_gcz_continue` at `index.js:2143`), which sets
`user.sawGambleCodezStep = true` and calls `sendPersistentUserMenu(ctx, user)`.

2. Ensure the user has not completed onboarding: `user.verified === false`,
`user.runewagerUsername` empty, `user.claimedPromo === false`, `user.hasJoinedChannel ===
false`, `user.hasJoinedGroup === false`, so `getNextOnboardingStep(user)` at
`index.js:1214-1223` still returns `1` ("Account Setup").

3. Call `/start` again; in the `/start` handler at `index.js:1765-1867`, after intro
messages it computes `nextOnboardStep = getNextOnboardingStep(user)` (value `1`), sees `<
5`, then sends the auto-deleting "Your next step: Account Setup" message and calls `await
showOnboardingPrompt(ctx, user, nextOnboardStep)` at `index.js:1852-1861`.

4. Inside `showOnboardingPrompt` (current code at `index.js:1302-1345`), case `1` calls
`sendGambleCodezVIPStep(ctx, user)`; in that helper at `index.js:2193-2211` the first line
`if (user.sawGambleCodezStep) { await sendPersistentUserMenu(ctx, user); return; }`
short-circuits, so the user is taken straight to the main menu instead of seeing the
account-setup step UI, despite `/start` detecting that onboarding is still at step 1.
Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** index.js
**Line:** 1306:1306
**Comment:**
	*Logic Error: The new onboarding gate can be bypassed for users whose next step is "Account Setup" because `showOnboardingPrompt` calls `sendGambleCodezVIPStep`, which immediately shows the main menu when `user.sawGambleCodezStep` is true, so users who have seen that screen once can still reach the main menu even if other onboarding steps (verification, linking, promo, community) are incomplete. Resetting `sawGambleCodezStep` in the gated path ensures they see the intended step UI instead of skipping straight to the menu.

Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
👍 | 👎

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);
Expand Down Expand Up @@ -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);
}));

Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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);
});

Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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
// =========================
Expand Down
Loading