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
72 changes: 42 additions & 30 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,8 @@ const LINKS = {
rwDiscordJoin: process.env.RW_DISCORD_JOIN || 'https://discord.gg/runewagers',
rwDiscordLink: process.env.RW_DISCORD_LINK || 'https://discord.com/channels/1100486422395355197/1249181934811349052',
// Telegram channels/groups — external links
gczChannel: 'https://t.me/GambleCodezDrops',
gczGroup: 'https://t.me/GambleCodezPrizeHub',
gczChannel: process.env.TELEGRAM_CHANNEL_LINK || 'https://t.me/GambleCodezDrops',
gczGroup: process.env.TELEGRAM_GROUP_LINK || 'https://t.me/GambleCodezPrizeHub',
// Mini App links — always open INSIDE Telegram
miniAppPlay: process.env.MINI_APP_PLAY_URL || 'https://t.me/RuneWager_bot/Play',
miniAppProfile: process.env.MINI_APP_PROFILE_URL || 'https://t.me/RuneWager_bot/profile',
Expand All @@ -51,10 +51,12 @@ const PROMO_REQUIREMENT = {
};

// Channel username (or chat_id) for admin announcements via /announce
const ANNOUNCE_CHANNEL = process.env.ANNOUNCE_CHANNEL || '@GambleCodezDrops';
const TELEGRAM_CHANNEL_ID = process.env.TELEGRAM_CHANNEL_ID || process.env.ANNOUNCE_CHANNEL || '';
const TELEGRAM_GROUP_ID = process.env.TELEGRAM_GROUP_ID || '';
const ANNOUNCE_CHANNEL = TELEGRAM_CHANNEL_ID;

// Group username (or chat_id) for automatic silent tips
const TIPS_GROUP = process.env.TIPS_GROUP || '@GambleCodezPrizeHub';
// Group chat_id for automatic Content Drops
const TIPS_GROUP = process.env.TIPS_GROUP || TELEGRAM_GROUP_ID || '';
Comment on lines +54 to +59

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

issue (bug_risk): Empty defaults for TELEGRAM_CHANNEL_ID / TIPS_GROUP may silently disable broadcasts/drops.

These env vars used to have non-empty defaults (@GambleCodezDrops, @GambleCodezPrizeHub), but now can be '' if unset. If callers don’t explicitly check for non-empty IDs, broadcasts/drops may silently no-op instead of failing visibly. Consider either failing fast when these are missing (throw / prominent log) or retaining a non-empty default to avoid silent misconfiguration.


// ── Images ─────────────────────────────────────────────────────────────────
// Images live in /images/ next to index.js.
Expand Down Expand Up @@ -881,6 +883,7 @@ function getUser(ctx) {
if (user.supportTicket === undefined) user.supportTicket = null;
if (!user.claimedPromoCodes) user.claimedPromoCodes = [];
if (!Array.isArray(user.claimedPromoCodes)) user.claimedPromoCodes = [];
if (user.hasClaimedNewUserPromo === undefined) user.hasClaimedNewUserPromo = false;
if (!user.claimedPromoAt) user.claimedPromoAt = 0;
if (!user.lastAnyPromoClaimAt) user.lastAnyPromoClaimAt = 0;
if (user.claimedPromo && !user.claimedPromoAt) user.claimedPromoAt = Date.now();
Expand Down Expand Up @@ -1710,7 +1713,7 @@ async function evaluatePromoEligibility(user, promo, { logFailure = false } = {}
const reasons = [];
const userId = Number(user.id);
const anyRedeemed = hasRedeemedAnyPromo(userId);
if (promo.new_user_only && anyRedeemed) reasons.push('New-user promo only. You already redeemed a promo.');
if (promo.new_user_only && (anyRedeemed || user.hasClaimedNewUserPromo)) reasons.push('New-user promo only. You already redeemed a promo.');
if (promo.existing_user_only && !promo.new_user_only && !anyRedeemed) reasons.push('This promo is only for existing promo users.');
if (promo.requirement_type === PROMO_REQUIREMENT.EXISTING_USER_WITH_WAGER) {
const wager = await getRollingWagerForUser(user, promo.required_wager_days || 7);
Expand Down Expand Up @@ -1893,7 +1896,7 @@ function adminPromoToolsKeyboard() {
[Markup.button.callback('👀 Preview Promo', 'admin_pm_preview')],
[Markup.button.callback('🧾 Approval Queue', 'admin_pm_queue')],
[Markup.button.callback('📣 Announcements', 'admin_cmd_announce_start')],
[Markup.button.callback('💡 Tips Manager', 'admin_cmd_tips_dashboard')],
[Markup.button.callback('💡 Content Drops', 'admin_cmd_tips_dashboard')],
[Markup.button.callback('⬅️ Admin Dashboard', 'open_admin_dashboard')],
]);
}
Expand All @@ -1917,7 +1920,7 @@ function adminSystemToolsKeyboard(user = null) {
[Markup.button.callback('🩺 Health Check', 'admin_cmd_health')],
[Markup.button.callback('📦 Bot Version', 'admin_cmd_version')],
[Markup.button.callback('🤖 Verify Bot Setup', 'admin_cmd_verify_setup')],
[Markup.button.callback('🧪 Tip Test (4h broadcast)', 'admin_cmd_tiptest')],
[Markup.button.callback('🧪 Drop Test (4h broadcast)', 'admin_cmd_tiptest')],
[Markup.button.callback('💾 Backup State', 'admin_backup_action')],
[Markup.button.callback(toggleLabel, 'admin_cmd_mode_toggle')],
[Markup.button.callback('⬅️ Admin Dashboard', 'open_admin_dashboard')],
Expand Down Expand Up @@ -2152,7 +2155,7 @@ function adminMainMenuKeyboard(user) {
],
[
Markup.button.callback('🎛 Promo Manager', 'admin_cat_promo'),
Markup.button.callback('📣 Announce & Tips', 'admin_cmd_announce_start'),
Markup.button.callback('📣 Broadcasts & Drops', 'admin_cmd_announce_start'),
],
[
Markup.button.callback('⚙️ System & Health', 'admin_cat_system'),
Expand Down Expand Up @@ -3308,14 +3311,14 @@ function buildHelpPages(user) {
'/unapprove_group <chat_id> — Remove group from whitelist',
'/list_groups — List all approved groups',
'',
'💡 TIPS MANAGEMENT',
'/tips (or /t, /tp) — Post a tip to the group',
'💡 CONTENT DROPS MANAGEMENT',
'/tips (or /t, /tp) — Post a content drop to the group',
'/tiplist — List all tips',
'/tipadd — Add a new tip',
'/tipremove — Remove a tip',
'/tipedit — Edit an existing tip',
'/tiptoggle — Enable/disable tips feature',
'/tipsettings — Configure tips settings',
'/tipsettings — Configure content drop settings',
'',
'🐞 BUG REPORTS',
'/bugreports — View all open bug reports',
Expand Down Expand Up @@ -5195,10 +5198,19 @@ bot.action('menu_claim_bonus', async (ctx) => {
}

const rows = eligiblePromos.map((promo) => [Markup.button.callback(`🎁 ${promo.name}`, `promo_open_${promo.promo_id}`)]);
rows.push([Markup.button.callback('✅ I claimed successfully', 'promo_user_claimed_successfully')]);
rows.push([Markup.button.callback('⬅️ Main Menu', 'to_main_menu')]);
await ctx.reply(`🎁 *Promo Menu*\n\nOnly promos you are eligible for are shown below.`, { parse_mode: 'Markdown', ...Markup.inlineKeyboard(rows) });
});

bot.action('promo_user_claimed_successfully', async (ctx) => {
const user = getUser(ctx);
user.hasClaimedNewUserPromo = true;
user.lastAnyPromoClaimAt = Date.now();
await ctx.answerCbQuery('Saved');
await ctx.reply('✅ Got it — you are marked as already claimed. New-user-only promos are now hidden for your account.');
Comment on lines +5206 to +5211

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

issue (bug_risk): Marking hasClaimedNewUserPromo and lastAnyPromoClaimAt is not persisted, so state is lost on process restart.

This handler mutates user.hasClaimedNewUserPromo and user.lastAnyPromoClaimAt but, unlike other user updates (e.g. promo creation, tips settings), it doesn’t call the central persistence mechanism (e.g. persistRuntimeState()). If getUser(ctx) returns an in-memory object, these flags will be reset on restart and users can see new-user promos again. Please persist the user after updating these fields so the “claimed successfully” state survives restarts.

});

bot.action(/promo_open_(\d+)/, async (ctx) => {
const user = getUser(ctx);
const promoId = Number(ctx.match[1]);
Expand Down Expand Up @@ -7131,7 +7143,7 @@ C = Existing User With Wager Requirement`);
if (action.type === 'await_tip_add_text') {
if (!requireAdmin(ctx)) return;
if (!text) {
await ctx.reply('Tip text cannot be empty. Please send the tip you want to add.');
await ctx.reply('Drop text cannot be empty. Please send the tip you want to add.');
return;
}
const newTip = { id: tipsStore.nextTipId, text, enabled: true };
Expand All @@ -7140,15 +7152,15 @@ C = Existing User With Wager Requirement`);
user.pendingAction = null;
persistRuntimeState();
saveHelpfulMessages();
await ctx.reply(`Added as Tip #${newTip.id}.`);
await ctx.reply(`Added as Drop #${newTip.id}.`);
return;
}

// Tips system: await edited tip text
if (action.type === 'await_tip_edit_text') {
if (!requireAdmin(ctx)) return;
if (!text) {
await ctx.reply('Tip text cannot be empty. Please send the updated tip text.');
await ctx.reply('Drop text cannot be empty. Please send the updated tip text.');
return;
}
const tipId = action.data && action.data.tipId;
Expand All @@ -7162,7 +7174,7 @@ C = Existing User With Wager Requirement`);
user.pendingAction = null;
persistRuntimeState();
saveHelpfulMessages();
await ctx.reply(`Tip #${tipId} updated.`);
await ctx.reply(`Drop #${tipId} updated.`);
return;
}

Expand Down Expand Up @@ -7395,7 +7407,7 @@ bot.action('announce_send_channel', async (ctx) => {
});

// =========================
// Tips System — scheduler + admin commands + callbacks
// Content Drops System — scheduler + admin commands + callbacks
// =========================

async function resolveTipTargetChatId(rawTarget) {
Expand Down Expand Up @@ -7454,7 +7466,7 @@ function startTipsScheduler() {
}, ms);
}

/** Build the Tips Manager dashboard keyboard */
/** Build the Content Drops Manager dashboard keyboard */
function tipsDashboardKeyboard() {
return Markup.inlineKeyboard([
[Markup.button.callback('➕ Add Tip', 'tips_cmd_add'), Markup.button.callback('✏️ Edit Tip', 'tips_cmd_edit')],
Expand All @@ -7479,12 +7491,12 @@ function tipSelectKeyboard(action) {
return Markup.inlineKeyboard(rows);
}

/** Send (or re-send) the Tips Manager dashboard */
/** Send (or re-send) the Content Drops Manager dashboard */
async function sendTipsDashboard(ctx) {
const total = tipsStore.tips.length;
const enabled = tipsStore.tips.filter((t) => t.enabled).length;
const status = tipsStore.systemEnabled ? '🟢 Enabled' : '🔴 Disabled';
const text = `📝 *Tips Manager*\n\nStatus: ${status}\nTotal Tips: ${total} (${enabled} active)\nInterval: every ${tipsStore.intervalHours}h\nTarget: ${tipsStore.targetGroup}`;
const text = `📝 *Content Drops Manager*\n\nStatus: ${status}\nTotal Drops: ${total} (${enabled} active)\nInterval: every ${tipsStore.intervalHours}h\nTarget: ${tipsStore.targetGroup}`;
await replaceCallbackPanel(ctx, text, { parse_mode: 'Markdown', ...tipsDashboardKeyboard() });
}

Expand All @@ -7501,7 +7513,7 @@ bot.command('tp', handleTipsCommand);

bot.command('tiplist', async (ctx) => {
if (!requireAdmin(ctx)) return;
const lines = ['📋 *Current Tips:*\n'];
const lines = ['📋 *Current Content Drops:*\n'];
for (const [idx, tip] of tipsStore.tips.entries()) {
const preview = tip.text.replace(/\*/g, '').slice(0, 80);
lines.push(`${idx + 1}. #${tip.id} ${tip.enabled ? '✅' : '🔇'} ${preview}${tip.text.length > 80 ? '…' : ''}`);
Expand Down Expand Up @@ -7578,7 +7590,7 @@ bot.command('tiptoggle', async (ctx) => {
tipsStore.systemEnabled = !tipsStore.systemEnabled;
persistRuntimeState();
saveHelpfulMessages();
const status = tipsStore.systemEnabled ? '🟢 Tips System Enabled' : '🔴 Tips System Disabled';
const status = tipsStore.systemEnabled ? '🟢 Content Drops System Enabled' : '🔴 Content Drops System Disabled';
await ctx.reply(status);
});

Expand All @@ -7604,7 +7616,7 @@ Tip: run /register_chat and forward a message from the target group/channel.`);

bot.command('tipsettings', async (ctx) => {
if (!requireAdmin(ctx)) return;
const text = `⚙️ *Tips Settings*\n\nInterval: every ${tipsStore.intervalHours} hours\nSilent mode: Always ON\nTarget group: ${tipsStore.targetGroup}\n\nTo change the interval, reply with the number of hours (e.g. \`4\`).`;
const text = `⚙️ *Content Drops Settings*\n\nInterval: every ${tipsStore.intervalHours} hours\nSilent mode: Always ON\nTarget group: ${tipsStore.targetGroup}\n\nTo change the interval, reply with the number of hours (e.g. \`4\`).`;
const user = getUser(ctx);
clearPendingAction(user);
user.pendingAction = { type: 'await_tip_settings_interval' };
Expand Down Expand Up @@ -7642,15 +7654,15 @@ bot.action('tips_cmd_toggle', async (ctx) => {
tipsStore.systemEnabled = !tipsStore.systemEnabled;
persistRuntimeState();
saveHelpfulMessages();
const status = tipsStore.systemEnabled ? '🟢 Tips System Enabled' : '🔴 Tips System Disabled';
const status = tipsStore.systemEnabled ? '🟢 Content Drops System Enabled' : '🔴 Content Drops System Disabled';
await ctx.reply(status);
await sendTipsDashboard(ctx);
});

bot.action('tips_cmd_list', async (ctx) => {
if (!requireAdmin(ctx)) return;
await ctx.answerCbQuery();
const lines = ['📋 *Current Tips:*\n'];
const lines = ['📋 *Current Content Drops:*\n'];
for (const [idx, tip] of tipsStore.tips.entries()) {
const preview = tip.text.replace(/\*/g, '').slice(0, 80);
lines.push(`${idx + 1}. #${tip.id} ${tip.enabled ? '✅' : '🔇'} ${preview}${tip.text.length > 80 ? '…' : ''}`);
Expand Down Expand Up @@ -7695,7 +7707,7 @@ bot.action('tips_cmd_settings', async (ctx) => {
const user = getUser(ctx);
clearPendingAction(user);
user.pendingAction = { type: 'await_tip_settings_interval' };
const text = `⚙️ *Tips Settings*\n\nInterval: every ${tipsStore.intervalHours} hours\nSilent mode: Always ON\nTarget group: ${tipsStore.targetGroup}\n\nSend the new interval in hours (e.g. \`4\`).`;
const text = `⚙️ *Content Drops Settings*\n\nInterval: every ${tipsStore.intervalHours} hours\nSilent mode: Always ON\nTarget group: ${tipsStore.targetGroup}\n\nSend the new interval in hours (e.g. \`4\`).`;
await ctx.reply(text, { parse_mode: 'Markdown' });
});

Expand All @@ -7717,7 +7729,7 @@ bot.action(/^tip_remove_(\d+)$/, async (ctx) => {
tipsStore.tips.splice(idx, 1);
persistRuntimeState();
saveHelpfulMessages();
await ctx.reply(`Tip #${tipId} removed.`);
await ctx.reply(`Drop #${tipId} removed.`);
});

// tip_edit_select_<id>
Expand All @@ -7730,7 +7742,7 @@ bot.action(/^tip_edit_select_(\d+)$/, async (ctx) => {
const user = getUser(ctx);
clearPendingAction(user);
user.pendingAction = { type: 'await_tip_edit_text', data: { tipId } };
await ctx.reply(`Send the updated text for Tip #${tipId}.\n\nCurrent text:\n${tip.text}`);
await ctx.reply(`Send the updated text for Drop #${tipId}.\n\nCurrent text:\n${tip.text}`);
});

// tip_toggle_<id> (per-tip enable/disable, accessible from tiplist)
Expand All @@ -7743,7 +7755,7 @@ bot.action(/^tip_toggle_(\d+)$/, async (ctx) => {
tip.enabled = !tip.enabled;
persistRuntimeState();
saveHelpfulMessages();
await ctx.reply(`Tip #${tipId} is now ${tip.enabled ? '✅ enabled' : '🔇 disabled'}.`);
await ctx.reply(`Drop #${tipId} is now ${tip.enabled ? '✅ enabled' : '🔇 disabled'}.`);
});

// =========================
Expand Down Expand Up @@ -9373,7 +9385,7 @@ bot.command('verify_bot_setup', safeAdminHandler('verify_bot_setup', { usage: '/
`Can read all group messages: ${me.can_read_all_group_messages ? 'YES' : 'NO'} (BotFather privacy mode affects this)`,
`Supports inline queries: ${me.supports_inline_queries ? 'YES' : 'NO'}`,
`Announce channel target: ${broadcastConfigStore.targetChannel}`,
`Tips/broadcast group target: ${broadcastConfigStore.targetGroup}`,
`Content Drops/broadcast group target: ${broadcastConfigStore.targetGroup}`,
`Approved broadcast/group chats tracked: ${approvedGroupsStore.size}`,
];
await ctx.reply(`🤖 *Bot Setup Verification*\n\n${checks.join('\n')}\n\nIf group visibility is limited, disable privacy mode in @BotFather and ensure admin rights in each target chat.`, { parse_mode: 'Markdown' });
Expand Down