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
179 changes: 151 additions & 28 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -169,21 +169,21 @@ const giveawayStore = {
// Tips Store — rotating silent group posts
// =========================
const DEFAULT_TIPS_LIST = [
{ id: 1, text: '💡 *Did you know?* Runewager gives *instant crypto redemptions* on all sweepstakes prizes. No delays, no stress — just claim and flex.', enabled: true },
{ id: 2, text: '👋 New here? Tap /start in the bot to unlock your *FREE SC bonuses*, link your account, and join our giveaways.', enabled: true },
{ id: 3, text: '🏆 Runewager\'s weekly leaderboard is *LIVE* — climb the ranks, earn bragging rights, and win real prizes. All 100% free to play.', enabled: true },
{ id: 4, text: '🔗 *Pro tip:* Linking your Runewager username in the bot unlocks bonuses, giveaways, and faster support.', enabled: true },
{ id: 1, text: '💡 <b>Did you know?</b> Runewager gives <b>instant crypto redemptions</b> on all sweepstakes prizes. No delays, no stress — just claim and flex.', enabled: true },
{ id: 2, text: '👋 New here? Tap /start in the bot to unlock your <b>FREE SC bonuses</b>, link your account, and join our giveaways.', enabled: true },
{ id: 3, text: '🏆 Runewager\'s weekly leaderboard is <b>LIVE</b> — climb the ranks, earn bragging rights, and win real prizes. All 100% free to play.', enabled: true },
{ id: 4, text: '🔗 <b>Pro tip:</b> Linking your Runewager username in the bot unlocks bonuses, giveaways, and faster support.', enabled: true },
{ id: 5, text: '🎁 Want more SC? Join our Telegram channel for surprise drops, flash bonuses, and exclusive codes.', enabled: true },
{ id: 6, text: '🎮 Runewager features top providers like *Hacksaw*, *BGaming*, *Betsoft*, and more — all sweepstakes‑safe and free to play.', enabled: true },
{ id: 6, text: '🎮 Runewager features top providers like <b>Hacksaw</b>, <b>BGaming</b>, <b>Betsoft</b>, and more — all sweepstakes‑safe and free to play.', enabled: true },
{ id: 7, text: '🔒 Make sure you\'re Discord‑verified in the bot — it unlocks extra rewards and keeps your account secure.', enabled: true },
{ id: 8, text: '🎉 Tap *Giveaways* in the bot menu to join our SC giveaways with automatic eligibility checks.', enabled: true },
{ id: 8, text: '🎉 Tap <b>Giveaways</b> in the bot menu to join our SC giveaways with automatic eligibility checks.', enabled: true },
{ id: 9, text: '🌍 Runewager accepts players worldwide — no region blocks, no deposit requirements, just pure sweepstakes fun.', enabled: true },
{ id: 10, text: '💰 Your bonuses refresh often — check the bot for new user bonuses, 30 SC rewards, and seasonal promos.', enabled: true },
{ id: 11, text: '🤝 Want to help the community grow? Share the bot link with a friend — more players means bigger giveaways.', enabled: true },
{ id: 12, text: '❓ If you ever get stuck, tap *Help / Commands* in the bot for a full guide, tooltips, and troubleshooting.', enabled: true },
{ id: 13, text: '📱 Runewager\'s mini‑app loads instantly inside Telegram — or switch to browser mode in *Settings* if you prefer.', enabled: true },
{ id: 12, text: '❓ If you ever get stuck, tap <b>Help / Commands</b> in the bot for a full guide, tooltips, and troubleshooting.', enabled: true },
{ id: 13, text: '📱 Runewager\'s mini‑app loads instantly inside Telegram — or switch to browser mode in <b>Settings</b> if you prefer.', enabled: true },
{ id: 14, text: '👀 Stay active in the group — admins drop surprise SC codes and leaderboard boosts at random times.', enabled: true },
{ id: 15, text: '🆓 Reminder: Runewager is *100% free to play*. No deposits, no wagering, no risk — just sweepstakes fun and instant crypto prizes.', enabled: true },
{ id: 15, text: '🆓 Reminder: Runewager is <b>100% free to play</b>. No deposits, no wagering, no risk — just sweepstakes fun and instant crypto prizes.', enabled: true },
];

const tipsStore = {
Expand Down Expand Up @@ -1737,15 +1737,16 @@ function adminUserToolsKeyboard() {
]);
}

