diff --git a/RUNEWAGER_FUNCTIONALITY_MAP.md b/RUNEWAGER_FUNCTIONALITY_MAP.md index 8c06eda..eb48946 100644 --- a/RUNEWAGER_FUNCTIONALITY_MAP.md +++ b/RUNEWAGER_FUNCTIONALITY_MAP.md @@ -1,6 +1,6 @@ # RUNEWAGER_FUNCTIONALITY_MAP.md -_Last audited: 2026-02-26_ +_Last audited: 2026-02-28_ _Source of truth files: `index.js`, `test/*.test.js`, scripts under `scripts/`, deployment/runtime docs in repo root._ --- @@ -11,7 +11,7 @@ Runewager is a Telegraf-based Telegram bot that provides: - User onboarding (age gate, account/Discord guidance, username linking). - Promo flows (DB-backed promo manager + eligibility + claim lifecycle). - Giveaway flows (creation, join, eligibility checks, auto finalization, admin controls). -- Content Drops (scheduled/random posts to configured target chat). +- Helpful Tooltips (scheduled/random posts to configured target chat; formerly "Content Drops"). - Admin operations (broadcasts, diagnostics, SSHV console, bug triage, backups). - Runtime health and deploy tooling (`/health`, scripts, systemd template). @@ -25,7 +25,7 @@ Navigation is driven by inline menus plus command aliases. Persistent user/admin - Per-user mutable state stored in memory and persisted to JSON runtime snapshots. ### State/storage layers -- In-memory stores: users, giveaway state, analytics, promo manager store, content drops store, broadcast config, SSHV sessions. +- In-memory stores: users, giveaway state, analytics, promo manager store, helpful tooltips store (`tipsStore`), broadcast config, SSHV sessions. - File persistence under `data/` (runtime snapshots + promo DB + optional backups). - Periodic persistence timer + startup restore. @@ -80,10 +80,10 @@ Navigation is driven by inline menus plus command aliases. Persistent user/admin - System & Health - Tests & Bugs - VPS Console (/sshv) -- Content Drops manager shortcut +- Helpful Tooltips manager shortcut ### Admin category menus -- TestAll engine (`/testall`) runs structured diagnostics across environment, data/stores, callbacks/commands, navigation helpers, giveaway/promo/content-drop, SSHV, and pendingAction timeout/label rules; summary line: `TestAll complete — X passed, Y warnings, Z failures.` +- TestAll engine (`/testall`) runs structured diagnostics across environment, data/stores, callbacks/commands, navigation helpers, giveaway/promo/helpful-tooltips, SSHV, pendingAction timeout/label rules; summary line: `TestAll complete — X passed, Y warnings, Z failures.` - `admin_cat_giveaway`: start/test/status + persistent navigation row (`Admin Dashboard`, `Main Menu`, `Cancel`). - `admin_cat_promo`: full promo manager actions + guide + persistent navigation row (`Admin Dashboard`, `Main Menu`, `Cancel`). - `admin_cat_system`: health/version/verify/setup/backup/admin mode/testall/sshv + persistent navigation row (`Admin Dashboard`, `Main Menu`, `Cancel`). @@ -234,7 +234,7 @@ Pending-action timeout handling is enforced in the text input router: each pendi ### Verification controls - Smoke test enforces `REGISTERED_COMMANDS` parity with command handlers detected from `bot.command(...)` and `registerCommand(...)`, including single/double/backtick literals and simple const-driven names (except `start`, handled by `bot.start`). - Callback coverage smoke check ignores catch-all callback fallbacks using robust pattern detection (`/.*/`, `/^.*$/`, `/.+/` with optional flags/whitespace), so button coverage must be satisfied by literal handlers or meaningful regex families. -- Smoke checks assert presence of critical 2.0 command families (onboarding/promo/giveaway/content-drops/admin tools) and pending-action escape routes (`/cancel`, `/menu`, `/start`, `/help`). +- Smoke checks assert presence of critical 2.0 command families (onboarding/promo/giveaway/helpful-tooltips/admin tools) and pending-action escape routes (`/cancel`, `/menu`, `/start`, `/help`). - Smoke checks verify `.env.example` documents core runtime configuration keys used by production flows (Telegram links, HTTPS cert/key paths, mini-app URLs, Discord links, `BOT_PRIVACY_MODE`). @@ -308,24 +308,50 @@ admin_cmd_announce_start or /announce -> summary result ``` -### Content drop target registration +### Helpful Tooltip target registration ```text -Admin forwards message from channel/group - -> autoRegisterForwardedChatIfPresent - -> approvedGroupsStore add(chatId) +Admin taps "Link Channel/Group" in Tooltip Settings OR forwards any group/channel message + -> extractForwardedChat -> chatId + title extracted + -> await_tip_link_target or autoRegisterForwardedChatIfPresent + -> approvedGroupsStore.add(chatId) -> tipsStore.targetGroup = chatId + -> tipsStore.targetGroupTitle = title -> broadcastConfigStore.targetGroup = chatId + -> confirmation to admin (title + id) ``` -### Giveaway start/join +### Helpful Tooltip inline button syntax +```text +Tooltip text: + + [Label - https://url] && [Label2 - https://url2] ← same row + [Label3 - https://url3] ← new row + [Open Bot] ← standard "Open Bot" button + +parseTooltipButtons() strips button lines from text and builds Telegraf inlineKeyboard. +URL validation + label presence enforced; admin receives error message on malformed syntax. +``` + +### Giveaway start/join (v3.0+) ```text Admin starts wizard (gwiz) - -> collect config steps + -> giveawayPreflightCheck: validates group linked, warns if missing pin permission + -> collect config steps (9-step wizard) -> createGiveaway + announceGiveaway + -> post announcement to group + -> pin announcement (notify admin if permission missing) + -> scheduleGiveawayRefresh: timers at 25%, 50%, 75% of duration + -> each fires: edit/resend message, re-pin + -> scheduleGiveawayReminders: 10m, 5m, 1m, 30s, 10→1 countdown messages -> users click gw_join_ -> evaluateEligibility -> join accepted/rejected -> timer expires -> finalizeGiveaway + -> HTML winners announcement in group (parse_mode: HTML) + -> DM each winner with SC amount, boost status, "tip has been sent" notice + -> admin DM: full report (TG handle, display name, RW username, prize, boost) + + "View Results in Group" deep-link button + + Reroll/Mark Paid inline actions ``` ## 24. Future Updates Log diff --git a/add_tooltip.sh b/add_tooltip.sh new file mode 100755 index 0000000..e1d5066 --- /dev/null +++ b/add_tooltip.sh @@ -0,0 +1,63 @@ +#!/usr/bin/env bash +# add_tooltip.sh — Append a new placeholder tooltip entry to tooltips.json. +# Called by the bot admin panel "Add Tooltip (Script)" button, or manually. +# +# Usage: +# ./add_tooltip.sh [--text "Custom tooltip text"] +# +# Outputs: new tooltip ID on stdout. +# On success exits 0; on failure exits non-zero. + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +APP_DIR="${RUNEWAGER_DIR:-$SCRIPT_DIR}" +DATA_DIR="$APP_DIR/data" +TOOLTIPS_FILE="$DATA_DIR/tooltips.json" +TMP_FILE="$TOOLTIPS_FILE.tmp.$$" + +CUSTOM_TEXT="" +while [[ $# -gt 0 ]]; do + case "$1" in + --text) CUSTOM_TEXT="$2"; shift 2 ;; + *) shift ;; + esac +done + +info() { echo "[add_tooltip] INFO: $*"; } +error() { echo "[add_tooltip] ERROR: $*" >&2; exit 1; } + +mkdir -p "$DATA_DIR" || error "Cannot create data dir" + +# Ensure tooltips.json exists +if [[ ! -f "$TOOLTIPS_FILE" ]]; then + info "tooltips.json not found — initialising empty list" + echo '[]' > "$TOOLTIPS_FILE" +fi + +# Validate existing file +node -e "JSON.parse(require('fs').readFileSync('$TOOLTIPS_FILE','utf8'))" 2>/dev/null \ + || error "Existing $TOOLTIPS_FILE is not valid JSON" + +TOOLTIP_TEXT="${CUSTOM_TEXT:-New tooltip — edit in admin panel via /tips.}" + +# Append new entry and get new ID using Node.js +NEW_ID=$(node - "$TOOLTIPS_FILE" < Math.max(m, Number(t.id) || 0), 0); +const newId = maxId + 1; +list.push({ id: newId, text: $(node -e "process.stdout.write(JSON.stringify('$TOOLTIP_TEXT'))"), enabled: true }); +fs.writeFileSync('${TMP_FILE}', JSON.stringify(list, null, 2)); +console.log(newId); +EOF +) + +# Validate and move +node -e "JSON.parse(require('fs').readFileSync('$TMP_FILE','utf8'))" 2>/dev/null \ + || { rm -f "$TMP_FILE"; error "Generated JSON failed validation"; } + +mv "$TMP_FILE" "$TOOLTIPS_FILE" +info "Added tooltip #${NEW_ID}: ${TOOLTIP_TEXT:0:60}" +echo "$NEW_ID" diff --git a/deploy.sh b/deploy.sh index c08825d..ce56cf2 100755 --- a/deploy.sh +++ b/deploy.sh @@ -211,6 +211,24 @@ else exit 1 fi +# --------------------------------------------------------- +# 3b) Auto-run tooltip generation script (must run before bot restart) +# --------------------------------------------------------- +say "Step 3b — Refreshing Helpful Tooltips…" +TOOLTIP_SCRIPT="$PROJECT_DIR/generate_tooltips.sh" +if [[ -x "$TOOLTIP_SCRIPT" ]]; then + if TOOLTIP_OUT=$(RUNEWAGER_DIR="$PROJECT_DIR" bash "$TOOLTIP_SCRIPT" 2>&1); then + say "Helpful tooltips refreshed." + else + warn "generate_tooltips.sh failed (non-fatal): $TOOLTIP_OUT" + # DM admin but continue deploy + send_admin "⚠️ generate_tooltips.sh failed during deploy: ${TOOLTIP_OUT:0:200} — continuing deploy." + fi +else + warn "generate_tooltips.sh not found or not executable at $TOOLTIP_SCRIPT — skipping tooltip refresh." + send_admin "⚠️ generate_tooltips.sh missing at $TOOLTIP_SCRIPT — tooltips not refreshed." +fi + # --------------------------------------------------------- # 4) Start bot via systemctl # --------------------------------------------------------- diff --git a/generate_tooltips.sh b/generate_tooltips.sh new file mode 100755 index 0000000..9d03261 --- /dev/null +++ b/generate_tooltips.sh @@ -0,0 +1,75 @@ +#!/usr/bin/env bash +# generate_tooltips.sh — Refresh the Helpful Tooltips data file. +# Idempotent: safe to run on every deploy. Creates/overwrites tooltips.json +# from the source-of-truth DEFAULT_TIPS_LIST embedded in index.js. +# Called automatically by deploy.sh and prod-run.sh before bot restart. +# +# Usage: +# ./generate_tooltips.sh [--dry-run] +# --dry-run Print what would be written without making changes. + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +APP_DIR="${RUNEWAGER_DIR:-$SCRIPT_DIR}" +DATA_DIR="$APP_DIR/data" +TOOLTIPS_FILE="$DATA_DIR/tooltips.json" +TMP_FILE="$TOOLTIPS_FILE.tmp.$$" + +DRY_RUN=false +for arg in "$@"; do + [[ "$arg" == "--dry-run" ]] && DRY_RUN=true +done + +info() { echo "[generate_tooltips] INFO: $*"; } +warn() { echo "[generate_tooltips] WARN: $*" >&2; } +error() { echo "[generate_tooltips] ERROR: $*" >&2; exit 1; } + +# Ensure data directory exists +mkdir -p "$DATA_DIR" || error "Cannot create data dir: $DATA_DIR" + +# Extract DEFAULT_TIPS_LIST from index.js using Node.js +if [[ ! -f "$APP_DIR/index.js" ]]; then + error "index.js not found at $APP_DIR/index.js" +fi + +info "Extracting DEFAULT_TIPS_LIST from index.js..." +TOOLTIP_JSON=$(node - <<'EOF' +const fs = require('fs'); +const src = fs.readFileSync(process.argv[1] || 'index.js', 'utf8'); +// Execute just the DEFAULT_TIPS_LIST block and print it as JSON +const m = src.match(/const DEFAULT_TIPS_LIST\s*=\s*(\[[\s\S]+?\]);/); +if (!m) { process.stderr.write('DEFAULT_TIPS_LIST not found\n'); process.exit(1); } +try { + // Use Function constructor for safe eval of the array literal + const list = (new Function('return ' + m[1]))(); + console.log(JSON.stringify(list, null, 2)); +} catch (e) { process.stderr.write('Parse error: ' + e.message + '\n'); process.exit(1); } +EOF +node "$APP_DIR/index.js" --version 2>/dev/null || true +) || { + # Fallback: emit a minimal valid tooltips.json with a placeholder + warn "Could not extract tooltips from index.js — writing placeholder." + TOOLTIP_JSON='[{"id":1,"text":"Helpful tooltip placeholder — configure via /tips in the bot admin.","enabled":true}]' +} + +if [[ "$DRY_RUN" == "true" ]]; then + info "[dry-run] Would write to $TOOLTIPS_FILE:" + echo "$TOOLTIP_JSON" + exit 0 +fi + +# Atomic write: write to temp file, validate JSON, then move +echo "$TOOLTIP_JSON" > "$TMP_FILE" + +# Validate JSON before replacing +node -e "JSON.parse(require('fs').readFileSync('$TMP_FILE','utf8'))" 2>/dev/null || { + rm -f "$TMP_FILE" + # Write placeholder instead of failing + warn "Generated JSON failed validation — writing placeholder." + echo '[{"id":1,"text":"Helpful tooltip placeholder — configure via /tips in the bot admin.","enabled":true}]' > "$TMP_FILE" +} + +mv "$TMP_FILE" "$TOOLTIPS_FILE" +info "Helpful tooltips refreshed → $TOOLTIPS_FILE" +info "Total entries: $(node -e "console.log(JSON.parse(require('fs').readFileSync('$TOOLTIPS_FILE','utf8')).length)")" diff --git a/index.js b/index.js index b24475b..46798cb 100644 --- a/index.js +++ b/index.js @@ -140,7 +140,7 @@ const TELEGRAM_GROUP_ID = process.env.TELEGRAM_GROUP_ID || ''; const ANNOUNCE_CHANNEL = TELEGRAM_CHANNEL_ID; const BOT_PRIVACY_MODE = String(process.env.BOT_PRIVACY_MODE || process.env.BOTPRIVACYMODE || '').trim().toLowerCase(); -// Group chat_id for automatic Content Drops +// Group chat_id for automatic Helpful Tooltips const TIPS_GROUP = process.env.TIPS_GROUP || TELEGRAM_GROUP_ID || ''; // ── Images ───────────────────────────────────────────────────────────────── @@ -314,6 +314,8 @@ const tipsStore = { systemEnabled: true, intervalHours: 4, targetGroup: TIPS_GROUP, + targetGroupTitle: null, // human-readable name of linked group/channel + silentMode: true, // always ON per spec; stored so settings panel can display it nextTipId: 16, lastSentTipId: null, }; @@ -538,6 +540,8 @@ const ACTION_LABELS = { await_sshv_danger_confirm: 'dangerous SSHV confirmation', await_register_chat_forward: 'chat registration forward', await_referral_code: 'referral code entry', + await_tip_link_target: 'tooltip target group/channel link', + await_tip_button_syntax: 'tooltip inline button definition', }; /** @@ -952,6 +956,8 @@ function createRuntimeStateSnapshot() { systemEnabled: tipsStore.systemEnabled, intervalHours: tipsStore.intervalHours, targetGroup: tipsStore.targetGroup, + targetGroupTitle: tipsStore.targetGroupTitle, + silentMode: tipsStore.silentMode, nextTipId: tipsStore.nextTipId, }, broadcastConfigStore: { @@ -1205,6 +1211,8 @@ function loadRuntimeState() { if (typeof raw.tipsStore.targetGroup === 'string' && raw.tipsStore.targetGroup) { tipsStore.targetGroup = raw.tipsStore.targetGroup; } + if (typeof raw.tipsStore.targetGroupTitle === 'string') tipsStore.targetGroupTitle = raw.tipsStore.targetGroupTitle; + if (typeof raw.tipsStore.silentMode === 'boolean') tipsStore.silentMode = raw.tipsStore.silentMode; if (typeof raw.tipsStore.nextTipId === 'number') tipsStore.nextTipId = raw.tipsStore.nextTipId; } tipsStore.nextTipId = Math.max(tipsStore.nextTipId, tipsStore.tips.reduce((m, t) => Math.max(m, Number(t.id) || 0), 0) + 1); @@ -3558,7 +3566,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('💡 Content Drops', 'admin_cmd_tips_dashboard')], + [Markup.button.callback('💡 Helpful Tooltips', 'admin_cmd_tips_dashboard')], [Markup.button.callback('⬅️ Admin Dashboard', 'open_admin_dashboard')], [Markup.button.callback('🏠 Main Menu', 'to_main_menu'), Markup.button.callback('❌ Cancel', 'to_main_menu')], ]); @@ -4085,7 +4093,7 @@ function adminMainMenuKeyboard(user) { ], [ Markup.button.callback('📟 VPS Console (/sshv)', 'sshv_open'), - Markup.button.callback('💡 Content Drops', 'admin_cmd_tips_dashboard'), + Markup.button.callback('💡 Helpful Tooltips', 'admin_cmd_tips_dashboard'), ], [Markup.button.callback(toggleLabel, 'admin_cmd_mode_toggle')], [Markup.button.callback('↩ Back to User Menu', 'pamenu_back_user')], @@ -6045,14 +6053,14 @@ function buildHelpPages(user) { '/unapprove_group — Remove group from whitelist', '/list_groups — List all approved groups', '', - '💡 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 content drop settings', + '💡 HELPFUL TOOLTIPS MANAGEMENT', + '/tips (or /t, /tp) — Open Helpful Tooltips Manager', + '/tiplist — List all tooltips', + '/tipadd — Add a new tooltip', + '/tipremove — Remove a tooltip', + '/tipedit — Edit an existing tooltip', + '/tiptoggle — Enable/disable tooltips feature', + '/tipsettings — Configure tooltip settings (interval, target)', '', '📊 ANALYTICS', '/funnel — Conversion funnel stats', @@ -6604,8 +6612,47 @@ function gwizSummaryText(d) { * Start the inline giveaway wizard. * config = { chatId, chatTitle, startedBy } */ + +/** + * Pre-flight safety check before starting a giveaway. + * Returns { ok: true } if safe to proceed, or { ok: false, reason } with a reason string. + * Also DMs admin with specific remediation guidance on failure. + */ +async function giveawayPreflightCheck(ctx, chatId) { + // Check 1: group/channel must be linked + const hasGroup = approvedGroupsStore.size > 0 || broadcastConfigStore.targetGroup; + if (!hasGroup) { + const msg = '⚠️ No valid group/channel linked. Configure in Settings → Group Linking Tools before starting a giveaway.'; + await notifyAdmins(msg); + return { ok: false, reason: msg }; + } + + // Check 2: if a specific chatId is given, verify bot has post permissions + if (chatId && chatId !== (ctx.from && ctx.from.id)) { + try { + const botMember = await bot.telegram.getChatMember(chatId, bot.botInfo.id).catch(() => null); + if (botMember) { + const canPin = botMember.can_pin_messages || botMember.status === 'administrator'; + if (!canPin) { + const chatTitle = broadcastConfigStore.targetGroupTitle || String(chatId); + await notifyAdmins(`⚠️ Missing pin permission in ${chatTitle}. Please grant the bot "Pin Messages" permission before starting a giveaway.`); + // Not a hard block — warn only; admin can proceed + } + } + } catch (_) {} // non-fatal — just warn + } + return { ok: true }; +} + async function gwizStart(ctx, user, config) { clearPendingAction(user); + // Safety check before starting wizard + const targetChatId = config.chatId || (ctx.chat && ctx.chat.id); + const preflight = await giveawayPreflightCheck(ctx, targetChatId); + if (!preflight.ok) { + await ctx.reply(`❌ Cannot start giveaway:\n${preflight.reason}`); + return; + } const data = { chatId: config.chatId || (ctx.chat && ctx.chat.id), chatTitle: config.chatTitle || (ctx.chat && ctx.chat.title) || 'DM', @@ -7660,7 +7707,7 @@ bot.action('admin_cmd_tiptest', 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 content drops to test.'); return; } + if (!enabled.length) { await ctx.reply('No enabled tooltips to test.'); return; } const pool = enabled.length > 1 && tipsStore.lastSentTipId != null ? enabled.filter((t) => t.id !== tipsStore.lastSentTipId) : enabled; @@ -7670,9 +7717,9 @@ bot.action('admin_cmd_tiptest', async (ctx) => { 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')]])); + await ctx.reply(`✅ Test tooltip posted to ${sendResult.target} (silent).`, Markup.inlineKeyboard([[Markup.button.callback('💡 Open Tooltips 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.`); + await ctx.reply(`❌ Failed to post test tooltip. ${sendResult.error ? sendResult.error.message : 'Unknown error'}\nUse Settings → Link Channel/Group or forward a message from the target group/channel here to auto-register.`); } }); @@ -8657,7 +8704,7 @@ 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('✅ Resolve Bug', 'admin_cmd_resolvebug_prompt'), Markup.button.callback('💡 Test Tooltip', 'admin_cmd_tiptest')], [Markup.button.callback('📟 Open SSHV Console', 'sshv_open')], [Markup.button.callback('⬅️ Admin Dashboard', 'open_admin_dashboard')], [Markup.button.callback('🏠 Main Menu', 'to_main_menu'), Markup.button.callback('❌ Cancel', 'to_main_menu')], @@ -8674,7 +8721,7 @@ 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 +• Test Tooltip = sends one random tooltip to current target • SSHV = VPS console tools`, { parse_mode: 'Markdown', ...adminTestsToolsKeyboard() }, ); @@ -10040,12 +10087,10 @@ async function autoRegisterForwardedChatIfPresent(ctx, user) { const chatId = Number(fwdChat.id); approvedGroupsStore.add(chatId); tipsStore.targetGroup = String(chatId); + tipsStore.targetGroupTitle = fwdChat.title || null; 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.`); + await ctx.reply(`✅ Auto-registered target chat from forwarded message:\n• ${fwdChat.title || chatId} (${chatId})\n\nHelpful Tooltips + group broadcasts will now use this target.`); return true; } @@ -10187,13 +10232,11 @@ bot.on('text', async (ctx) => { const chatId = Number(fwdChat.id); approvedGroupsStore.add(chatId); tipsStore.targetGroup = String(chatId); + tipsStore.targetGroupTitle = fwdChat.title || null; broadcastConfigStore.targetGroup = String(chatId); user.pendingAction = null; persistRuntimeState(); - await ctx.reply(`✅ Registered chat for broadcasts/content drops: -• ${fwdChat.title || chatId} (${chatId}) - -Content Drops and group broadcasts will now use this target.`); + await ctx.reply(`✅ Helpful Tooltips target linked:\n• ${fwdChat.title || chatId} (${chatId})\n\nHelpful Tooltips and group broadcasts will now post to this chat.\n\n✅ Bot must already be a member with send permissions.`); return; } @@ -10847,7 +10890,13 @@ C = Existing User With Wager Requirement`); if (action.type === 'await_tip_add_text') { if (!requireAdmin(ctx)) return; if (!text) { - await ctx.reply('Drop text cannot be empty. Please send the tip you want to add.'); + await ctx.reply('Tooltip text cannot be empty. Please send the tooltip content to add.'); + return; + } + // Validate any inline button syntax before saving + const addParsed = parseTooltipButtons(text); + if (addParsed.error) { + await ctx.reply(`❌ Button syntax error:\n${addParsed.error}\n\nPlease fix the button syntax and try again.`); return; } const newTip = { id: tipsStore.nextTipId, text, enabled: true }; @@ -10856,7 +10905,8 @@ C = Existing User With Wager Requirement`); user.pendingAction = null; persistRuntimeState(); saveHelpfulMessages(); - await ctx.reply(`Added as Drop #${newTip.id}.`); + const buttonNote = addParsed.keyboard ? ' (with inline buttons)' : ''; + await ctx.reply(`✅ Added as Tooltip #${newTip.id}${buttonNote}.`); return; } @@ -10864,21 +10914,28 @@ C = Existing User With Wager Requirement`); if (action.type === 'await_tip_edit_text') { if (!requireAdmin(ctx)) return; if (!text) { - await ctx.reply('Drop text cannot be empty. Please send the updated tip text.'); + await ctx.reply('Tooltip text cannot be empty. Please send the updated text.'); + return; + } + // Validate any inline button syntax before saving + const editParsed = parseTooltipButtons(text); + if (editParsed.error) { + await ctx.reply(`❌ Button syntax error:\n${editParsed.error}\n\nPlease fix the button syntax and try again.`); return; } const tipId = action.data && action.data.tipId; const tip = tipsStore.tips.find((t) => t.id === tipId); if (!tip) { user.pendingAction = null; - await ctx.reply('Tip not found — it may have been removed.'); + await ctx.reply('Tooltip not found — it may have been removed.'); return; } tip.text = text; user.pendingAction = null; persistRuntimeState(); saveHelpfulMessages(); - await ctx.reply(`Drop #${tipId} updated.`); + const editButtonNote = editParsed.keyboard ? ' (with inline buttons)' : ''; + await ctx.reply(`✅ Tooltip #${tipId} updated${editButtonNote}.`); return; } @@ -10886,8 +10943,8 @@ C = Existing User With Wager Requirement`); if (action.type === 'await_tip_settings_interval') { if (!requireAdmin(ctx)) return; const hours = Number(text); - if (!Number.isFinite(hours) || hours <= 0) { - await ctx.reply('Please send a valid number of hours (e.g. `4`).', { parse_mode: 'Markdown' }); + if (!Number.isInteger(hours) || hours <= 0) { + await ctx.reply('Please send a whole number of hours (e.g. `4`).', { parse_mode: 'Markdown' }); return; } tipsStore.intervalHours = hours; @@ -10895,7 +10952,26 @@ C = Existing User With Wager Requirement`); persistRuntimeState(); saveHelpfulMessages(); startTipsScheduler(); // re-arm with new interval - await ctx.reply(`✅ Tip interval updated to every ${hours} hour(s). Scheduler restarted.`); + await ctx.reply(`✅ Tooltip interval updated to every ${hours} hour(s). Scheduler restarted.`); + return; + } + + // Tips system: await group/channel forward to link as tooltip target + if (action.type === 'await_tip_link_target') { + if (!requireAdmin(ctx)) return; + const fwdChat = extractForwardedChat(ctx.message || {}); + if (!fwdChat || !fwdChat.id) { + await ctx.reply('Please forward a message from the target channel or group. The bot must already be a member.'); + return; + } + const chatId = Number(fwdChat.id); + approvedGroupsStore.add(chatId); + tipsStore.targetGroup = String(chatId); + tipsStore.targetGroupTitle = fwdChat.title || null; + broadcastConfigStore.targetGroup = String(chatId); + user.pendingAction = null; + persistRuntimeState(); + await ctx.reply(`✅ Helpful Tooltips target linked:\n• ${fwdChat.title || chatId} (${chatId})\n\nTooltips and group broadcasts will now post to this chat.`); return; } @@ -11132,7 +11208,7 @@ bot.action('announce_send_channel', async (ctx) => { }); // ========================= -// Content Drops System — scheduler + admin commands + callbacks +// Helpful Tooltips System — scheduler + admin commands + callbacks // ========================= /** @@ -11188,10 +11264,20 @@ async function postTipToConfiguredTarget(tip, telegram) { const fallbackTarget = approvedGroupsStore.size ? Array.from(approvedGroupsStore)[0] : null; const candidates = [primaryTarget, fallbackTarget].filter(Boolean); let lastErr = null; + + // Parse inline buttons from tip text if present + const parsed = parseTooltipButtons(String(tip.text || '')); + const messageText = parsed.text || String(tip.text || ''); + const extra = { + parse_mode: 'HTML', + disable_notification: tipsStore.silentMode !== false, + }; + if (parsed.keyboard) Object.assign(extra, parsed.keyboard); + for (const target of candidates) { try { // eslint-disable-next-line no-await-in-loop - await telegram.sendMessage(target, formatTipForHtml(tip.text), { parse_mode: 'HTML', disable_notification: true }); + await telegram.sendMessage(target, formatTipForHtml(messageText), extra); tipsStore.targetGroup = String(target); broadcastConfigStore.targetGroup = String(target); return { ok: true, target }; @@ -11202,6 +11288,56 @@ async function postTipToConfiguredTarget(tip, telegram) { return { ok: false, error: lastErr }; } +/** + * Parse inline button syntax from tooltip text. + * Syntax: [Label - https://url] && [Label2 - https://url2] = same row; new line = new row + * Special: [Open Bot] adds a standard "Open Bot" button to the row. + * Returns { text, keyboard } — text with button lines stripped, keyboard as Telegraf array or null. + * Returns { error } if button syntax is malformed. + */ +function parseTooltipButtons(rawText) { + const lines = rawText.split('\n'); + const textLines = []; + const buttonRows = []; + const OPEN_BOT_URL = 'https://t.me/RuneWager_bot'; + const BUTTON_LINE_RE = /^\[.+?\s*-\s*https?:\/\/.+?\](\s*&&\s*\[.+?\s*-\s*https?:\/\/.+?\])*$|^\[Open Bot\](\s*&&\s*\[Open Bot\])*$/i; + + for (const line of lines) { + const trimmed = line.trim(); + // A button row line: one or more [Label - URL] items separated by && + if (/^\[.+?\](\s*&&\s*\[.+?\])*$/.test(trimmed)) { + const rowButtons = []; + const parts = trimmed.split(/\s*&&\s*/); + for (const part of parts) { + const openBotMatch = /^\[Open Bot\]$/i.test(part.trim()); + if (openBotMatch) { + rowButtons.push(Markup.button.url('Open Bot', OPEN_BOT_URL)); + continue; + } + const m = part.trim().match(/^\[(.+?)\s*-\s*(https?:\/\/\S+?)\]$/); + if (!m) { + return { error: `Invalid button syntax: ${part.trim()}\n\nExpected format: [Label - https://url]` }; + } + const [, label, url] = m; + try { + new URL(url); + } catch (_) { + return { error: `Invalid URL in button: ${url}` }; + } + if (!label.trim()) return { error: 'Button label cannot be empty.' }; + rowButtons.push(Markup.button.url(label.trim(), url)); + } + if (rowButtons.length > 0) buttonRows.push(rowButtons); + } else { + textLines.push(line); + } + } + + const text = textLines.join('\n').trim(); + const keyboard = buttonRows.length > 0 ? Markup.inlineKeyboard(buttonRows) : null; + return { text, keyboard }; +} + /** * Start (or restart) the tips scheduler. * Clears any existing timer then arms a fresh one using the current interval. @@ -11231,14 +11367,16 @@ function startTipsScheduler() { }, ms); } -/** Build the Content Drops Manager dashboard keyboard */ +/** Build the Helpful Tooltips Manager keyboard */ function tipsDashboardKeyboard() { + const count = tipsStore.tips.length; return Markup.inlineKeyboard([ - [Markup.button.callback('➕ Add Tip', 'tips_cmd_add'), Markup.button.callback('✏️ Edit Tip', 'tips_cmd_edit')], - [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('➕ Add Tooltip', 'tips_cmd_add'), Markup.button.callback('✏️ Edit Tooltip', 'tips_cmd_edit')], + [Markup.button.callback('❌ Remove Tooltip', 'tips_cmd_remove'), Markup.button.callback('🔁 Toggle System', 'tips_cmd_toggle')], + [Markup.button.callback(`📋 Show all Helpful Tooltips (${count})`, 'tips_cmd_list')], + [Markup.button.callback('🧪 Test Random Tooltip', 'tips_cmd_test')], + [Markup.button.callback('⚙️ Helpful Tooltips Settings', 'tips_cmd_settings')], + [Markup.button.callback('📥 Import Tooltips (JSON)', 'tips_cmd_import_batch')], [Markup.button.callback('↩ Admin Menu', 'pamenu_back_admin')], ]); } @@ -11256,12 +11394,15 @@ function tipSelectKeyboard(action) { return Markup.inlineKeyboard(rows); } -/** Send (or re-send) the Content Drops Manager dashboard */ +/** Send (or re-send) the Helpful Tooltips 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 = `📝 *Content Drops Manager*\n\nStatus: ${status}\nTotal Drops: ${total} (${enabled} active)\nInterval: every ${tipsStore.intervalHours}h\nTarget: ${tipsStore.targetGroup}`; + const targetDisplay = tipsStore.targetGroup + ? `${tipsStore.targetGroupTitle ? `${tipsStore.targetGroupTitle} ` : ''}(${tipsStore.targetGroup})` + : '⚠️ Not linked — use Settings → Link Channel/Group'; + const text = `💡 *Helpful Tooltips Manager*\n\nStatus: ${status}\nTotal Tooltips: ${total} (${enabled} active)\nInterval: every ${tipsStore.intervalHours}h\nSilent mode: ${tipsStore.silentMode ? 'ON' : 'OFF'}\nTarget: ${targetDisplay}`; await replaceCallbackPanel(ctx, text, { parse_mode: 'Markdown', ...tipsDashboardKeyboard() }); } @@ -11298,11 +11439,15 @@ bot.command('tp', handleTipsCommand); bot.command('tiplist', async (ctx) => { if (!requireAdmin(ctx)) return; - const lines = ['📋 *Current Content Drops:*\n']; + const lines = [`📋 *Current Helpful Tooltips (${tipsStore.tips.length}):*\n`]; for (const [idx, tip] of tipsStore.tips.entries()) { - const preview = tip.text.replace(/\*/g, '').slice(0, 80); + const preview = tip.text.replace(/\*/g, '').replace(/<[^>]+>/g, '').slice(0, 80); lines.push(`${idx + 1}. #${tip.id} ${tip.enabled ? '✅' : '🔇'} ${preview}${tip.text.length > 80 ? '…' : ''}`); } + const targetLabel = tipsStore.targetGroupTitle + ? `${tipsStore.targetGroupTitle} (${tipsStore.targetGroup})` + : (tipsStore.targetGroup || 'Not linked'); + lines.push(`\n📌 Target: ${targetLabel}`); await ctx.reply(lines.join('\n'), { parse_mode: 'Markdown' }); }); @@ -11366,8 +11511,8 @@ bot.command('tipedit', async (ctx) => { return; } } - if (!tipsStore.tips.length) { await ctx.reply('No tips to edit.'); return; } - await ctx.reply('Edit which tip?', tipSelectKeyboard('tip_edit_select')); + if (!tipsStore.tips.length) { await ctx.reply('No tooltips to edit.'); return; } + await ctx.reply('Edit which tooltip?', tipSelectKeyboard('tip_edit_select')); }); bot.command('tiptoggle', async (ctx) => { @@ -11375,14 +11520,14 @@ bot.command('tiptoggle', async (ctx) => { tipsStore.systemEnabled = !tipsStore.systemEnabled; persistRuntimeState(); saveHelpfulMessages(); - const status = tipsStore.systemEnabled ? '🟢 Content Drops System Enabled' : '🔴 Content Drops System Disabled'; + const status = tipsStore.systemEnabled ? '🟢 Helpful Tooltips System Enabled' : '🔴 Helpful Tooltips System Disabled'; await ctx.reply(status); }); 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 test.'); return; } + if (!enabled.length) { await ctx.reply('No enabled tooltips to test.'); return; } const pool = enabled.length > 1 && tipsStore.lastSentTipId != null ? enabled.filter((t) => t.id !== tipsStore.lastSentTipId) : enabled; @@ -11392,20 +11537,21 @@ bot.command('tiptest', async (ctx) => { tipsStore.lastSentTipId = tip.id; persistRuntimeState(); saveHelpfulMessages(); - await ctx.reply(`✅ Test tip posted to ${sendResult.target} (silent).`); + await ctx.reply(`✅ Test tooltip posted to ${sendResult.target} (silent).`); } else { - await ctx.reply(`❌ Failed to post test tip. ${sendResult.error ? sendResult.error.message : 'Unknown error'} -Tip: run /register_chat and forward a message from the target group/channel.`); + await ctx.reply(`❌ Failed to post test tooltip. ${sendResult.error ? sendResult.error.message : 'Unknown error'}\nUse Settings → Link Channel/Group to configure the target.`); } }); bot.command('tipsettings', async (ctx) => { if (!requireAdmin(ctx)) return; - 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 targetDisplay = tipsStore.targetGroup + ? `${tipsStore.targetGroupTitle ? `${tipsStore.targetGroupTitle} ` : ''}(${tipsStore.targetGroup})` + : '⚠️ Not linked'; + const text = `⚙️ *Helpful Tooltips Settings*\n\nInterval: every ${tipsStore.intervalHours} hours\nSilent mode: ${tipsStore.silentMode ? 'ON ✅' : 'OFF'}\nTarget: ${targetDisplay}\n\nUse the buttons below to update settings.`; const user = getUser(ctx); clearPendingAction(user); - user.pendingAction = { type: 'await_tip_settings_interval' }; - await ctx.reply(text, { parse_mode: 'Markdown' }); + await ctx.reply(text, { parse_mode: 'Markdown', ...tipsSettingsKeyboard() }); }); // ── Tips Dashboard inline button actions ── @@ -11415,22 +11561,29 @@ bot.action('tips_cmd_add', async (ctx) => { const user = getUser(ctx); await ctx.answerCbQuery(); clearPendingAction(user); - user.pendingAction = { type: 'await_tip_add_text' }; - await ctx.reply('Send the new tip text. HTML and Markdown are both allowed.'); + user.pendingAction = { type: 'await_tip_add_text', createdAt: Date.now() }; + await ctx.reply( + '➕ *Add Helpful Tooltip*\n\nSend the tooltip text (HTML or plain text).\n\n' + + 'To attach inline buttons, use the button syntax on a new line:\n' + + '`[Label - https://url] && [Label2 - https://url2]` = same row\n' + + 'New line = new row\n\n' + + 'To add a standard "Open Bot" button, include `[Open Bot]` on its own line.', + { parse_mode: 'Markdown' }, + ); }); bot.action('tips_cmd_edit', async (ctx) => { if (!requireAdmin(ctx)) return; await ctx.answerCbQuery(); - if (!tipsStore.tips.length) { await ctx.reply('No tips to edit.'); return; } - await ctx.reply('Edit which tip?', tipSelectKeyboard('tip_edit_select')); + if (!tipsStore.tips.length) { await ctx.reply('No tooltips to edit.'); return; } + await ctx.reply('Edit which tooltip?', tipSelectKeyboard('tip_edit_select')); }); bot.action('tips_cmd_remove', async (ctx) => { if (!requireAdmin(ctx)) return; await ctx.answerCbQuery(); - if (!tipsStore.tips.length) { await ctx.reply('No tips to remove.'); return; } - await ctx.reply('Remove which tip?', tipSelectKeyboard('tip_remove')); + if (!tipsStore.tips.length) { await ctx.reply('No tooltips to remove.'); return; } + await ctx.reply('Remove which tooltip?', tipSelectKeyboard('tip_remove')); }); bot.action('tips_cmd_toggle', async (ctx) => { @@ -11439,7 +11592,7 @@ bot.action('tips_cmd_toggle', async (ctx) => { tipsStore.systemEnabled = !tipsStore.systemEnabled; persistRuntimeState(); saveHelpfulMessages(); - const status = tipsStore.systemEnabled ? '🟢 Content Drops System Enabled' : '🔴 Content Drops System Disabled'; + const status = tipsStore.systemEnabled ? '🟢 Helpful Tooltips System Enabled' : '🔴 Helpful Tooltips System Disabled'; await ctx.reply(status); await sendTipsDashboard(ctx); }); @@ -11447,11 +11600,15 @@ bot.action('tips_cmd_toggle', async (ctx) => { bot.action('tips_cmd_list', async (ctx) => { if (!requireAdmin(ctx)) return; await ctx.answerCbQuery(); - const lines = ['📋 *Current Content Drops:*\n']; + const lines = [`📋 *All Helpful Tooltips (${tipsStore.tips.length}):*\n`]; for (const [idx, tip] of tipsStore.tips.entries()) { - const preview = tip.text.replace(/\*/g, '').slice(0, 80); + const preview = tip.text.replace(/\*/g, '').replace(/<[^>]+>/g, '').slice(0, 80); lines.push(`${idx + 1}. #${tip.id} ${tip.enabled ? '✅' : '🔇'} ${preview}${tip.text.length > 80 ? '…' : ''}`); } + const targetLabel = tipsStore.targetGroupTitle + ? `${tipsStore.targetGroupTitle} (${tipsStore.targetGroup})` + : (tipsStore.targetGroup || '⚠️ Not linked'); + lines.push(`\n📌 Target: ${targetLabel}`); await ctx.reply(lines.join('\n'), { parse_mode: 'Markdown' }); }); @@ -11459,7 +11616,7 @@ 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 test.'); return; } + if (!enabled.length) { await ctx.reply('No enabled tooltips to test.'); return; } const pool = enabled.length > 1 && tipsStore.lastSentTipId != null ? enabled.filter((t) => t.id !== tipsStore.lastSentTipId) : enabled; @@ -11469,10 +11626,9 @@ bot.action('tips_cmd_test', async (ctx) => { tipsStore.lastSentTipId = tip.id; persistRuntimeState(); saveHelpfulMessages(); - await ctx.reply(`✅ Test tip posted to ${sendResult.target} (silent).`); + await ctx.reply(`✅ Test tooltip posted to ${sendResult.target} (silent).`); } else { - await ctx.reply(`❌ Failed to post test tip. ${sendResult.error ? sendResult.error.message : 'Unknown error'} -Tip: run /register_chat and forward a message from the target group/channel.`); + await ctx.reply(`❌ Failed to post test tooltip. ${sendResult.error ? sendResult.error.message : 'Unknown error'}\nUse Settings → Link Channel/Group or forward a message from the target group/channel.`); } await sendTipsDashboard(ctx); }); @@ -11482,18 +11638,56 @@ bot.action('tips_cmd_import_batch', async (ctx) => { 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}]'); + user.pendingAction = { type: 'await_tip_add_text', createdAt: Date.now() }; + await ctx.reply('Paste a JSON array to append tooltips (supports plain text or HTML):\n\n`/tipadd [{"text":"My Tooltip","enabled":true}]`', { parse_mode: 'Markdown' }); }); +/** Build the Helpful Tooltips Settings keyboard */ +function tipsSettingsKeyboard() { + return Markup.inlineKeyboard([ + [Markup.button.callback('⏱ Change Interval', 'tips_set_interval'), Markup.button.callback('🔗 Link Channel/Group', 'tips_set_link_target')], + [Markup.button.callback('↩ Back to Tooltips', 'tips_settings_back')], + ]); +} + bot.action('tips_cmd_settings', async (ctx) => { + if (!requireAdmin(ctx)) return; + await ctx.answerCbQuery(); + const targetDisplay = tipsStore.targetGroup + ? `${tipsStore.targetGroupTitle ? `${tipsStore.targetGroupTitle} ` : ''}(${tipsStore.targetGroup})` + : '⚠️ Not linked'; + const text = `⚙️ *Helpful Tooltips Settings*\n\nInterval: every ${tipsStore.intervalHours} hours\nSilent mode: ${tipsStore.silentMode ? 'ON ✅' : 'OFF'}\nTarget: ${targetDisplay}\n\nUse the buttons below to update settings.`; + await ctx.reply(text, { parse_mode: 'Markdown', ...tipsSettingsKeyboard() }); +}); + +bot.action('tips_settings_back', async (ctx) => { + if (!requireAdmin(ctx)) return; + await ctx.answerCbQuery(); + await sendTipsDashboard(ctx); +}); + +bot.action('tips_set_interval', async (ctx) => { if (!requireAdmin(ctx)) return; await ctx.answerCbQuery(); const user = getUser(ctx); clearPendingAction(user); - user.pendingAction = { type: 'await_tip_settings_interval' }; - 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' }); + user.pendingAction = { type: 'await_tip_settings_interval', createdAt: Date.now() }; + await ctx.reply(`⏱ *Change Tooltip Interval*\n\nCurrent: every ${tipsStore.intervalHours} hours\n\nSend the new interval as a whole number of hours (e.g. \`4\`).`, { parse_mode: 'Markdown' }); +}); + +bot.action('tips_set_link_target', async (ctx) => { + if (!requireAdmin(ctx)) return; + await ctx.answerCbQuery(); + const user = getUser(ctx); + clearPendingAction(user); + user.pendingAction = { type: 'await_tip_link_target', createdAt: Date.now() }; + const currentDisplay = tipsStore.targetGroup + ? `Current: ${tipsStore.targetGroupTitle || tipsStore.targetGroup} (${tipsStore.targetGroup})` + : 'No target linked yet.'; + await ctx.reply( + `🔗 *Link Tooltip Channel/Group*\n\n${currentDisplay}\n\nForward any message from the target channel or group here to link it.\n\n⚠️ The bot must already be a member of that channel/group with send message permissions.`, + { parse_mode: 'Markdown' }, + ); }); bot.action('tips_select_cancel', async (ctx) => { @@ -11510,11 +11704,11 @@ bot.action(/^tip_remove_(\d+)$/, async (ctx) => { await ctx.answerCbQuery(); const tipId = Number(ctx.match[1]); const idx = tipsStore.tips.findIndex((t) => t.id === tipId); - if (idx === -1) { await ctx.reply('Tip not found.'); return; } + if (idx === -1) { await ctx.reply('Tooltip not found.'); return; } tipsStore.tips.splice(idx, 1); persistRuntimeState(); saveHelpfulMessages(); - await ctx.reply(`Drop #${tipId} removed.`); + await ctx.reply(`✅ Tooltip #${tipId} removed.`); }); // tip_edit_select_ @@ -11523,11 +11717,19 @@ bot.action(/^tip_edit_select_(\d+)$/, async (ctx) => { await ctx.answerCbQuery(); const tipId = Number(ctx.match[1]); const tip = tipsStore.tips.find((t) => t.id === tipId); - if (!tip) { await ctx.reply('Tip not found.'); return; } + if (!tip) { await ctx.reply('Tooltip not found.'); return; } const user = getUser(ctx); clearPendingAction(user); - user.pendingAction = { type: 'await_tip_edit_text', data: { tipId } }; - await ctx.reply(`Send the updated text for Drop #${tipId}.\n\nCurrent text:\n${tip.text}`); + user.pendingAction = { type: 'await_tip_edit_text', data: { tipId }, createdAt: Date.now() }; + await ctx.reply( + `✏️ *Edit Tooltip #${tipId}*\n\nCurrent text:\n${tip.text}\n\n` + + 'Send the updated tooltip text.\n\n' + + 'To attach inline buttons, add button rows after the text:\n' + + '`[Label - https://url] && [Label2 - https://url2]` = same row\n' + + 'New line = new row\n' + + '`[Open Bot]` = adds standard Open Bot button.', + { parse_mode: 'Markdown' }, + ); }); // tip_toggle_ (per-tip enable/disable, accessible from tiplist) @@ -11540,7 +11742,7 @@ bot.action(/^tip_toggle_(\d+)$/, async (ctx) => { tip.enabled = !tip.enabled; persistRuntimeState(); saveHelpfulMessages(); - await ctx.reply(`Drop #${tipId} is now ${tip.enabled ? '✅ enabled' : '🔇 disabled'}.`); + await ctx.reply(`Tooltip #${tipId} is now ${tip.enabled ? '✅ enabled' : '🔇 disabled'}.`); }); // ========================= @@ -12062,75 +12264,25 @@ function createGiveaway(config) { */ async function announceGiveaway(giveaway, botUsername) { - const titleLine = giveaway.title ? `\n📌 *${escapeMarkdownV2(giveaway.title)}*\n` : ''; - const minPartLine = giveaway.minParticipants > 0 - ? `• Min participants: ${giveaway.minParticipants}\n` - : ''; - const reqLines = [ - giveaway.requireLinked ? '• Linked Runewager username ✅' : null, - giveaway.requireChannel ? '• Joined GambleCodez channel ✅' : null, - giveaway.requireGroup ? '• Joined GambleCodez group ✅' : null, - giveaway.requireAge ? '• Age confirmed (18+) ✅' : null, - giveaway.requireVerified ? '• Account confirmed ✅' : null, - giveaway.requirePromo ? '• Claimed promo ✅' : null, - giveaway.requireWalkthrough ? '• Full walkthrough ✅' : null, - ].filter(Boolean); - const reqBlock = reqLines.length > 0 ? `\n*Requirements:*\n${reqLines.join('\n')}\n` : ''; - - const text = [ - `🎉 *SC Giveaway Started!*${titleLine}`, - `🏆 Winners: ${giveaway.maxWinners}`, - `💰 SC amount per winner: ${giveaway.scPerWinner}`, - `⏱ Countdown timer: ${giveaway.durationMinutes} min`, - `ℹ️ Referrals give 2× boost for 7 days`, - minPartLine.trim() ? minPartLine.trim() : null, - reqBlock.trim() ? reqBlock.trim() : null, - '*Helpful tips:*', - '• Must open bot', - '• Must have Runewager username linked', - '• Must have joined Runewager', - `👥 Live joined count: ${giveaway.participants.size}`, - `\n👇 Tap below to join before countdown ends!`, - ].filter(Boolean).join('\n'); - - // Build join button row — depends on joinSurface - const joinRows = []; - if (giveaway.joinSurface === 'group' || giveaway.joinSurface === 'both') { - joinRows.push(Markup.button.callback('✅ Join Here', `gw_join_${giveaway.id}`)); - } - if ((giveaway.joinSurface === 'dm' || giveaway.joinSurface === 'both') && botUsername) { - joinRows.push(Markup.button.url('💬 Join via DM', `https://t.me/${botUsername}?start=join_gw_${giveaway.id}`)); - } - - const kb = Markup.inlineKeyboard([ - joinRows.length > 0 ? joinRows : [Markup.button.callback('✅ Join Giveaway', `gw_join_${giveaway.id}`)], - [Markup.button.url('🤖 Open Bot', botUsername ? `https://t.me/${botUsername}` : LINKS.miniAppPlay)], - [ - Markup.button.callback('📋 Details', `gw_details_${giveaway.id}`), - Markup.button.callback('🧪 My Eligibility', `gw_elig_${giveaway.id}`), - ], - [ - Markup.button.callback('⛔ Cancel (Admin)', `gw_cancel_${giveaway.id}`), - Markup.button.callback('⏱ Extend (Admin)', `gw_extend_${giveaway.id}`), - ], - [ - Markup.button.callback('👥 Edit Winners (Admin)', `gw_edit_winners_${giveaway.id}`), - Markup.button.callback('💠 Edit SC (Admin)', `gw_edit_sc_${giveaway.id}`), - ], - ]); - + // Post giveaway-started announcement to group + const text = buildGiveawayAnnouncementText(giveaway, null); + const kb = buildGiveawayAnnouncementKeyboard(giveaway, botUsername); const sent = await bot.telegram.sendMessage(giveaway.chatId, text, { parse_mode: 'Markdown', ...kb }); + giveaway.announceMsgId = sent ? sent.message_id : null; - // Attempt to pin the announcement in the group; ignore permission errors + // Attempt to pin the announcement in the group; notify admin if missing permission if (sent && (giveaway.joinSurface === 'group' || giveaway.joinSurface === 'both')) { try { await bot.telegram.pinChatMessage(giveaway.chatId, sent.message_id, { disable_notification: true }); giveaway.pinnedMsgId = sent.message_id; } catch (e) { - await notifyAdmins(`⚠️ Failed to pin giveaway #${giveaway.id} in chat ${giveaway.chatId}: ${e.message}`); + await notifyAdmins(`⚠️ Missing pin permission for giveaway #${giveaway.id} in chat ${giveaway.chatId}.\nError: ${e.message}\n\nPlease grant the bot "Pin Messages" permission.`); } } + // Schedule auto-refresh at 25% intervals (re-pins after each refresh) + scheduleGiveawayRefresh(giveaway); + // If DM surface, also send a DM join announcement to all opted-in users if (giveaway.joinSurface === 'dm' || giveaway.joinSurface === 'both') { const dmText = `${text}\n\n_Join directly here in DM:_`; @@ -12223,6 +12375,105 @@ function resetGiveawayTimer(giveaway) { giveaway.endTimer = setTimeout(() => finalizeGiveaway(giveaway.id), ms); } +/** + * Schedule auto-refresh of the giveaway post at 25% intervals of total duration. + * Edits the announcement message, updates stats and remaining time, re-pins. + * Spec: 10 min → refresh at 2.5m, 5m, 7.5m (final results at 10m handled by finalizeGiveaway). + */ +function scheduleGiveawayRefresh(giveaway) { + const totalMs = giveaway.durationMinutes * 60 * 1000; + const refreshPoints = [0.25, 0.50, 0.75]; // 25%, 50%, 75% + const startTime = giveaway.endTime - totalMs; + + for (const fraction of refreshPoints) { + const fireAt = startTime + totalMs * fraction; + const delay = fireAt - Date.now(); + if (delay < 1000) continue; // already past + const t = setTimeout(async () => { + if (!giveawayStore.running.has(giveaway.id)) return; + try { + const remaining = Math.max(0, giveaway.endTime - Date.now()); + const remMins = Math.floor(remaining / 60000); + const remSecs = Math.floor((remaining % 60000) / 1000); + const remStr = remMins > 0 ? `${remMins}m ${remSecs}s` : `${remSecs}s`; + const refreshText = buildGiveawayAnnouncementText(giveaway, remStr); + const kb = buildGiveawayAnnouncementKeyboard(giveaway); + + if (giveaway.announceMsgId) { + // Try to edit existing message (jumps to bottom in some clients) + await bot.telegram.editMessageText( + giveaway.chatId, giveaway.announceMsgId, null, + refreshText, { parse_mode: 'Markdown', ...kb }, + ).catch(async () => { + // Edit failed — send a fresh message + const sent = await bot.telegram.sendMessage(giveaway.chatId, refreshText, { parse_mode: 'Markdown', ...kb }); + giveaway.announceMsgId = sent.message_id; + }); + } else { + const sent = await bot.telegram.sendMessage(giveaway.chatId, refreshText, { parse_mode: 'Markdown', ...kb }); + giveaway.announceMsgId = sent.message_id; + } + + // Re-pin after refresh + if (giveaway.announceMsgId && (giveaway.joinSurface === 'group' || giveaway.joinSurface === 'both')) { + await bot.telegram.pinChatMessage(giveaway.chatId, giveaway.announceMsgId, { disable_notification: true }) + .catch(() => {}); + giveaway.pinnedMsgId = giveaway.announceMsgId; + } + } catch (e) { + logEvent('warn', 'giveaway_refresh_failed', { gwId: giveaway.id, error: e.message }); + } + }, delay); + giveaway.reminders.push(t); + } +} + +/** Build the announcement text for a running giveaway (used for initial post and refreshes). */ +function buildGiveawayAnnouncementText(giveaway, remainingStr) { + const titleLine = giveaway.title ? `\n📌 *${escapeMarkdownV2(giveaway.title)}*\n` : ''; + const minPartLine = giveaway.minParticipants > 0 ? `• Min participants: ${giveaway.minParticipants}\n` : ''; + const reqLines = [ + giveaway.requireLinked ? '• Linked Runewager username ✅' : null, + giveaway.requireChannel ? '• Joined GambleCodez channel ✅' : null, + giveaway.requireGroup ? '• Joined GambleCodez group ✅' : null, + giveaway.requireAge ? '• Age confirmed (18+) ✅' : null, + giveaway.requireVerified ? '• Account confirmed ✅' : null, + giveaway.requirePromo ? '• Claimed promo ✅' : null, + giveaway.requireWalkthrough ? '• Full walkthrough ✅' : null, + ].filter(Boolean); + const reqBlock = reqLines.length > 0 ? `\n*Requirements:*\n${reqLines.join('\n')}\n` : ''; + const timeDisplay = remainingStr || `${giveaway.durationMinutes} min`; + return [ + `🎉 *SC Giveaway!*${titleLine}`, + `🏆 Winners: ${giveaway.maxWinners}`, + `💰 SC per winner: ${giveaway.scPerWinner}`, + `⏱ ${remainingStr ? 'Time remaining: ' + remainingStr : 'Duration: ' + timeDisplay}`, + `ℹ️ Referrals give 2× boost for 7 days`, + minPartLine.trim() || null, + reqBlock.trim() || null, + `👥 Joined: ${giveaway.participants.size}`, + `\n👇 Tap below to join before countdown ends!`, + ].filter(Boolean).join('\n'); +} + +/** Build the inline keyboard for a running giveaway announcement. */ +function buildGiveawayAnnouncementKeyboard(giveaway, botUsername) { + const joinRows = []; + if (giveaway.joinSurface === 'group' || giveaway.joinSurface === 'both') { + joinRows.push(Markup.button.callback('✅ Join Here', `gw_join_${giveaway.id}`)); + } + if ((giveaway.joinSurface === 'dm' || giveaway.joinSurface === 'both') && botUsername) { + joinRows.push(Markup.button.url('💬 Join via DM', `https://t.me/${botUsername}?start=join_gw_${giveaway.id}`)); + } + return Markup.inlineKeyboard([ + joinRows.length > 0 ? joinRows : [Markup.button.callback('✅ Join Giveaway', `gw_join_${giveaway.id}`)], + [Markup.button.url('🤖 Open Bot', botUsername ? `https://t.me/${botUsername}` : LINKS.miniAppPlay)], + [Markup.button.callback('📋 Details', `gw_details_${giveaway.id}`), Markup.button.callback('🧪 My Eligibility', `gw_elig_${giveaway.id}`)], + [Markup.button.callback('⛔ Cancel (Admin)', `gw_cancel_${giveaway.id}`), Markup.button.callback('⏱ Extend (Admin)', `gw_extend_${giveaway.id}`)], + [Markup.button.callback('👥 Edit Winners (Admin)', `gw_edit_winners_${giveaway.id}`), Markup.button.callback('💠 Edit SC (Admin)', `gw_edit_sc_${giveaway.id}`)], + ]); +} + /** * scheduleGiveawayReminders executes its scoped Runewager logic and participates in menu/command or utility flow composition. @@ -12244,18 +12495,37 @@ function resetGiveawayTimer(giveaway) { */ function scheduleGiveawayReminders(giveaway) { - const points = [10, 5, 1]; - points.forEach((m) => { - const at = giveaway.endTime - m * 60 * 1000; - const delay = at - Date.now(); - if (delay > 1000) { - const t = setTimeout(async () => { - if (!giveawayStore.running.has(giveaway.id)) return; - await bot.telegram.sendMessage(giveaway.chatId, `⏳ Giveaway #${giveaway.id}: ${m} minute(s) remaining.`); - }, delay); - giveaway.reminders.push(t); - } - }); + const scheduleReminder = (delayMs, getMessage) => { + if (delayMs < 500) return; + const t = setTimeout(async () => { + if (!giveawayStore.running.has(giveaway.id)) return; + try { + await bot.telegram.sendMessage(giveaway.chatId, getMessage()); + } catch (_) {} + }, delayMs); + giveaway.reminders.push(t); + }; + + // Minute-based checkpoints: 10, 5 min + for (const m of [10, 5]) { + const delay = giveaway.endTime - m * 60 * 1000 - Date.now(); + scheduleReminder(delay, () => `⏳ Giveaway #${giveaway.id}: ${m} minutes remaining!`); + } + + // 1 minute remaining + scheduleReminder(giveaway.endTime - 60 * 1000 - Date.now(), + () => `⏰ 1 minute remaining! Last chance to join Giveaway #${giveaway.id}!`); + + // 30 seconds remaining + scheduleReminder(giveaway.endTime - 30 * 1000 - Date.now(), + () => `⏰ 30 seconds remaining! Giveaway #${giveaway.id} ends very soon!`); + + // 10-second countdown: 10, 9, 8, ... 1 + for (let sec = 10; sec >= 1; sec--) { + const delay = giveaway.endTime - sec * 1000 - Date.now(); + const s = sec; // capture + scheduleReminder(delay, () => `⏳ ${s}...`); + } } /** @@ -12361,9 +12631,10 @@ async function finalizeGiveaway(gwId, forceEnd = false) { giveaway.winners = selected; if (!giveaway.dryRun) { - await bot.telegram.sendMessage(giveaway.chatId, renderWinnersText(giveaway)).catch(() => {}); + await bot.telegram.sendMessage(giveaway.chatId, renderWinnersText(giveaway), { parse_mode: 'HTML' }).catch(() => {}); } + const dmFailedUsers = []; for (const winner of giveaway.winners) { try { // Apply 2× SC boost if the winner has an active referral boost @@ -12372,25 +12643,52 @@ async function finalizeGiveaway(gwId, forceEnd = false) { const awardedSc = hasBoostedPrize ? giveaway.scPerWinner * 2 : giveaway.scPerWinner; winner.awardedSc = awardedSc; winner.boosted = hasBoostedPrize; + winner.rwUsername = (winnerUser && winnerUser.runewagerUsername) || winner.runewagerUsername || ''; // eslint-disable-next-line no-await-in-loop await bot.telegram.sendMessage( winner.userId, - `🎉 You won Giveaway #${giveaway.id}!\n` - + `Prize: ${awardedSc} SC${hasBoostedPrize ? ' 🔥 (2× Referral Boost applied!)' : ''}\n` - + `Runewager username on file: ${winner.runewagerUsername || '(not linked)'}\n` - + `Admin will handle prize distribution.`, + `🎉 *You won Giveaway #${giveaway.id}!*\n\n` + + `Prize: *${awardedSc} SC*${hasBoostedPrize ? ' 🔥 (2× Referral Boost applied!)' : ''}\n` + + `Runewager username on file: \`${winner.rwUsername || 'not linked'}\`\n\n` + + `ℹ️ The bot will DM you automatically once your tip has been sent. Please allow up to 24 hours for processing.`, + { parse_mode: 'Markdown' }, ); } catch (_) { - // ignore DM failures (user may have blocked bot) + // DM failed — log for admin report + dmFailedUsers.push(winner.userId); } } - const winnerLines = giveaway.winners.map((w) => - `• ${w.tgUsername ? '@' + w.tgUsername : w.firstName || 'User'} — ${w.awardedSc || giveaway.scPerWinner} SC${w.boosted ? ' 🔥 boosted' : ''}` - ).join('\n'); - await notifyAdmins( - `📊 Giveaway Report #${giveaway.id}\nChat ID: ${giveaway.chatId}\nBase SC each: ${giveaway.scPerWinner}\nTotal participants: ${eligibleCount}\nWinners:\n${winnerLines}\n\nAdmin actions:\n- Reroll: /admin then callback gw_reroll_${giveaway.id}\n- Mark paid: callback gw_paid_${giveaway.id}\n- Export: callback gw_export_${giveaway.id}`, - ); + // Full admin report per spec item 10 + const winnerDetailLines = giveaway.winners.map((w, i) => { + const handle = w.tgUsername ? `@${w.tgUsername}` : '(no handle)'; + const name = w.firstName || '(no name)'; + const rw = w.rwUsername || '(not linked)'; + const sc = w.awardedSc || giveaway.scPerWinner; + const boost = w.boosted ? ' 🔥 2x boost' : ''; + return `${i + 1}. TG: ${handle} | Name: ${name} | RW: ${rw} | Prize: ${sc} SC${boost}`; + }).join('\n'); + + const dmFailNote = dmFailedUsers.length > 0 + ? `\n⚠️ DM failed for ${dmFailedUsers.length} winner(s): ${dmFailedUsers.join(', ')} (may have blocked bot)` + : '\n✅ All winner DMs delivered.'; + + for (const adminId of ADMIN_IDS) { + try { + // eslint-disable-next-line no-await-in-loop + await bot.telegram.sendMessage( + adminId, + `📊 *Giveaway #${giveaway.id} — Final Report*\n\nChat ID: \`${giveaway.chatId}\`\nBase SC each: ${giveaway.scPerWinner}\nTotal participants: ${eligibleCount}\nWinners: ${giveaway.winners.length}\n\n${winnerDetailLines}${dmFailNote}\n\nAdmin actions:\n/gw_reroll_${giveaway.id} — Reroll\n/gw_paid_${giveaway.id} — Mark paid\n/gw_export_${giveaway.id} — Export`, + { + parse_mode: 'Markdown', + ...Markup.inlineKeyboard([ + [Markup.button.url('👀 View Results in Group', `https://t.me/c/${String(giveaway.chatId).replace('-100', '')}/${giveaway.announceMsgId || ''}`)], + [Markup.button.callback('🔄 Reroll', `gw_reroll_${giveaway.id}`), Markup.button.callback('✅ Mark Paid', `gw_paid_${giveaway.id}`)], + ]), + }, + ); + } catch (_) {} + } } /** @@ -12473,7 +12771,14 @@ function renderWinnersList(winners) { */ function renderWinnersText(giveaway) { - return `🎉 Giveaway ended!\nWinners (${giveaway.scPerWinner} SC each):\n${renderWinnersList(giveaway.winners)}\n\nAdmin will handle prize distribution.`; + // HTML format per spec item 9 + const winnerLines = (giveaway.winners || []).map((w) => { + const handle = w.tgUsername ? `@${escapeHtml(w.tgUsername)}` : escapeHtml(w.firstName || 'Winner'); + const sc = w.awardedSc || giveaway.scPerWinner; + const boost = w.boosted ? ' (2x boost applied)' : ''; + return `• ${handle} — ${sc} SC${boost}`; + }).join('\n'); + return `🎉 Giveaway Results!\n\nWinners:\n${winnerLines}\n\nThe bot will DM you automatically once your tip has been sent.`; } /** @@ -13296,6 +13601,38 @@ bot.command('testall', async (ctx) => { if (approvedGroupsStore && typeof approvedGroupsStore.size === 'number') pass('Database Checks', 'linked_groups_store_readable'); else fail('Database Checks', 'linked_groups_store_readable', 'approvedGroupsStore unreadable'); + // ── 11. Helpful Tooltips System ────────────────────────────────────────── + if (typeof tipsStore === 'object' && Array.isArray(tipsStore.tips)) pass('Helpful Tooltips', 'tipsStore_shape'); + else fail('Helpful Tooltips', 'tipsStore_shape', 'tipsStore is missing or malformed'); + if (tipsStore.tips.length > 0) pass('Helpful Tooltips', `tooltips_loaded (${tipsStore.tips.length})`); + else fail('Helpful Tooltips', 'tooltips_loaded', 'No tooltips in tipsStore.tips'); + if (typeof tipsStore.intervalHours === 'number' && tipsStore.intervalHours > 0) pass('Helpful Tooltips', `interval_hours (${tipsStore.intervalHours}h)`); + else fail('Helpful Tooltips', 'interval_hours', `Invalid interval: ${tipsStore.intervalHours}`); + if (tipsStore.targetGroup) pass('Helpful Tooltips', `target_linked (${tipsStore.targetGroupTitle || tipsStore.targetGroup})`); + else warn('Helpful Tooltips', 'target_linked', '⚠️ No target group/channel linked — use Settings → Link Channel/Group'); + if (typeof parseTooltipButtons === 'function') pass('Helpful Tooltips', 'parseTooltipButtons_defined'); + else fail('Helpful Tooltips', 'parseTooltipButtons_defined', 'parseTooltipButtons helper missing'); + // Validate button parser with a sample + try { + const pb = parseTooltipButtons('Test text\n[Label - https://example.com]'); + if (pb.keyboard && pb.text === 'Test text') pass('Helpful Tooltips', 'button_parser_valid'); + else throw new Error(`unexpected result: text="${pb.text}" keyboard=${!!pb.keyboard}`); + } catch (e) { fail('Helpful Tooltips', 'button_parser_valid', e.message); } + // Validate Open Bot button syntax + try { + const pbOB = parseTooltipButtons('Test\n[Open Bot]'); + if (pbOB.keyboard) pass('Helpful Tooltips', 'open_bot_button_syntax'); + else throw new Error('Open Bot button not parsed'); + } catch (e) { fail('Helpful Tooltips', 'open_bot_button_syntax', e.message); } + + // ── 12. Giveaway System Extended ──────────────────────────────────────── + if (typeof buildGiveawayAnnouncementText === 'function') pass('Giveaway System', 'buildGiveawayAnnouncementText_defined'); + else fail('Giveaway System', 'buildGiveawayAnnouncementText_defined', 'Missing helper'); + if (typeof scheduleGiveawayRefresh === 'function') pass('Giveaway System', 'scheduleGiveawayRefresh_defined'); + else fail('Giveaway System', 'scheduleGiveawayRefresh_defined', 'Missing 25% refresh scheduler'); + if (typeof giveawayPreflightCheck === 'function') pass('Giveaway System', 'preflight_check_defined'); + else fail('Giveaway System', 'preflight_check_defined', 'Missing preflight safety check'); + if (!process.env.HTTPS_KEY_PATH || !process.env.HTTPS_CERT_PATH) warn('Environment Checks', 'https_paths_optional', 'HTTPS cert/key not set (HTTP mode expected).'); else { try { @@ -13966,7 +14303,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}`, - `Content Drops/broadcast group target: ${broadcastConfigStore.targetGroup}`, + `Helpful Tooltips/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' }); diff --git a/prod-run.sh b/prod-run.sh index 8eaee7b..a768a75 100755 --- a/prod-run.sh +++ b/prod-run.sh @@ -259,6 +259,28 @@ else npm install --omit=dev fi +# --------------------------------------------------------- +# 6b) Auto-run tooltip generation (must run before bot restart) +say "Refreshing Helpful Tooltips..." +TOOLTIP_SCRIPT="$PROJECT_DIR/generate_tooltips.sh" +if [[ -x "$TOOLTIP_SCRIPT" ]]; then + if RUNEWAGER_DIR="$PROJECT_DIR" bash "$TOOLTIP_SCRIPT" >> /tmp/runewager-deploy.log 2>&1; then + say "Helpful tooltips refreshed." + else + warn "generate_tooltips.sh failed (non-fatal) — check /tmp/runewager-deploy.log" + # Attempt DM admin notification if bot token available + if [[ -n "${BOT_TOKEN:-}" && -n "${ADMIN_IDS:-}" ]]; then + ADMIN_ID="${ADMIN_IDS%%,*}" + curl -s "https://api.telegram.org/bot${BOT_TOKEN}/sendMessage" \ + -d chat_id="$ADMIN_ID" \ + -d text="⚠️ generate_tooltips.sh failed during prod-run — tooltips may be stale." \ + >/dev/null 2>&1 || true + fi + fi +else + warn "generate_tooltips.sh not found at $TOOLTIP_SCRIPT — skipping tooltip refresh" +fi + # --------------------------------------------------------- # 7) Project PID detection get_bot_pid() { pgrep -f "node .*${PROJECT_DIR}/index\.js" | head -n 1 || true; }