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
195 changes: 162 additions & 33 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -1851,10 +1851,10 @@ function adminDashboardKeyboard(page = 1) {
],
[
Markup.button.callback('🛟 Support Tools', 'admin_cat_support'),
Markup.button.callback('📈 Stats', 'admin_stats_menu'),
Markup.button.callback('🧪 Tests & Bugs', 'admin_cat_tests'),
],
[
Markup.button.callback('📄 Admin Help', 'help_tab_admin'),
Markup.button.callback('📈 Stats', 'admin_stats_menu'),
Markup.button.callback('▶️ Quick Actions', 'admin_dash_page_2'),
],
[Markup.button.callback('⬅️ Back to User Menu', 'to_main_menu')],
Expand Down Expand Up @@ -1888,6 +1888,7 @@ function adminGiveawayToolsKeyboard() {
function adminPromoToolsKeyboard() {
return Markup.inlineKeyboard([
[Markup.button.callback('🎛 Promo Manager', 'admin_promo_manager')],
[Markup.button.callback('ℹ️ Promo Step Guide', 'admin_pm_help')],
[Markup.button.callback('➕ Create Promo', 'admin_pm_create')],
[Markup.button.callback('✏️ Edit Promo', 'admin_pm_edit')],
[Markup.button.callback('⏸ Pause/Unpause', 'admin_pm_pause_toggle')],
Expand Down Expand Up @@ -2159,7 +2160,11 @@ function adminMainMenuKeyboard(user) {
],
[
Markup.button.callback('⚙️ System & Health', 'admin_cat_system'),
Markup.button.callback('📄 Admin Help', 'help_tab_admin'),
Markup.button.callback('🧪 Tests & Bugs', 'admin_cat_tests'),
],
[
Markup.button.callback('📟 VPS Console (/sshv)', 'sshv_open'),
Markup.button.callback('💡 Content Drops', 'admin_cmd_tips_dashboard'),
],
[Markup.button.callback(toggleLabel, 'admin_cmd_mode_toggle')],
[Markup.button.callback('↩ Back to User Menu', 'pamenu_back_user')],
Expand Down Expand Up @@ -3320,32 +3325,18 @@ function buildHelpPages(user) {
'/tiptoggle — Enable/disable tips feature',
'/tipsettings — Configure content drop settings',
'',
'🐞 BUG REPORTS',
'/bugreports — View all open bug reports',
'/exportbugs — Export all reports as plain text',
'/resolvebug <id> — Mark a report resolved',
'',
'📊 ANALYTICS',
'/funnel — Conversion funnel stats',
'/discord_stats — Discord verification stats',
'',
'🔧 SYSTEM',
'/testall — Full static self-test (every feature PASS/FAIL)',
'/testgiveaway — Fake giveaway end-to-end test',
'/health — Live health endpoint data',
'/version — Bot version, Node.js version, uptime',
'/deploy — Trigger live deploy from latest git commit',
'/deploy_status — Last deploy result and timing',
'/logs [lines] — Last N lines of bot log',
'/admin_log [lines] — Admin event log (NDJSON)',
'/admin_backup — Snapshot runtime state',
'🧪 BUGS + TESTS + SYSTEM COMMANDS',
'Moved to Admin Dashboard → Tests & Bugs / System Tools menus for easier use.',
'',
'ℹ️ All commands include smart errors and usage hints.',
'ℹ️ Use /on to enable admin view, /off to hide admin UI.',
].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')],
[Markup.button.callback('🛠 Open Admin Dashboard', 'open_admin_dashboard'), Markup.button.callback('🧪 Tests & Bugs', 'admin_cat_tests')],
navRow(6, total),
[Markup.button.callback('⬅️ Menu', 'to_main_menu')],
],
Expand Down Expand Up @@ -3608,7 +3599,7 @@ async function handleAnnounceCommand(ctx) {
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. After sending text, choose targets (DM/channel/group).');
await ctx.reply('Okay! What would you like to announce?\nYou can send normal text or HTML. After sending text, choose targets (DM/channel/group).', Markup.inlineKeyboard([[Markup.button.callback('⬅️ Return to Admin Menu', 'open_admin_dashboard')], [Markup.button.callback('❌ Cancel', 'admin_cancel')]]));
}

bot.command('A', handleAnnounceCommand);
Expand Down Expand Up @@ -4607,7 +4598,21 @@ bot.action('admin_cmd_announce_start', async (ctx) => {
bot.action('admin_cmd_tiptest', async (ctx) => {
if (!requireAdmin(ctx)) return;
await ctx.answerCbQuery();
await ctx.reply('Use /tiptest to send a test tip to the configured group.');
const enabled = tipsStore.tips.filter((t) => t.enabled);
if (!enabled.length) { await ctx.reply('No enabled content drops to test.'); return; }
const pool = enabled.length > 1 && tipsStore.lastSentTipId != null
? enabled.filter((t) => t.id !== tipsStore.lastSentTipId)
: enabled;
const tip = pool[Math.floor(Math.random() * pool.length)];
const sendResult = await postTipToConfiguredTarget(tip, bot.telegram);
if (sendResult.ok) {
tipsStore.lastSentTipId = tip.id;
persistRuntimeState();
saveHelpfulMessages();
await ctx.reply(`✅ Test content drop posted to ${sendResult.target} (silent).`, Markup.inlineKeyboard([[Markup.button.callback('💡 Open Content Drops Manager', 'admin_cmd_tips_dashboard')], [Markup.button.callback('⬅️ Admin Dashboard', 'open_admin_dashboard')]]));
} else {
await ctx.reply(`❌ Failed to post test content drop. ${sendResult.error ? sendResult.error.message : 'Unknown error'}\nTip: use /register_chat or forward any message from the target group/channel here to auto-register.`);
}
});

bot.action('admin_broadcast', async (ctx) => {
Expand Down Expand Up @@ -4711,7 +4716,7 @@ bot.action('pamenu_active_giveaways', async (ctx) => {
const running = Array.from(giveawayStore.running.values());
const text = buildActiveGiveawaysText();
const keyboard = activeGiveawaysKeyboard(running);
await ctx.reply(text, { parse_mode: 'Markdown', ...keyboard });
await replaceCallbackPanel(ctx, text, { parse_mode: 'Markdown', ...keyboard });
});

bot.action('pamenu_tools', async (ctx) => {
Expand Down Expand Up @@ -4772,6 +4777,11 @@ bot.action('pamenu_back_admin', async (ctx) => {
const user = getUser(ctx);
await ctx.answerCbQuery();
if (!requireAdmin(ctx)) return;
try {
await ctx.deleteMessage();
} catch (_) {
try { await ctx.editMessageReplyMarkup({ inline_keyboard: [] }); } catch (_) { /* ignore */ }
}
await sendPersistentAdminMenu(ctx, user);
});

Expand Down Expand Up @@ -5403,6 +5413,33 @@ bot.action('admin_cat_support', async (ctx) => {
await replaceCallbackPanel(ctx, '🛟 *Support Tools*', { parse_mode: 'Markdown', ...adminSupportToolsKeyboard() });
});


function adminTestsToolsKeyboard() {
return Markup.inlineKeyboard([
[Markup.button.callback('🧪 Run TestAll', 'admin_cmd_testall'), Markup.button.callback('🎁 Test Giveaway', 'admin_cmd_testgiveaway')],
[Markup.button.callback('🐛 View Bug Reports', 'admin_cmd_viewbugs'), Markup.button.callback('📤 Export Bugs', 'admin_cmd_exportbugs')],
[Markup.button.callback('✅ Resolve Bug', 'admin_cmd_resolvebug_prompt'), Markup.button.callback('🧪 Test Content Drop', 'admin_cmd_tiptest')],
[Markup.button.callback('📟 Open SSHV Console', 'sshv_open')],
[Markup.button.callback('⬅️ Admin Dashboard', 'open_admin_dashboard')],
]);
}

bot.action('admin_cat_tests', async (ctx) => {
if (!requireAdmin(ctx)) return;
await ctx.answerCbQuery();
await replaceCallbackPanel(ctx,
`🧪 *Tests & Bug Tools*

Use this menu for diagnostics, bug workflows, and test sends.

• TestAll = full sanity check
• Test Giveaway = simulated giveaway flow
• Test Content Drop = sends one random drop to current target
• SSHV = VPS console tools`,
{ parse_mode: 'Markdown', ...adminTestsToolsKeyboard() },
);
});

// Inline triggers for admin dashboard quick-action buttons
bot.action('admin_cmd_start_giveaway', async (ctx) => {
if (!requireAdmin(ctx)) return;
Expand All @@ -5415,9 +5452,29 @@ bot.action('admin_cmd_giveaway_status', async (ctx) => {
if (!requireAdmin(ctx)) return;
await ctx.answerCbQuery();
const running = Array.from(giveawayStore.running.values());
if (!running.length) { await ctx.reply('No giveaways currently running.'); return; }
const lines = running.map((g) => `#${g.id} — ${g.participants.size} entries — ends ${new Date(g.endTime).toUTCString()}`);
await ctx.reply(`📊 Running Giveaways (${running.length}):\n\n${lines.join('\n')}`);
if (!running.length) {
await replaceCallbackPanel(ctx, `🎁 *Active Giveaways*\n\nNo giveaways are currently running.`, {
parse_mode: 'Markdown',
...Markup.inlineKeyboard([[Markup.button.callback('⬅️ Return to Admin Menu', 'pamenu_back_admin')]]),
});
return;
}
const lines = running.map((g) => {
const remainSec = Math.max(0, Math.floor((g.endTime - Date.now()) / 1000));
const remain = remainSec > 3600
? `${Math.floor(remainSec / 3600)}h ${Math.floor((remainSec % 3600) / 60)}m`
: `${Math.floor(remainSec / 60)}m ${remainSec % 60}s`;
return `#${g.id} — ${g.participants.size} entries — ⏱ ${remain} remaining`;
});
const actionRows = running.slice(0, 5).map((g) => ([
Markup.button.callback(`🎉 Join #${g.id}`, `gw_join_${g.id}`),
Markup.button.callback(`📋 Details #${g.id}`, `gw_details_${g.id}`),
]));
actionRows.push([Markup.button.callback('⬅️ Return to Admin Menu', 'pamenu_back_admin')]);
await replaceCallbackPanel(ctx, `📊 *Running Giveaways* (${running.length})\n\n${lines.join('\\n')}`, {
parse_mode: 'Markdown',
...Markup.inlineKeyboard(actionRows),
});
});

bot.action('admin_cmd_testgiveaway', async (ctx) => {
Expand All @@ -5441,7 +5498,29 @@ bot.action('admin_cmd_health', async (ctx) => {
bot.action('admin_cmd_version', async (ctx) => {
if (!requireAdmin(ctx)) return;
await ctx.answerCbQuery();
await ctx.reply('Use /version to view bot version info.');
const commitHash = process.env.COMMIT_HASH || 'local';
const deployTime = process.env.DEPLOY_TIME || 'unknown';
const uptime = Math.floor(process.uptime());
const uptimeStr = `${Math.floor(uptime / 3600)}h ${Math.floor((uptime % 3600) / 60)}m ${uptime % 60}s`;
await replaceCallbackPanel(ctx,
`📦 *Bot Version Info*

Version: \`${pkgVersion}\`
Commit: \`${commitHash}\`
Deployed: ${deployTime}
Node: \`${process.version}\`
Uptime: ${uptimeStr}`,
{ parse_mode: 'Markdown', ...Markup.inlineKeyboard([
[Markup.button.callback('⚙️ System Tools', 'admin_cat_system')],
[Markup.button.callback('⬅️ Admin Dashboard', 'open_admin_dashboard')],
]) },
);
});

bot.action('admin_cmd_verify_setup', async (ctx) => {
if (!requireAdmin(ctx)) return;
await ctx.answerCbQuery();
await ctx.reply('Use /verify_bot_setup to validate BotFather setup, permissions, and targets.');
});

bot.action('admin_cmd_verify_setup', async (ctx) => {
Expand Down Expand Up @@ -5908,6 +5987,33 @@ bot.action('admin_promo_manager', async (ctx) => {
await replaceCallbackPanel(ctx, promoManagerSummaryText(), { parse_mode: 'Markdown', ...adminPromoToolsKeyboard() });
});


bot.action('admin_pm_help', async (ctx) => {
if (!requireAdmin(ctx)) return;
await ctx.answerCbQuery();
const guide = [
'ℹ️ *Promo Manager Step Guide*',
'',
'Create Promo is a 9-step wizard. Example values:',
'1) Name: `New User Bonus`',
'2) Code/Link: `WELCOME35` or `https://casino.example/promo`',
'3) Casino base URL: `casino.example`',
'4) Description: `Claim 3.5 SC for first-time users`',
'5) Image URL (optional): `https://.../promo.png` or `skip`',
'6) Requirement type: `A` New user, `B` Existing user, `C` Existing + wager',
' If C: enter wager SC amount, then rolling days (e.g. `3000`, `7`)',
'7) Claim limit: number or `none`',
'8) Cooldown hours: number or `none`',
'9) Approval mode: `auto` or `admin`',
'',
'Tips:',
'• Use base domain only in step 3 (no path/query).',
'• Use clear names so users know who the promo is for.',
'• You can preview, pause, and edit anytime from this menu.',
].join('\n');
await replaceCallbackPanel(ctx, guide, { parse_mode: 'Markdown', ...adminPromoToolsKeyboard() });
});

bot.action('admin_view', async (ctx) => {
if (!requireAdmin(ctx)) return;
await ctx.answerCbQuery();
Expand Down Expand Up @@ -6389,6 +6495,30 @@ bot.on('inline_query', safeStepHandler('inline_query', async (ctx) => {
await ctx.answerInlineQuery(filtered, { cache_time: 5, is_personal: true });
}));


function extractForwardedChat(msg = {}) {
return msg.forward_from_chat
|| (msg.forward_origin && msg.forward_origin.chat)
|| (msg.sender_chat ? msg.sender_chat : null)
|| null;
}

async function autoRegisterForwardedChatIfPresent(ctx, user) {
if (!isAdmin(ctx)) return false;
const fwdChat = extractForwardedChat(ctx.message || {});
if (!fwdChat || !fwdChat.id) return false;
const chatId = Number(fwdChat.id);
approvedGroupsStore.add(chatId);
tipsStore.targetGroup = String(chatId);
broadcastConfigStore.targetGroup = String(chatId);
persistRuntimeState();
await ctx.reply(`✅ Auto-registered target chat from forwarded message:
• ${fwdChat.title || chatId} (${chatId})

Content Drops + group broadcasts now use this target.`);
return true;
}

// =========================
// Text message handler for pending actions
// =========================
Expand Down Expand Up @@ -6429,6 +6559,8 @@ bot.on('text', async (ctx) => {
return;
}

if (!user.pendingAction && await autoRegisterForwardedChatIfPresent(ctx, user)) return;

// 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. Always requires confirmation — never auto-accepts.
Expand Down Expand Up @@ -6485,10 +6617,7 @@ bot.on('text', async (ctx) => {

if (action.type === 'await_register_chat_forward') {
if (!requireAdmin(ctx)) return;
const msg = ctx.message || {};
const fwdChat = msg.forward_from_chat
|| (msg.forward_origin && msg.forward_origin.chat)
|| (msg.sender_chat ? msg.sender_chat : null);
const fwdChat = extractForwardedChat(ctx.message || {});
if (!fwdChat || !fwdChat.id) {
await ctx.reply('Please forward a message from the target channel/group.');
return;
Expand All @@ -6499,10 +6628,10 @@ bot.on('text', async (ctx) => {
broadcastConfigStore.targetGroup = String(chatId);
user.pendingAction = null;
persistRuntimeState();
await ctx.reply(`✅ Registered chat for broadcasts/tips:
await ctx.reply(`✅ Registered chat for broadcasts/content drops:
• ${fwdChat.title || chatId} (${chatId})

Tips and group broadcasts will now use this target.`);
Content Drops and group broadcasts will now use this target.`);
return;
}

Expand Down
10 changes: 9 additions & 1 deletion test/smoke.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,15 @@ function extractLiteralIds(source, kind) {
const regex = kind === 'callback'
? /Markup\.button\.callback\([^,]+,\s*(["'`])((?:\\.|(?!\1).)*)\1\)/g
: /bot\.action\(\s*(["'`])((?:\\.|(?!\1).)*)\1/g;
return new Set(Array.from(source.matchAll(regex), (m) => m[2]));
const ids = [];
for (const m of source.matchAll(regex)) {
const quote = m[1];
const id = m[2];
// Skip templated callback ids (e.g. `help_page_${page + 1}`), they are dynamic.
if (kind === 'callback' && quote === '`' && id.includes('${')) continue;
ids.push(id);
}
return new Set(ids);
}

function extractActionRegexPatterns(source) {
Expand Down