function adminSystemToolsKeyboard() {
function adminSystemToolsKeyboard(user = null) {
const toggleLabel = user && user.adminModeOn ? '🟢 Admin Mode: ON (tap to toggle)' : '⚪ Admin Mode: OFF (tap to toggle)';
return Markup.inlineKeyboard([
[Markup.button.callback('🛠 Run TestAll', 'admin_cmd_testall')],
[Markup.button.callback('📟 VPS Console (/sshv)', 'sshv_open')],
[Markup.button.callback('🩺 Health Check', 'admin_cmd_health')],
[Markup.button.callback('📦 Bot Version', 'admin_cmd_version')],
[Markup.button.callback('🧪 Tip Test (4h broadcast)', 'admin_cmd_tiptest')],
[Markup.button.callback('💾 Backup State', 'admin_backup_action')],
[Markup.button.callback('🔘 Admin Mode On', 'admin_cmd_mode_on'), Markup.button.callback('⚪ Admin Mode Off', 'admin_cmd_mode_off')],
[Markup.button.callback(toggleLabel, 'admin_cmd_mode_toggle')],
[Markup.button.callback('⬅️ Admin Dashboard', 'open_admin_dashboard')],
]);
}
Expand Down Expand Up @@ -1833,6 +1834,36 @@ async function replaceCallbackPanel(ctx, text, extra = {}) {
await ctx.reply(text, extra);
}

function extractCallbackDataFromKeyboard(markup) {
const rows = (markup && markup.reply_markup && Array.isArray(markup.reply_markup.inline_keyboard))
? markup.reply_markup.inline_keyboard
: [];
const out = [];
for (const row of rows) {
for (const btn of row || []) {
if (btn && typeof btn.callback_data === 'string') out.push(btn.callback_data);
}
}
return out;
}

function escapeHtml(text) {
return String(text)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
}

function formatTipForHtml(text) {
const t = String(text || '').trim();
if (!t) return '';
// If admin intentionally provided HTML tags, preserve them.
if (/[<][a-zA-Z/!][^>]*>/.test(t)) return t;
return escapeHtml(t);
}

async function sendSettingsMenu(ctx, user) {
const s = user.settings;
const lines = [
Expand Down Expand Up @@ -1937,13 +1968,14 @@ async function sendPersistentUserMenu(ctx, user) {
}

/** Keyboard for the persistent ADMIN MAIN MENU */
function adminMainMenuKeyboard() {
function adminMainMenuKeyboard(user) {
const toggleLabel = user && user.adminModeOn ? '🟢 Admin Mode: ON (tap to toggle)' : '⚪ Admin Mode: OFF (tap to toggle)';
return Markup.inlineKeyboard([
[Markup.button.callback('👤 Users & Stats', 'pamenu_stats')],
[Markup.button.callback('🎉 Giveaways', 'pamenu_active_giveaways')],
[Markup.button.callback('📣 Promo Tools', 'admin_cat_promo')],
[Markup.button.callback('⚙️ System Tools', 'admin_cat_system')],
[Markup.button.callback('🔧 Mode Toggle: ON (/on)', 'admin_cmd_mode_on'), Markup.button.callback('👤 Mode Toggle: OFF (/off)', 'admin_cmd_mode_off')],
[Markup.button.callback(toggleLabel, 'admin_cmd_mode_toggle')],
[Markup.button.callback('↩ Back to User Menu', 'pamenu_back_user')],
]);
}
Expand Down Expand Up @@ -1992,7 +2024,7 @@ async function sendPersistentAdminMenu(ctx, user) {
}

const text = adminMainMenuText(user);
const keyboard = adminMainMenuKeyboard();
const keyboard = adminMainMenuKeyboard(user);
const sent = await ctx.telegram.sendMessage(chatId, text, { parse_mode: 'Markdown', ...keyboard });
user.adminMenuMsgId = sent.message_id;
user.adminMenuChatId = sent.chat.id;
Expand Down Expand Up @@ -5164,7 +5196,8 @@ bot.action('admin_cat_user', async (ctx) => {
bot.action('admin_cat_system', async (ctx) => {
if (!requireAdmin(ctx)) return;
await ctx.answerCbQuery();
await replaceCallbackPanel(ctx, '⚙️ *System Tools*', { parse_mode: 'Markdown', ...adminSystemToolsKeyboard() });
const user = getUser(ctx);
await replaceCallbackPanel(ctx, '⚙️ *System Tools*', { parse_mode: 'Markdown', ...adminSystemToolsKeyboard(user) });
});

bot.action('admin_cat_support', async (ctx) => {
Expand Down Expand Up @@ -5226,22 +5259,33 @@ bot.action('admin_backup_action', async (ctx) => {
}
});

bot.action('admin_cmd_mode_toggle', async (ctx) => {
if (!requireAdmin(ctx)) return;
const user = getUser(ctx);
const next = !Boolean(user.adminModeOn);
persistAdminMode(user, next);
await ctx.answerCbQuery(next ? 'Admin mode enabled' : 'Admin mode disabled');
await refreshAdminMenuHeader(ctx, user);
await sendPersistentAdminMenu(ctx, user);
});

// Backward-compatible aliases
bot.action('admin_cmd_mode_on', async (ctx) => {
if (!requireAdmin(ctx)) return;
await ctx.answerCbQuery('Admin mode enabled');
const user = getUser(ctx);
persistAdminMode(user, true);
await ctx.answerCbQuery('Admin mode enabled');
await refreshAdminMenuHeader(ctx, user);
await ctx.reply('🔧 Admin Mode Enabled');
await sendPersistentAdminMenu(ctx, user);
});

bot.action('admin_cmd_mode_off', async (ctx) => {
if (!requireAdmin(ctx)) return;
await ctx.answerCbQuery('Admin mode disabled');
const user = getUser(ctx);
persistAdminMode(user, false);
await ctx.answerCbQuery('Admin mode disabled');
await refreshAdminMenuHeader(ctx, user);
await ctx.reply('👤 Admin Mode Disabled');
await sendPersistentAdminMenu(ctx, user);
});

bot.action('admin_cmd_whois_prompt', async (ctx) => {
Expand Down Expand Up @@ -6916,8 +6960,8 @@ function startTipsScheduler() {
logEvent('warn', 'Skipping helpful message: missing permission in target group', { target: tipsStore.targetGroup });
return;
}
await bot.telegram.sendMessage(tipsStore.targetGroup, tip.text, {
parse_mode: 'Markdown',
await bot.telegram.sendMessage(tipsStore.targetGroup, formatTipForHtml(tip.text), {
parse_mode: 'HTML',
disable_notification: true,
});
tipsStore.lastSentTipId = tip.id;
Expand All @@ -6935,6 +6979,7 @@ function tipsDashboardKeyboard() {
[Markup.button.callback('❌ Remove Tip', 'tips_cmd_remove'), Markup.button.callback('🔁 Toggle System', 'tips_cmd_toggle')],
[Markup.button.callback('📋 View All Tips', 'tips_cmd_list'), Markup.button.callback('🧪 Test Random Tip', 'tips_cmd_test')],
[Markup.button.callback('⚙️ Settings', 'tips_cmd_settings')],
[Markup.button.callback('📥 Import Batch JSON', 'tips_cmd_import_batch')],
[Markup.button.callback('↩ Admin Menu', 'pamenu_back_admin')],
]);
}
Expand All @@ -6958,7 +7003,7 @@ async function sendTipsDashboard(ctx) {
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}`;
await ctx.reply(text, { parse_mode: 'Markdown', ...tipsDashboardKeyboard() });
await replaceCallbackPanel(ctx, text, { parse_mode: 'Markdown', ...tipsDashboardKeyboard() });
}

// ── /tips /tiplist /tipadd /tipremove /tipedit /tiptoggle /tiptest /tipsettings ──
Expand Down Expand Up @@ -7058,9 +7103,23 @@ bot.command('tiptoggle', async (ctx) => {
bot.command('tiptest', async (ctx) => {
if (!requireAdmin(ctx)) return;
const enabled = tipsStore.tips.filter((t) => t.enabled);
if (!enabled.length) { await ctx.reply('No enabled tips to preview.'); return; }
const tip = enabled[Math.floor(Math.random() * enabled.length)];
await ctx.reply(`Here is a random tip preview:\n\n${tip.text}`, { parse_mode: 'Markdown' });
if (!enabled.length) { await ctx.reply('No enabled tips 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)];
try {
await bot.telegram.sendMessage(tipsStore.targetGroup, formatTipForHtml(tip.text), {
parse_mode: 'HTML',
disable_notification: true,
});
tipsStore.lastSentTipId = tip.id;
persistRuntimeState();
saveHelpfulMessages();
await ctx.reply(`✅ Test tip posted to ${tipsStore.targetGroup} (silent).`);
} catch (e) {
await ctx.reply(`❌ Failed to post test tip to ${tipsStore.targetGroup}: ${e.message}`);
}
});

bot.command('tipsettings', async (ctx) => {
Expand Down Expand Up @@ -7123,9 +7182,33 @@ bot.action('tips_cmd_test', async (ctx) => {
if (!requireAdmin(ctx)) return;
await ctx.answerCbQuery();
const enabled = tipsStore.tips.filter((t) => t.enabled);
if (!enabled.length) { await ctx.reply('No enabled tips to preview.'); return; }
const tip = enabled[Math.floor(Math.random() * enabled.length)];
await ctx.reply(`Here is a random tip preview:\n\n${tip.text}`, { parse_mode: 'Markdown' });
if (!enabled.length) { await ctx.reply('No enabled tips 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)];
try {
await bot.telegram.sendMessage(tipsStore.targetGroup, formatTipForHtml(tip.text), {
parse_mode: 'HTML',
disable_notification: true,
});
tipsStore.lastSentTipId = tip.id;
persistRuntimeState();
saveHelpfulMessages();
await ctx.reply(`✅ Test tip posted to ${tipsStore.targetGroup} (silent).`);
} catch (e) {
await ctx.reply(`❌ Failed to post test tip to ${tipsStore.targetGroup}: ${e.message}`);
}
await sendTipsDashboard(ctx);
});

bot.action('tips_cmd_import_batch', async (ctx) => {
if (!requireAdmin(ctx)) return;
const user = getUser(ctx);
await ctx.answerCbQuery();
clearPendingAction(user);
user.pendingAction = { type: 'await_tip_add_text' };
await ctx.reply('Paste a JSON array to append tips (supports plain text or HTML):\n\n/tipadd [{"text":"<b>My Tip</b>","enabled":true}]');
});

bot.action('tips_cmd_settings', async (ctx) => {
Expand Down Expand Up @@ -8158,6 +8241,46 @@ bot.command('testall', async (ctx) => {
else throw new Error('not a function');
} catch (e) { fail('Admin Dashboard', 'sendCommandError', e.message); }

// ── 6b. Menu & Callback Audit ─────────────────────────────────────────────
try {
const source = fs.readFileSync(__filename, 'utf8');
const callbackPattern = /bot\.action\((?:'([^']+)'|"([^"]+)")/g;
const registered = new Set();
let m;
while ((m = callbackPattern.exec(source)) !== null) {
registered.add((m[1] || m[2] || '').trim());
}

const sampleUser = createDefaultUser({ id: -101, username: 'menuaudit', first_name: 'Menu' });
sampleUser.adminModeOn = true;
const menuKeyboards = [
['adminMainMenuKeyboard', adminMainMenuKeyboard(sampleUser)],
['adminDashboardKeyboard1', adminDashboardKeyboard(1)],
['adminDashboardKeyboard2', adminDashboardKeyboard(2)],
['adminStatsKeyboard', adminStatsKeyboard()],
['adminGiveawayToolsKeyboard', adminGiveawayToolsKeyboard()],
['adminPromoToolsKeyboard', adminPromoToolsKeyboard()],
['adminUserToolsKeyboard', adminUserToolsKeyboard()],
['adminSystemToolsKeyboard', adminSystemToolsKeyboard(sampleUser)],
['adminSupportToolsKeyboard', adminSupportToolsKeyboard()],
['tipsDashboardKeyboard', tipsDashboardKeyboard()],
['userMainMenuKeyboard', userMainMenuKeyboard(true)],
];

for (const [name, kb] of menuKeyboards) {
const ids = extractCallbackDataFromKeyboard(kb);
const duplicates = ids.filter((id, idx) => ids.indexOf(id) !== idx);
if (duplicates.length === 0) pass('Menu & Callback Audit', `${name}.no_duplicate_buttons`);
else fail('Menu & Callback Audit', `${name}.no_duplicate_buttons`, `Duplicates: ${Array.from(new Set(duplicates)).join(', ')}`);

const missing = ids.filter((id) => !registered.has(id) && !id.startsWith('tip_') && !id.startsWith('page_') && !id.startsWith('walk_') && !id.startsWith('gw_') && !id.startsWith('tgw_dur_'));
if (missing.length === 0) pass('Menu & Callback Audit', `${name}.callbacks_registered`);
else fail('Menu & Callback Audit', `${name}.callbacks_registered`, `Missing: ${Array.from(new Set(missing)).join(', ')}`);
}
} catch (e) {
fail('Menu & Callback Audit', 'menu_callback_audit', e.message);
}

// ── 7. Settings System ────────────────────────────────────────────────────
try {
const freshUser = createDefaultUser({ id: -2, username: 'y', first_name: 'Y' });
Expand Down
7 changes: 7 additions & 0 deletions prod-run.sh
Original file line number Diff line number Diff line change
Expand Up @@ -453,6 +453,13 @@ if is_port_listening "$PRECHECK_PORT"; then
free_port_if_conflicted "$PRECHECK_PORT" "$PID" || true
fi

PRECHECK_HEALTH_URL="$(resolve_health_url)"
PRECHECK_PORT="${PRECHECK_HEALTH_URL#*://127.0.0.1:}"
PRECHECK_PORT="${PRECHECK_PORT%%/*}"
if is_port_listening "$PRECHECK_PORT"; then
free_port_if_conflicted "$PRECHECK_PORT" "$PID" || true
fi

if command -v systemctl >/dev/null 2>&1 && [[ -f "$SERVICE_FILE" ]]; then
if systemctl restart "${APP_NAME}.service" 2>&1; then
sleep 3
Expand Down
Loading