diff --git a/index.js b/index.js
index 1472f84..1e923fc 100644
--- a/index.js
+++ b/index.js
@@ -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: '๐ก 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: 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 Hacksaw, BGaming, Betsoft, 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 Giveaways 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 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: 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 100% free to play. No deposits, no wagering, no risk โ just sweepstakes fun and instant crypto prizes.', enabled: true },
];
const tipsStore = {
@@ -1737,7 +1737,8 @@ 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')],
@@ -1745,7 +1746,7 @@ function adminSystemToolsKeyboard() {
[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')],
]);
}
@@ -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, '&')
+ .replace(//g, '>')
+ .replace(/"/g, '"')
+ .replace(/'/g, ''');
+}
+
+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 = [
@@ -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')],
]);
}
@@ -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;
@@ -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) => {
@@ -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) => {
@@ -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;
@@ -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')],
]);
}
@@ -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 โโ
@@ -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) => {
@@ -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":"My Tip","enabled":true}]');
});
bot.action('tips_cmd_settings', async (ctx) => {
@@ -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' });
diff --git a/prod-run.sh b/prod-run.sh
index f6a378f..f51f692 100755
--- a/prod-run.sh
+++ b/prod-run.sh
@@ -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