Skip to content
Merged
Show file tree
Hide file tree
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
1 change: 1 addition & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ 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.`
- 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
1. git pull origin main
Expand Down
5 changes: 5 additions & 0 deletions RUNEWAGER_FUNCTIONALITY_MAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ Navigation is driven by inline menus plus command aliases. Persistent user/admin
- Includes onboarding/account actions, promo/bonus entry points, community links, giveaways, help, settings.

### Key user branches
- Referral system: Main Menu exposes **👥 Refer a Friend** submenu (`Your Referral Code`, `Share Your Code`, `How Referral Boosts Work`, Back/Cancel).
- Play-link routing is deterministic: browser mode always returns external `https://gamble-codez.com/...`; telegram mode always returns `startapp` mini-app deep links.
- Discord links are browser-only via `getDiscordLink(...)` sanitation (no WebAppInfo/startapp wrappers).
- **Profile & status**: `/profile`, quick status callbacks.
Expand Down Expand Up @@ -139,6 +140,8 @@ The bot uses `user.pendingAction.type` as its input state machine. Key families:
## 9. Onboarding Flow (full step map)

1. `/start` intro + age confirmation gate.
2. After age confirmation, onboarding asks: **Were you referred by a friend?** (`Yes`/`No`).
3. If yes, user enters a referral code once; valid code applies 2× giveaway boost to both users for 7 days and stores `{ referral_code_owner, referred_user_id, boost_expiration_timestamp }`.
2. Account setup guidance (Runewager + Discord verification steps).
3. Username linking via `/link` or text detect + confirm callbacks.
4. Promo and bonus prompts depending on linked state and eligibility.
Expand Down Expand Up @@ -246,6 +249,8 @@ Pending-action timeout handling is enforced in the text input router: each pendi
- Markdown escaping utilities for user/path/output interpolation.
- Command error wrappers to reduce undefined behavior on invalid usage.

- Share-ready referral HTML template is generated via helper and opened with Telegram native share URL (`t.me/share/url`) while preserving external join link.

## 20. Edge Cases & Special Conditions

- Empty states (no giveaways/promos/bugs) return explicit panels.
Expand Down
128 changes: 120 additions & 8 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -377,7 +377,7 @@ const MAX_ANALYTICS_EVENTS = Number(process.env.MAX_ANALYTICS_EVENTS || 2000);
const MAX_ONBOARDING_STEPS_HISTORY = Number(process.env.MAX_ONBOARDING_STEPS_HISTORY || 500);
const promoHistoryStore = new Map();
const analyticsStore = { events: [], onboardingStarts: 0, onboardingCompletions: 0, webAppEvents: 0 };
const referralStore = { boosts: [], archivedBoosts: [], links: new Map(), stats: new Map(), weekly: new Map() };
const referralStore = { boosts: [], archivedBoosts: [], links: new Map(), stats: new Map(), weekly: new Map(), referrals: [] };
const approvedGroupsStore = new Set(); // Feature 8: set of chat_id numbers approved for giveaway posting
const broadcastFailedUsers = []; // Feature 5: [{userId, lastError, failedAt}] permanently failed users
const smartButtonTracker = new Map(); // token -> expiry timestamp (TTL = 2 minutes)
Expand Down Expand Up @@ -446,6 +446,7 @@ const ACTION_LABELS = {
await_sshv_editor_content: 'SSHV editor content',
await_sshv_danger_confirm: 'dangerous SSHV confirmation',
await_register_chat_forward: 'chat registration forward',
await_referral_code: 'referral code entry',
};

/**
Expand Down Expand Up @@ -830,6 +831,7 @@ function createRuntimeStateSnapshot() {
links: Array.from(referralStore.links.entries()),
stats: Array.from(referralStore.stats.entries()),
weekly: Array.from(referralStore.weekly.entries()),
referrals: referralStore.referrals || [],
},
approvedGroupsStore: Array.from(approvedGroupsStore),
broadcastFailedUsers,
Expand Down Expand Up @@ -1055,6 +1057,7 @@ function loadRuntimeState() {
referralStore.links = new Map(raw.referralStore.links || []);
referralStore.stats = new Map(raw.referralStore.stats || []);
referralStore.weekly = new Map(raw.referralStore.weekly || []);
referralStore.referrals = Array.isArray(raw.referralStore.referrals) ? raw.referralStore.referrals : [];
}
if (Array.isArray(raw.approvedGroupsStore)) {
for (const id of raw.approvedGroupsStore) approvedGroupsStore.add(Number(id));
Expand Down Expand Up @@ -1261,6 +1264,8 @@ function createDefaultUser(user) {
referralCount: 0,
lastReferralAt: 0,
boostExpiresAt: 0, // ms timestamp; if > now, winner gets 2× SC
referredByUserId: 0,
referralAppliedAt: 0,
commandsUsed: 0,
sessionsCount: 0,
wagerBonus: {
Expand Down Expand Up @@ -2538,7 +2543,7 @@ function mainMenuKeyboard(isAdminUser = false, page = 1, user = null) {
Markup.button.url('💬 Discord', LINKS.rwDiscordJoin),
],
[
Markup.button.callback('🏆 Referral Link', 'menu_referral'),
Markup.button.callback('👥 Refer a Friend', 'menu_referral'),
Markup.button.callback('👤 My Profile', 'menu_profile_action'),
],
[
Expand Down Expand Up @@ -2596,7 +2601,7 @@ function mainMenuKeyboard(isAdminUser = false, page = 1, user = null) {
if (user && user.settings && user.settings.showQuickCommands) {
rows.push([
Markup.button.callback('▶ Giveaways', 'menu_giveaways'),
Markup.button.callback('▶ Referral', 'menu_referral'),
Markup.button.callback('👥 Refer a Friend', 'menu_referral'),
Markup.button.callback('▶ Status', 'menu_qc_status'),
Markup.button.callback('▶ Help', 'menu_help'),
]);
Expand Down Expand Up @@ -4602,6 +4607,48 @@ function referralCodeForUser(user) {
return referralStore.links.get(user.id);
}


function referralShareHTML(code) {
return `<b>🚨 YO! I’m farming free SC on Runewager and you need to get in NOW.</b>

Daily giveaways, instant rewards, drops, and a secret new-user bonus waiting inside the bot.

Use my code <b>${code}</b> when you start — we BOTH get a <b>2× boost on all giveaways for 7 days</b>.

👉 <a href="https://t.me/RunewagerBot">Tap here to join Runewager</a>

Don’t sleep. This thing prints. ⚡👑`;
}

function applyOnboardingReferralCode(user, referralCode) {
const code = String(referralCode || '').trim().toLowerCase();
if (!code) return { ok: false, reason: 'Please enter a referral code.' };
if (!user.ageConfirmed) return { ok: false, reason: 'Referral codes can only be entered during onboarding.' };
if ((user.referralAppliedAt || 0) > 0 || (user.referredByUserId || 0) > 0) return { ok: false, reason: 'Referral code already used on this account.' };
if (user.onboarding && user.onboarding.completedAt) return { ok: false, reason: 'Referral code can only be entered before onboarding is complete.' };

let owner = null;
for (const [, u] of userStore) {
if (String(referralCodeForUser(u) || '').toLowerCase() === code) { owner = u; break; }
}
if (!owner) return { ok: false, reason: 'Referral code not found.' };
if (Number(owner.id) === Number(user.id)) return { ok: false, reason: 'You cannot use your own referral code.' };

const now = Date.now();
const expiresAt = now + 7 * 24 * 60 * 60 * 1000;
user.referredByUserId = owner.id;
user.referralTag = code;
user.referralAppliedAt = now;
user.boostExpiresAt = Math.max(Number(user.boostExpiresAt || 0), expiresAt);
owner.boostExpiresAt = Math.max(Number(owner.boostExpiresAt || 0), expiresAt);
owner.referralCount = (owner.referralCount || 0) + 1;
owner.lastReferralAt = now;
referralStore.referrals.push({ referral_code_owner: owner.id, referred_user_id: user.id, boost_expiration_timestamp: expiresAt, created_at: now });
awardReferralProgress(owner.id, 1);
persistRuntimeState();
return { ok: true, ownerId: owner.id, expiresAt };
}

/**

* getActiveReferralBoost executes its scoped Runewager logic and participates in menu/command or utility flow composition.
Expand Down Expand Up @@ -7414,7 +7461,34 @@ bot.action('age_yes', async (ctx) => {
trackOnboardingProgress(user);
await ctx.answerCbQuery('Age confirmed.');
await reactToMessage(ctx, '👍');
// Show GambleCodez VIP onboarding step before main menu
await sendOnboardingReferralPrompt(ctx, user);
});


async function sendOnboardingReferralPrompt(ctx, user) {
if ((user.referralAppliedAt || 0) > 0 || (user.referredByUserId || 0) > 0) {
await sendGambleCodezVIPStep(ctx, user);
return;
}
await ctx.reply(
'Were you referred by a friend?',
Markup.inlineKeyboard([
[Markup.button.callback('✅ Yes, I was referred', 'onboard_ref_yes')],
[Markup.button.callback('➡️ No, continue', 'onboard_ref_no')],
]),
);
}

bot.action('onboard_ref_yes', async (ctx) => {
const user = getUser(ctx);
await ctx.answerCbQuery();
user.pendingAction = { type: 'await_referral_code', createdAt: Date.now() };
await ctx.reply('Please enter your referral code now.');
});

bot.action('onboard_ref_no', async (ctx) => {
const user = getUser(ctx);
await ctx.answerCbQuery();
await sendGambleCodezVIPStep(ctx, user);
});

Expand Down Expand Up @@ -7834,14 +7908,39 @@ bot.command('pmdeny', safeAdminHandler('pmdeny', { usage: '/pmdeny <claim_id> <r
bot.action('menu_referral', async (ctx) => {
const user = getUser(ctx);
await ctx.answerCbQuery();
const code = referralCodeForUser(user);
const link = `https://t.me/${ctx.botInfo.username}?start=ref_${code}`;
await ctx.reply(`🏆 *Your Referral Link*\n\n${link}\n\nShare this link to earn referral rewards!`, {
await ctx.reply('👥 *Refer a Friend*\n\nChoose an option:', {
parse_mode: 'Markdown',
...Markup.inlineKeyboard([[Markup.button.callback('⬅️ Menu', 'to_main_menu')]]),
...Markup.inlineKeyboard([
[Markup.button.callback('🧾 Your Referral Code', 'ref_menu_code')],
[Markup.button.callback('📤 Share Your Code', 'ref_menu_share')],
[Markup.button.callback('ℹ️ How Referral Boosts Work', 'ref_menu_how')],
[Markup.button.callback('⬅️ Back', 'to_main_menu')],
[Markup.button.callback('❌ Cancel', 'to_main_menu')],
]),
});
});

bot.action('ref_menu_code', async (ctx) => {
const user = getUser(ctx);
await ctx.answerCbQuery();
const code = referralCodeForUser(user);
await ctx.reply(`Your referral code: \`${code}\``, { parse_mode: 'Markdown' });
});

bot.action('ref_menu_how', async (ctx) => {
await ctx.answerCbQuery();
await ctx.reply('Referral boosts: both you and your referred friend get 2× giveaway boost for 7 days when a valid referral code is entered during onboarding.');
});

bot.action('ref_menu_share', async (ctx) => {
const user = getUser(ctx);
await ctx.answerCbQuery();
const code = referralCodeForUser(user);
const html = referralShareHTML(code);
const shareUrl = `https://t.me/share/url?url=${encodeURIComponent('https://t.me/RunewagerBot')}&text=${encodeURIComponent(html)}`;
await ctx.reply('Tap below to open Telegram share sheet:', Markup.inlineKeyboard([[Markup.button.url('📤 Share Your Code', shareUrl)], [Markup.button.callback('⬅️ Back', 'menu_referral')]]));
});

bot.action('menu_bugreport', async (ctx) => {
const user = getUser(ctx);
await ctx.answerCbQuery();
Expand Down Expand Up @@ -9623,6 +9722,19 @@ Content Drops and group broadcasts will now use this target.`);
}

// User linking flow — NEVER auto-accept; always require explicit confirmation

if (action.type === 'await_referral_code') {
const result = applyOnboardingReferralCode(user, text);
if (!result.ok) {
await ctx.reply(`❌ ${result.reason}`, Markup.inlineKeyboard([[Markup.button.callback('❌ Skip Referral', 'onboard_ref_no')]]));
return;
}
user.pendingAction = null;
await ctx.reply('✅ Referral applied! Both accounts now have a 2× giveaway boost for 7 days.');
await sendGambleCodezVIPStep(ctx, user);
return;
}

if (action.type === 'await_runewager_username') {
if (!text) {
await ctx.reply(
Expand Down
17 changes: 17 additions & 0 deletions test/smoke.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -524,3 +524,20 @@ test('testall emits required final summary line', () => {
const source = fs.readFileSync(path.resolve(__dirname, '..', 'index.js'), 'utf8');
assert.ok(source.includes('TestAll complete — ${totalPassed} passed, ${totalWarnings} warnings, ${totalFailed} failures.'), 'Expected TestAll final summary string');
});


test('referral menu includes required friend-invite actions', () => {
const source = fs.readFileSync(path.resolve(__dirname, '..', 'index.js'), 'utf8');
for (const cb of ['menu_referral', 'ref_menu_code', 'ref_menu_share', 'ref_menu_how']) {
assert.ok(source.includes(cb), `Expected referral callback: ${cb}`);
}
assert.ok(source.includes('Were you referred by a friend?'), 'Expected onboarding referral prompt text');
assert.ok(source.includes('await_referral_code'), 'Expected referral onboarding pending action');
});

test('testall covers required diagnostic category labels', () => {
const source = fs.readFileSync(path.resolve(__dirname, '..', 'index.js'), 'utf8');
for (const cat of ['Environment Checks', 'Database Checks', 'Navigation Checks', 'PendingAction Checks']) {
assert.ok(source.includes(cat), `Expected TestAll category/check mention: ${cat}`);
}
});
59 changes: 59 additions & 0 deletions test/unit.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -472,6 +472,32 @@ test('discord links remain external and unwrap telegram wrappers', () => {
assert.ok(!out.includes('t.me'));
});



function referralShareHTML(code) {
return `<b>🚨 YO! I’m farming free SC on Runewager and you need to get in NOW.</b>

Daily giveaways, instant rewards, drops, and a secret new-user bonus waiting inside the bot.

Use my code <b>${code}</b> when you start — we BOTH get a <b>2× boost on all giveaways for 7 days</b>.

👉 <a href="https://t.me/RunewagerBot">Tap here to join Runewager</a>

Don’t sleep. This thing prints. ⚡👑`;
}

function applyOnboardingReferralCodeLike(user, owner, codeInput, ownerCode = 'abc123') {
const code = String(codeInput || '').trim().toLowerCase();
if (!code) return { ok: false, reason: 'Please enter a referral code.' };
if (!user.ageConfirmed) return { ok: false, reason: 'Referral codes can only be entered during onboarding.' };
if ((user.referralAppliedAt || 0) > 0 || (user.referredByUserId || 0) > 0) return { ok: false, reason: 'Referral code already used on this account.' };
if (user.onboarding && user.onboarding.completedAt) return { ok: false, reason: 'Referral code can only be entered before onboarding is complete.' };
if (String(ownerCode).toLowerCase() !== code) return { ok: false, reason: 'Referral code not found.' };
if (owner.id === user.id) return { ok: false, reason: 'You cannot use your own referral code.' };
const expiresAt = 1700000000000 + 7 * 24 * 60 * 60 * 1000;
return { ok: true, ownerId: owner.id, expiresAt };
}

// ---------------------------------------------------------------------------
// Promo claim limit check (pure logic)
// ---------------------------------------------------------------------------
Expand All @@ -491,3 +517,36 @@ test('remaining claims decrements correctly', () => {
remainingClaims = 0;
assert.ok(remainingClaims <= 0); // no more claims
});


test('referral share html includes code', () => {
const html = referralShareHTML('rwabc');
assert.ok(html.includes('<b>rwabc</b>'));
assert.ok(html.includes('2× boost'));
assert.ok(html.includes('https://t.me/RunewagerBot'));
});

test('referral abuse prevention blocks self-referral', () => {
const user = { id: 1, ageConfirmed: true, referralAppliedAt: 0, referredByUserId: 0, onboarding: { completedAt: 0 } };
const owner = { id: 1 };
const out = applyOnboardingReferralCodeLike(user, owner, 'abc123');
assert.equal(out.ok, false);
assert.match(out.reason, /cannot use your own/i);
});

test('referral code accepted only during onboarding', () => {
const user = { id: 2, ageConfirmed: true, referralAppliedAt: 0, referredByUserId: 0, onboarding: { completedAt: 1700 } };
const owner = { id: 1 };
const out = applyOnboardingReferralCodeLike(user, owner, 'abc123');
assert.equal(out.ok, false);
assert.match(out.reason, /before onboarding is complete/i);
});

test('valid referral applies boost expiry shape', () => {
const user = { id: 2, ageConfirmed: true, referralAppliedAt: 0, referredByUserId: 0, onboarding: { completedAt: 0 } };
const owner = { id: 1 };
const out = applyOnboardingReferralCodeLike(user, owner, 'abc123');
assert.equal(out.ok, true);
assert.equal(out.ownerId, 1);
assert.ok(out.expiresAt > 1700000000000);
});