diff --git a/.env.example b/.env.example index ad1845b..21aa629 100644 --- a/.env.example +++ b/.env.example @@ -18,6 +18,13 @@ MINI_APP_CLAIM_URL=https://t.me/RuneWager_bot/claim MINI_APP_PLAY_URL=https://t.me/RuneWager_bot/Play MINI_APP_PROFILE_URL=https://t.me/RuneWager_bot/profile PORT=3000 +# RUNEWAGER_AUTO_UPDATE: Set to 1 to auto-pull origin/main on start/restart. +# Default: 1 in prod (start.sh), 0 in dev (dev-run.sh). +# Set to 0 on local/staging to avoid overwriting uncommitted changes. +RUNEWAGER_AUTO_UPDATE=1 +# RUNEWAGER_FORCE_RESET: Set to 1 to use git reset --hard instead of merge --ff-only. +# Destructive: discards all local uncommitted changes. Default: 0 (disabled). +RUNEWAGER_FORCE_RESET=0 PROMO_ENTRY_IMAGE_URL=https://raw.githubusercontent.com/gamblecodezcom/Runewager/main/images/promo_entry.png RW_DISCORD_JOIN=https://discord.gg/runewagers RW_DISCORD_LINK=https://discord.com/channels/1100486422395355197/1249181934811349052 diff --git a/RUNEWAGER_FUNCTIONALITY_MAP.md b/RUNEWAGER_FUNCTIONALITY_MAP.md index bab8abc..493674a 100644 --- a/RUNEWAGER_FUNCTIONALITY_MAP.md +++ b/RUNEWAGER_FUNCTIONALITY_MAP.md @@ -402,4 +402,37 @@ Mandatory rules for any AI agent touching this repo: - 2026-02-27: Added QA tester scaffolding (`qa/context/bot_capabilities.json`, `qa/context/repo_info.json`, `qa/state/provider_status.json`, `qa/README_QA.md`), with runtime refresh via `/qa_*` commands and 10-minute provider cooldown reset. - 2026-02-27: Hardened SSHV Run prompt flow so admin text in private DM executes against active SSHV sessions if pending state desynchronizes. - 2026-02-28: v3.1 โ€” added group command guard middleware (`GROUP_PASSTHROUGH_COMMANDS` + `bot.use` interceptor); added `onboardingProgressBar()` and progress header on each onboarding step prompt (auto-deletes after 8s); added one-time onboarding completion card (tracked via `user.onboarding.completionCardShown`); added `๐Ÿ”— Group Linking` to Admin System Tools keyboard (`admin_sys_group_linking` callback with back-to-system-tools navigation). +- 2026-03-01: Created `docs/` feature documentation system โ€” 15 per-feature `.md` files, central `docs/INDEX.md` with full callback + pending-action cross-reference, and `docs/TODO_FUNCTIONALITY_UPGRADE.md` tracking 14 open upgrade/stale-menu items. Future Claude sessions must consult `docs/INDEX.md` first, then the relevant feature `.md`, before reading `index.js`. +- 2026-03-01: Phase implementation โ€” resolved T-01/T-02/T-03/T-15 from TODO list. (1) Walkthrough: `sendWalkthroughStep()` upgraded with `clearOldMenus()`, Back disabled on step 1, Finish on last step, `walk_done` on last step returns to main menu. New doc: `16-walkthrough.md`. (2) Menu stacking: `clearOldMenus()` added to `sendOnboardingReferralPrompt`, `renderSshvConsole`, `renderGroupLinkingTools`, `tips_cmd_edit`, `tips_cmd_remove`. (3) Tooltip view: `tips_cmd_view` selector + `tip_view_{id}` handler with Prev/Next/Edit/Toggle/Delete/Back/AdminMenu โ€” "๐Ÿ‘ View Tooltip" button added to dashboard. (4) Broadcast failures: 500-item cap removed; all failures logged via `adminLog()`; `/broadcast_failed` shows chunks of 30 with overflow note; >20% failure rate triggers admin DM warnings. PR comments fixed: `add_tooltip.sh` array validation hardened; `docs/12-group-linking.md` entry-point callback corrected to `admin_sys_group_linking`; merge conflicts (PRs #112-114) resolved keeping SIGTERMโ†’SIGKILL safety improvements. 60/60 tests pass. - 2026-02-28: PR #112 review + audit pass โ€” fixed 10 issues: (R1) `await_tip_import_batch` dedicated pending type with JSON-array router; (R2) `generate_tooltips.sh` command-substitution pollution fixed via `RUNEWAGER_APP` env var; (R3) `add_tooltip.sh` shell-injection fixed via `TOOLTIP_TEXT_ENV`/`TOOLTIP_TMP_FILE` env vars and `<<'EOF'`; (R4) `catchAllCases` test extended with multiline patterns + `CATCH_ALL_CORES` updated; (R5) `extractCommandHandlerNames` test extended with `let`/`var`/no-semicolon fixtures; (R6) typo "auto-deletes 8s" โ†’ "auto-deletes after 8s"; (A1) dead `buildGiveawayAnnouncementText(giveaway,remainingStr)` removed; (A2) simplified `buildGiveawayAnnouncementKeyboard` with wrong callback removed; (A3+A4) duplicate `bot.action('admin_cat_system')` and `bot.action('admin_cat_support')` first registrations removed. All 60 tests pass. + +--- + +## 26. Feature Documentation System + +> **For all Claude sessions: start here, not by searching `index.js`.** + +All bot functionality is documented in `docs/`: + +| File | Contents | +|------|---------| +| [`docs/INDEX.md`](docs/INDEX.md) | **Primary index** โ€” every callback, command, pending action cross-referenced to its feature doc | +| [`docs/features/01-onboarding.md`](docs/features/01-onboarding.md) | `/start`, age gate, referral, username link | +| [`docs/features/02-user-menu.md`](docs/features/02-user-menu.md) | User persistent menu, settings, submenus | +| [`docs/features/03-admin-menu.md`](docs/features/03-admin-menu.md) | Admin persistent menu, stats, tools, controls | +| [`docs/features/04-giveaway.md`](docs/features/04-giveaway.md) | Full giveaway wizard + join + finalization | +| [`docs/features/05-bonus-30sc.md`](docs/features/05-bonus-30sc.md) | 30 SC wager bonus request + admin approval | +| [`docs/features/06-promos.md`](docs/features/06-promos.md) | Promo creation, claim, admin management | +| [`docs/features/07-tooltips.md`](docs/features/07-tooltips.md) | Tooltip add/edit/view/remove/import/settings | +| [`docs/features/08-referral.md`](docs/features/08-referral.md) | Referral codes, boosts, leaderboard | +| [`docs/features/09-sshv.md`](docs/features/09-sshv.md) | Admin VPS console, security, session GC | +| [`docs/features/10-deploy-ops.md`](docs/features/10-deploy-ops.md) | Deploy, rollback, health, testall, metrics | +| [`docs/features/11-user-lookup.md`](docs/features/11-user-lookup.md) | Whois, bonus status, schema refresh | +| [`docs/features/12-group-linking.md`](docs/features/12-group-linking.md) | Group/channel link, view, remove, test | +| [`docs/features/13-bug-reports.md`](docs/features/13-bug-reports.md) | User submit, admin view/resolve/export | +| [`docs/features/14-announcements.md`](docs/features/14-announcements.md) | Broadcast builder, preview, retry | +| [`docs/features/15-misc-commands.md`](docs/features/15-misc-commands.md) | All other commands + background timers | +| [`docs/features/16-walkthrough.md`](docs/features/16-walkthrough.md) | 35-step walkthrough, nav guards, completion | +| [`docs/TODO_FUNCTIONALITY_UPGRADE.md`](docs/TODO_FUNCTIONALITY_UPGRADE.md) | T-01โ€“T-15 upgrade log (T-01/02/03/15 resolved) | + +**Mandate:** Any added/changed/removed feature โ†’ update the relevant feature `.md` + `docs/INDEX.md` + this map section, in the same commit. diff --git a/add_tooltip.sh b/add_tooltip.sh index 9360ecc..cc42b1a 100755 --- a/add_tooltip.sh +++ b/add_tooltip.sh @@ -49,7 +49,14 @@ const file = process.argv[2]; const text = process.env.TOOLTIP_TEXT_ENV; const tmpFile = process.env.TOOLTIP_TMP_FILE; const list = JSON.parse(fs.readFileSync(file, 'utf8')); -const maxId = list.reduce((m, t) => Math.max(m, Number(t.id) || 0), 0); +if (!Array.isArray(list)) { + throw new Error('tooltips.json must contain a JSON array'); +} +// Only count entries that have a genuine finite numeric id +const numericIds = list + .map((t) => (t && Object.prototype.hasOwnProperty.call(t, 'id') ? Number(t.id) : NaN)) + .filter((id) => Number.isFinite(id)); +const maxId = numericIds.length ? Math.max(...numericIds) : 0; const newId = maxId + 1; list.push({ id: newId, text, enabled: true }); fs.writeFileSync(tmpFile, JSON.stringify(list, null, 2)); diff --git a/deploy.sh b/deploy.sh index a9b1e96..5711dc6 100755 --- a/deploy.sh +++ b/deploy.sh @@ -232,21 +232,11 @@ fi # --------------------------------------------------------- # 3c) Kill anything blocking the bot port before starting # --------------------------------------------------------- -DEPLOY_PORT="$(grep -E '^PORT=' "$PROJECT_DIR/.env" 2>/dev/null | head -1 | cut -d= -f2 | tr -d '"' | tr -d "'" | tr -d $'\r')" +DEPLOY_PORT="$(grep -E '^PORT=' "$PROJECT_DIR/.env" 2>/dev/null | head -1 | cut -d= -f2 | cut -d'#' -f1 | tr -d '"' | tr -d "'" | tr -d ' ' | tr -d $'\r')" DEPLOY_PORT="${DEPLOY_PORT:-3000}" -_BLOCKING="" -if command -v lsof >/dev/null 2>&1; then - _BLOCKING="$(lsof -ti :"$DEPLOY_PORT" 2>/dev/null || true)" -elif command -v fuser >/dev/null 2>&1; then - _BLOCKING="$(fuser -n tcp "$DEPLOY_PORT" 2>/dev/null | tr ' ' '\n' | sed '/^$/d' || true)" -fi -if [[ -n "$_BLOCKING" ]]; then - say "Port $DEPLOY_PORT blocked โ€” killing before startโ€ฆ" - for _pid in $_BLOCKING; do - kill -9 "$_pid" 2>/dev/null || true - done - sleep 1 -fi +# shellcheck source=scripts/helpers/free_port.sh +. "$PROJECT_DIR/scripts/helpers/free_port.sh" +free_port "$DEPLOY_PORT" # --------------------------------------------------------- # 4) Start bot via systemctl diff --git a/dev-run.sh b/dev-run.sh index 85b33b3..6eba445 100755 --- a/dev-run.sh +++ b/dev-run.sh @@ -19,11 +19,38 @@ if [ "$NODE_MAJOR" -lt 20 ] 2>/dev/null; then exit 1 fi -# Pull latest code -echo "[dev-run] Pulling latest code from origin main..." -git -C "$ROOT_DIR" fetch origin main 2>&1 \ - && git -C "$ROOT_DIR" reset --hard origin/main 2>&1 \ - || echo "[dev-run] WARN: git pull failed โ€” starting with local copy" +# Optionally pull latest code (off by default in dev to preserve local changes). +# Reads RUNEWAGER_AUTO_UPDATE from environment first, then from .env as fallback. +# Set RUNEWAGER_AUTO_UPDATE=1 in environment or .env to enable auto-pull. +_AUTO_UPD_DOTENV=$(grep -E '^RUNEWAGER_AUTO_UPDATE=' "$ROOT_DIR/.env" 2>/dev/null | head -1 | cut -d= -f2 | cut -d'#' -f1 | tr -d '"' | tr -d "'" | tr -d ' ' || true) +_AUTO_UPDATE="${RUNEWAGER_AUTO_UPDATE:-${_AUTO_UPD_DOTENV:-0}}" +if [ "$_AUTO_UPDATE" = "1" ]; then + echo "[dev-run] Fetching latest code from origin main..." + if git -C "$ROOT_DIR" fetch origin main 2>&1; then + # Read RUNEWAGER_FORCE_RESET from env then .env fallback (default: off). + # Only perform the destructive reset --hard when explicitly opted in. + _FORCE_RST_DOTENV=$(grep -E '^RUNEWAGER_FORCE_RESET=' "$ROOT_DIR/.env" 2>/dev/null | head -1 | cut -d= -f2 | cut -d'#' -f1 | tr -d '"' | tr -d "'" | tr -d ' ' || true) + _FORCE_RESET="${RUNEWAGER_FORCE_RESET:-${_FORCE_RST_DOTENV:-0}}" + if [ "$_FORCE_RESET" = "1" ]; then + echo "[dev-run] RUNEWAGER_FORCE_RESET=1 โ€” running git reset --hard origin/main..." + if git -C "$ROOT_DIR" reset --hard origin/main 2>&1; then + echo "[dev-run] Code hard-reset to $(git -C "$ROOT_DIR" rev-parse --short HEAD)" + else + echo "[dev-run] WARN: git reset --hard failed โ€” starting with local copy" + fi + else + if git -C "$ROOT_DIR" merge --ff-only origin/main 2>&1; then + echo "[dev-run] Code updated to $(git -C "$ROOT_DIR" rev-parse --short HEAD)" + else + echo "[dev-run] WARN: fast-forward merge failed (local commits diverged?). Set RUNEWAGER_FORCE_RESET=1 to hard-reset." + fi + fi + else + echo "[dev-run] WARN: git fetch failed โ€” starting with local copy" + fi +else + echo "[dev-run] RUNEWAGER_AUTO_UPDATE not set โ€” skipping git pull (set to 1 in .env or environment to enable)" +fi # Refresh tooltips TOOLTIP_SCRIPT="$ROOT_DIR/generate_tooltips.sh" @@ -34,18 +61,10 @@ if [ -x "$TOOLTIP_SCRIPT" ]; then fi # Kill anything blocking port 3000 (or PORT from .env) -DEV_PORT=$(grep -E '^PORT=' "$ROOT_DIR/.env" 2>/dev/null | head -1 | cut -d= -f2 | tr -d '"' | tr -d "'" || true) +DEV_PORT=$(grep -E '^PORT=' "$ROOT_DIR/.env" 2>/dev/null | head -1 | cut -d= -f2 | cut -d'#' -f1 | tr -d '"' | tr -d "'" | tr -d ' ' || true) DEV_PORT="${DEV_PORT:-3000}" -if command -v lsof >/dev/null 2>&1; then - _DEV_PIDS=$(lsof -ti :"$DEV_PORT" 2>/dev/null || true) -elif command -v fuser >/dev/null 2>&1; then - _DEV_PIDS=$(fuser -n tcp "$DEV_PORT" 2>/dev/null | tr ' ' '\n' | sed '/^$/d' || true) -fi -if [ -n "${_DEV_PIDS:-}" ]; then - echo "[dev-run] WARN: Port $DEV_PORT blocked โ€” killing..." - for _p in $_DEV_PIDS; do kill -9 "$_p" 2>/dev/null || true; done - sleep 1 -fi +bash "$ROOT_DIR/scripts/helpers/free_port.sh" "$DEV_PORT" \ + || echo "[dev-run] WARN: free_port.sh failed (non-fatal)" # Foreground local run (Termux-safe). Runtime env is loaded by index.js via dotenv. echo "[dev-run] Starting Runewager in foreground (Node $(node -v))..." diff --git a/docs/INDEX.md b/docs/INDEX.md new file mode 100644 index 0000000..a21dc72 --- /dev/null +++ b/docs/INDEX.md @@ -0,0 +1,183 @@ +# Runewager Bot โ€” Feature Documentation Index + +> **Purpose:** This is the primary navigation index for all Claude sessions. Before modifying any feature, read the relevant `.md` file here. After any change, update both the feature `.md` and this index. + +**Last updated:** 2026-03-01 +**Bot version:** 3.0.0 | `index.js`: ~15,050 lines | Commands: 95 | Action handlers: 270+ + +--- + +## Quick Navigation + +| # | Feature | File | Role | Status | +|---|---------|------|------|--------| +| 01 | Onboarding & Account Setup | [01-onboarding.md](features/01-onboarding.md) | User | โœ… Active | +| 02 | User Main Menu & Persistent Menu | [02-user-menu.md](features/02-user-menu.md) | User | โœ… Active | +| 03 | Admin Menu & Admin Persistent Menu | [03-admin-menu.md](features/03-admin-menu.md) | Admin | โœ… Active | +| 04 | Giveaway System (Wizard + Controls) | [04-giveaway.md](features/04-giveaway.md) | Both | โœ… Active | +| 05 | 30 SC Wager Bonus System | [05-bonus-30sc.md](features/05-bonus-30sc.md) | Both | โœ… Active | +| 06 | Promo Manager & Content Drops | [06-promos.md](features/06-promos.md) | Both | โœ… Active | +| 07 | Helpful Tooltips System | [07-tooltips.md](features/07-tooltips.md) | Admin | โœ… Active | +| 08 | Referral System | [08-referral.md](features/08-referral.md) | Both | โœ… Active | +| 09 | SSHV Admin Console | [09-sshv.md](features/09-sshv.md) | Admin | โœ… Active | +| 10 | Deploy & Admin Ops | [10-deploy-ops.md](features/10-deploy-ops.md) | Admin | โœ… Active | +| 11 | User Lookup & Management | [11-user-lookup.md](features/11-user-lookup.md) | Admin | โœ… Active | +| 12 | Group & Channel Linking | [12-group-linking.md](features/12-group-linking.md) | Admin | โœ… Active | +| 13 | Bug Reports | [13-bug-reports.md](features/13-bug-reports.md) | Both | โœ… Active | +| 14 | Announcements & Broadcast | [14-announcements.md](features/14-announcements.md) | Admin | โœ… Active | +| 15 | Misc Commands & Background Tasks | [15-misc-commands.md](features/15-misc-commands.md) | Both | โœ… Active | +| 16 | Walkthrough System | [16-walkthrough.md](features/16-walkthrough.md) | User | โœ… Active | + +--- + +## Complete Callback Index + +### User-Facing Callbacks + +| Callback Pattern | Feature Doc | Description | +|---|---|---| +| `age_yes` / `age_no` | 01-onboarding | Age gate confirmation | +| `onboard_ref_yes` / `onboard_ref_no` | 01-onboarding | Referral code option | +| `onboard_gcz_continue` | 01-onboarding | GambleCodez step | +| `onboard_skip_to_link` | 01-onboarding | Skip to username link | +| `menu_link_runewager` / `cancel_link` | 01-onboarding | Username link flow | +| `confirm_yes_username` / `confirm_no_username` | 01-onboarding | Username confirm | +| `menu_join_channel` / `menu_join_group` | 01-onboarding | Join verifications | +| `onboarding_next_step` | 01-onboarding | Advance step | +| `to_main_menu` | 02-user-menu | Return to main menu | +| `menu_page_1` / `menu_page_2` | 02-user-menu | Legacy main menu pages | +| `pmenu_claim_bonus` | 02-user-menu | Bonus options | +| `pmenu_my_profile` | 02-user-menu | Profile card | +| `pmenu_giveaways` | 02-user-menu | Active giveaways | +| `user_giveaways_page_{N}` | 02-user-menu | Giveaway pagination | +| `pmenu_referral` | 02-user-menu / 08-referral | Referral boost | +| `pmenu_help` | 02-user-menu | Help center | +| `help_open_booklet` | 02-user-menu | Open help booklet | +| `help_page_{N}` | 02-user-menu | Help pagination | +| `help_open_bugreport` | 13-bug-reports | Start bug report | +| `menu_settings_tab` | 02-user-menu | Settings menu | +| `settings_toggle_playmode` | 02-user-menu | Toggle play mode | +| `settings_toggle_quick_commands` | 02-user-menu | Toggle quick cmds | +| `settings_toggle_tooltips` | 02-user-menu | Toggle tips | +| `menu_qc_play` / `menu_qc_profile` / `menu_qc_status` | 02-user-menu | Quick commands | +| `gw_join_{id}` | 04-giveaway | Join giveaway | +| `w30_request_start` | 05-bonus-30sc | Open bonus menu | +| `w30_menu_how` / `w30_menu_eligibility` | 05-bonus-30sc | Bonus info | +| `w30_menu_request` | 05-bonus-30sc | Submit request | +| `w30_my_status` / `menu_bonus_status` | 05-bonus-30sc | Bonus status | +| `menu_claim_bonus` | 06-promos | View promos | +| `promo_open_{id}` | 06-promos | Promo detail | +| `promo_claim_{id}` | 06-promos | Claim promo | +| `ref_leaderboard` | 08-referral | Referral leaderboard | +| `ref_menu_code` / `ref_menu_share` / `ref_menu_how` | 08-referral | Referral submenu | +| `menu_bugreport` | 13-bug-reports | Start bug report | + +### Admin-Facing Callbacks + +| Callback Pattern | Feature Doc | Description | +|---|---|---| +| `pmenu_admin` | 03-admin-menu | Open admin menu | +| `pamenu_status` | 03-admin-menu | System status | +| `pamenu_stats` / `pamenu_stats_{window}` | 03-admin-menu | Stats time windows | +| `pamenu_start_giveaway` | 04-giveaway | Start wizard | +| `pamenu_active_giveaways` | 04-giveaway | List giveaways | +| `admin_gw_page_{N}` | 04-giveaway | Giveaway pagination | +| `pamenu_gw_end_{id}` / `pamenu_gw_extend_{id}` | 04-giveaway | Giveaway controls | +| `pamenu_gw_cancel_{id}` / `pamenu_gw_participants_{id}` | 04-giveaway | Giveaway controls | +| `pamenu_tools` | 03-admin-menu | Tools submenu | +| `pamenu_tools_refresh` / `pamenu_tools_clear_flows` | 03-admin-menu | Tool actions | +| `pamenu_tools_health` / `pamenu_tools_logs` | 03-admin-menu | Health/logs | +| `pamenu_bug_reports` | 13-bug-reports | Admin bug list | +| `pamenu_back_user` / `pamenu_back_admin` | 03-admin-menu | Menu navigation | +| `gwiz_cancel` | 04-giveaway | Abort wizard | +| `gwiz_title_skip` | 04-giveaway | Skip title | +| `gwiz_sc_{N}` / `gwiz_sc_custom` | 04-giveaway | SC selection | +| `gwiz_winners_{N}` / `gwiz_winners_custom` | 04-giveaway | Winner selection | +| `gwiz_dur_{N}` / `gwiz_dur_custom` | 04-giveaway | Duration selection | +| `gwiz_minp_{N}` / `gwiz_minp_custom` | 04-giveaway | Min parts selection | +| `gwiz_surface_group` / `gwiz_surface_dm` / `gwiz_surface_done` | 04-giveaway | Surface toggle | +| `gwiz_joininfo_done` | 04-giveaway | Confirm join info | +| `gw_create_yes` / `gw_create_no` | 04-giveaway | Create or abort | +| `admin_promo_manager` | 06-promos | Promo manager | +| `admin_pm_create` / `admin_pm_edit` | 06-promos | Promo CRUD | +| `admin_pm_pause_toggle` / `admin_pm_delete` | 06-promos | Promo state | +| `admin_cmd_tips_dashboard` | 07-tooltips | Tips manager | +| `tips_cmd_add` / `tips_cmd_edit` / `tips_cmd_remove` | 07-tooltips | Tip CRUD | +| `tips_cmd_view` | 07-tooltips | Open tip view selector | +| `tip_view_{id}` | 07-tooltips | Preview tip with nav/edit/delete | +| `tips_cmd_toggle` / `tips_cmd_list` / `tips_cmd_test` | 07-tooltips | Tip ops | +| `tips_cmd_import_batch` | 07-tooltips | Batch import | +| `tips_cmd_settings` / `tips_settings_back` | 07-tooltips | Tip settings | +| `tips_set_interval` / `tips_set_link_target` | 07-tooltips | Settings | +| `tips_select_cancel` | 07-tooltips | Cancel selection | +| `tip_remove_{id}` | 07-tooltips | Remove tip | +| `tip_edit_select_{id}` | 07-tooltips | Edit tip | +| `tip_toggle_{id}` | 07-tooltips | Toggle tip | +| `sshv_open` / `sshv_run_prompt` / `sshv_refresh` | 09-sshv | SSHV console | +| `sshv_ctrl_c` / `sshv_ctrl_z` / `sshv_lock` / `sshv_unlock` | 09-sshv | SSHV controls | +| `sshv_confirm_run` / `sshv_cancel_run` / `sshv_exit` | 09-sshv | SSHV ops | +| `admin_cmd_testall` / `admin_cmd_health` / `admin_cmd_version` | 10-deploy-ops | Diagnostics | +| `admin_cmd_verify_setup` / `admin_backup_action` | 10-deploy-ops | System ops | +| `admin_cmd_mode_toggle` / `admin_cmd_mode_on` / `admin_cmd_mode_off` | 10-deploy-ops | Admin mode | +| `admin_cmd_whois_prompt` / `admin_cmd_bonusstatus_prompt` | 11-user-lookup | User lookup | +| `admin_cmd_refreshuser_prompt` | 11-user-lookup | Schema refresh | +| `settings_group_linking_tools` | 12-group-linking | Group linking | +| `group_link_start` / `group_link_view` | 12-group-linking | Link ops | +| `group_link_remove_menu` / `group_link_remove_{id}` | 12-group-linking | Unlink | +| `group_link_test_permissions` | 12-group-linking | Perm check | +| `admin_cmd_viewbugs` / `admin_cmd_resolvebug_prompt` | 13-bug-reports | Bug mgmt | +| `admin_cmd_exportbugs` | 13-bug-reports | Export bugs | +| `admin_cmd_announce_start` | 14-announcements | Broadcast | + +--- + +## Pending Action Types โ€” Master List + +| Type | Feature | Description | +|------|---------|-------------| +| `await_referral_code` | 01-onboarding | Enter referral code during onboarding | +| `await_runewager_username` | 01-onboarding | Enter Runewager username | +| `await_username_confirm` | 01-onboarding | Confirm detected username | +| `await_bugreport` | 13-bug-reports | Enter bug description | +| `await_tip_add_text` | 07-tooltips | Enter new tooltip text | +| `await_tip_edit_text` | 07-tooltips | Enter updated tooltip text | +| `await_tip_import_batch` | 07-tooltips | Paste JSON array of tips | +| `await_tip_settings_interval` | 07-tooltips | Enter interval hours | +| `await_tip_link_target` | 07-tooltips | Forward message to link group | +| `await_register_chat_forward` | 12-group-linking | Forward to link group | +| `gwiz_await_title` | 04-giveaway | Giveaway title | +| `gwiz_await_custom_sc` | 04-giveaway | Custom SC amount | +| `gwiz_await_custom_winners` | 04-giveaway | Custom winner count | +| `gwiz_await_custom_duration` | 04-giveaway | Custom duration (minutes) | +| `gwiz_await_custom_minparts` | 04-giveaway | Custom min participants | +| `w30_await_wager_total` | 05-bonus-30sc | Declare wager amount | +| `w30_admin_pick_approve` | 05-bonus-30sc | Admin approve | +| `w30_admin_pick_deny` | 05-bonus-30sc | Admin deny (+ reason) | +| `w30_admin_lookup` | 05-bonus-30sc | Admin user lookup | +| `w30_admin_reset` | 05-bonus-30sc | Admin reset bonus | +| `w30_admin_link_username` | 05-bonus-30sc | Admin manual link | +| `admin_pm_create_name` | 06-promos | Promo name | +| `admin_pm_create_code` | 06-promos | Promo code | +| `admin_pm_edit_select_id` | 06-promos | Select promo to edit | +| `admin_pm_edit_field` | 06-promos | New field value | +| `await_sshv_command` | 09-sshv | VPS command text | +| `await_sshv_editor_content` | 09-sshv | File editor content | +| `await_admin_whois` | 11-user-lookup | Lookup userId/username | +| `await_admin_bonusstatus` | 11-user-lookup | Bonus check userId | +| `await_admin_refreshuser` | 11-user-lookup | Schema refresh userId | +| `await_admin_resolvebug` | 13-bug-reports | Resolve bug by ID | + +--- + +## Architecture Notes + +- **Single-file monolith:** All logic in `index.js` (~14,960 lines). +- **State:** In-memory Maps/Sets; persisted to `data/runtime-state.json` every 15s. +- **Menu lifecycle:** `clearOldMenus()` โ†’ `replyMenu()` / `replaceCallbackPanel()` โ†’ `sendPersistentUserMenu()`. +- **Pending actions:** 15-minute timeout via `evaluatePendingActionTimeout()`. +- **User mutations:** `runUserMutation(userId, fn)` โ€” queue prevents race conditions. +- **Error handling:** `bot.catch()` global handler + `uncaughtException`/`unhandledRejection` process handlers. + +--- + +## Known Issues โ†’ See TODO_FUNCTIONALITY_UPGRADE.md diff --git a/docs/TODO_FUNCTIONALITY_UPGRADE.md b/docs/TODO_FUNCTIONALITY_UPGRADE.md new file mode 100644 index 0000000..ad0406e --- /dev/null +++ b/docs/TODO_FUNCTIONALITY_UPGRADE.md @@ -0,0 +1,161 @@ +# TODO: Functionality Upgrade & Stale Menu Log + +> Maintained by Claude at end of every coding session. Each entry has title, type, location, impact, and proposed fix. +> **Last updated:** 2026-03-01 + +--- + +## Priority Legend +- ๐Ÿ”ด P1 โ€” Breaks user experience / unreachable flow +- ๐ŸŸก P2 โ€” Degraded UX / incomplete navigation +- ๐Ÿ”ต P3 โ€” Polish / doc drift / minor inconsistency + +--- + +## OPEN ITEMS + +--- + +### [T-01] Walkthrough buttons have no handlers +**Type:** Missing handler +**Priority:** โœ… RESOLVED โ€” Fixed 2026-03-01 +**Fix:** Handlers already existed. `sendWalkthroughStep()` upgraded: `clearOldMenus()` added, Back disabled on step 1, Next replaced with Finish on last step, `walk_done` on last step sets `started=false` and returns to main menu. New doc: `docs/features/16-walkthrough.md`. + +--- + +### [T-02] `clearOldMenus` missing in 6 locations +**Type:** Stale menu / menu stacking +**Priority:** โœ… RESOLVED โ€” Fixed 2026-03-01 +**Fix:** `clearOldMenus(ctx, user)` added to all 5 confirmed locations: +- `sendOnboardingReferralPrompt()` โœ… +- `renderSshvConsole()` โœ… +- `renderGroupLinkingTools()` โœ… +- `tips_cmd_edit` handler โœ… +- `tips_cmd_remove` handler โœ… +(Note: `sendGiveawayListPage()` already used `replyMenu()` which internally calls `clearOldMenus` โ€” no fix needed.) + +--- + +### [T-03] Tooltip system missing `tip_view_{id}` handler +**Type:** Missing functionality +**Priority:** โœ… RESOLVED โ€” Fixed 2026-03-01 +**Fix:** Implemented `tips_cmd_view` callback (selector) + `bot.action(/^tip_view_(\d+)$/)` handler with full preview panel: Prev/Next navigation, Edit/Toggle/Delete buttons, Back to List, Admin Menu. Added "๐Ÿ‘ View Tooltip" button to `tipsDashboardKeyboard()`. Docs updated in `docs/features/07-tooltips.md`. + +--- + +### [T-04] `tipsDashboardKeyboard` missing "Preview All" button +**Type:** Missing functionality +**Priority:** ๐Ÿ”ต P3 +**Location:** `index.js` โ€” `tipsDashboardKeyboard()` ~11463 +**Impact:** Admin cannot preview all enabled tips sequentially before system goes live. +**Proposed fix:** +1. Add `๐Ÿ“ข Preview All` button โ†’ callback `tips_cmd_preview_all`. +2. Add handler: iterate enabled tips, send each to admin DM with 1s delay. +3. Report: "Sent N tip previews to your DM." + +--- + +### [T-05] `tipsSettingsKeyboard` missing "Main Menu" button +**Type:** Incomplete navigation +**Priority:** ๐Ÿ”ต P3 +**Location:** `index.js` โ€” `tipsSettingsKeyboard()` ~11743 +**Impact:** Admin in settings must click "Back to Tooltips" then "Admin Menu" โ€” two clicks instead of one. +**Proposed fix:** Add `๐Ÿ  Main Menu` โ†’ `pamenu_back_admin` as second row in `tipsSettingsKeyboard()`. + +--- + +### [T-06] `tips_select_cancel` doesn't clear old menus +**Type:** Stale menu +**Priority:** ๐Ÿ”ต P3 +**Location:** `index.js` โ€” `tips_select_cancel` ~11790 +**Impact:** Selector panel remains after cancel; only a plain text "Cancelled." is sent. +**Proposed fix:** Call `await clearOldMenus(ctx, user)` or edit the message before sending cancel reply. + +--- + +### [T-07] Code duplication: port-kill logic in 5 scripts +**Type:** Code quality / maintenance +**Priority:** ๐Ÿ”ต P3 +**Location:** `deploy.sh`, `dev-run.sh`, `start.sh`, `scripts/rollback.sh`, `prod-run.sh` +**Impact:** Maintenance overhead โ€” bugs must be fixed in 5 places (e.g., SIGTERM-first was missed in early versions). +**Proposed fix:** Extract to `scripts/helpers/free_port.sh` sourced by all scripts. `prod-run.sh` already has `free_port_if_conflicted()` โ€” generalize as a shared function. + +--- + +### [T-08] Port-kill in scripts uses lsof/fuser only โ€” systemd conflict risk +**Type:** Race condition / systemd interaction +**Priority:** ๐Ÿ”ต P3 +**Location:** `deploy.sh` ~235, `start.sh` ~64, `dev-run.sh` ~37, `scripts/rollback.sh` ~104 +**Impact:** Pre-start port kill may terminate the legitimate systemd-managed process before systemd can cleanly stop it, causing service record inconsistency. +**Proposed fix:** In systemd environments, prefer `systemctl stop {SERVICE}` before port kill. Port kill should only run as fallback after systemctl stop. + +--- + +### [T-09] `showOnboardingPrompt` JSDoc step count was 1โ€“4 (fixed in PR #113) +**Type:** Doc drift +**Priority:** โœ… RESOLVED โ€” Fixed 2026-02-28 + +--- + +### [T-10] Markdown injection in `runewagerUsername` (fixed in PR #113) +**Type:** Bug / security +**Priority:** โœ… RESOLVED โ€” Fixed 2026-02-28 + +--- + +### [T-11] Missing test assertions for (.|)* (.|)+ catch-all forms (fixed in PR #113) +**Type:** Test gap +**Priority:** โœ… RESOLVED โ€” Fixed 2026-02-28 + +--- + +### [T-12] PORT inline comment not stripped from .env (fixed in PR #113) +**Type:** Bug +**Priority:** โœ… RESOLVED โ€” Fixed 2026-02-28 + +--- + +### [T-13] Admin stats window indicator missing "Refresh" button +**Type:** UX improvement +**Priority:** ๐Ÿ”ต P3 +**Location:** `index.js` โ€” `adminStatsKeyboard(activeWindow)` ~3530 +**Impact:** After viewing stats, admin must manually re-open the stats to refresh data. +**Proposed fix:** Add `๐Ÿ”„ Refresh` button that re-fetches the current window. +(Note: A Refresh button was added in v3.0 โ€” verify it persists in current code.) + +--- + +### [T-14] `/language` command is a stub +**Type:** Dead command / stale +**Priority:** ๐Ÿ”ต P3 +**Location:** `index.js` ~5850 +**Impact:** Command exists in REGISTERED_COMMANDS and is visible to users but does nothing meaningful. +**Proposed fix:** Either implement language selection or remove the command from `REGISTERED_COMMANDS` and `groupCommands`. + +--- + +### [T-15] `broadcastFailedUsers` capped at 500 (may silently drop entries) +**Type:** Data loss risk +**Priority:** โœ… RESOLVED โ€” Fixed 2026-03-01 +**Fix:** Removed 500-item cap entirely (both in-memory push and on-load). Each failure is now logged to `data/admin-events.log` via `adminLog('broadcast_failure', ...)`. `/broadcast_failed` shows chunks of 30 (up to 90 inline) with overflow note pointing to log file. Added high-failure-rate โš ๏ธ warning: >20% failure rate triggers DM to all `ADMIN_IDS`. Docs updated in `docs/features/14-announcements.md`. + +--- + +## COMPLETED (Reference) + +| ID | Title | Fixed In | Date | +|----|-------|---------|------| +| T-09 | showOnboardingPrompt JSDoc | PR #113 | 2026-02-28 | +| T-10 | Markdown injection runewagerUsername | PR #113 | 2026-02-28 | +| T-11 | Missing (.\|)* test assertions | PR #113 | 2026-02-28 | +| T-12 | PORT inline comment stripping | PR #113 | 2026-02-28 | +| R1 | await_tip_import_batch pending type | PR #112 | 2026-02-28 | +| R2 | generate_tooltips.sh command substitution | PR #112 | 2026-02-28 | +| R3 | add_tooltip.sh shell injection | PR #112 | 2026-02-28 | +| R4 | catchAllCases coverage | PR #112 | 2026-02-28 | +| R5 | extractCommandHandlerNames let/var | PR #112 | 2026-02-28 | +| R6 | Typo "auto-deletes 8s" | PR #112 | 2026-02-28 | +| A1 | buildGiveawayAnnouncementText duplicate | PR #112 | 2026-02-28 | +| A2 | buildGiveawayAnnouncementKeyboard duplicate | PR #112 | 2026-02-28 | +| A3 | admin_cat_system duplicate action | PR #112 | 2026-02-28 | +| A4 | admin_cat_support duplicate action | PR #112 | 2026-02-28 | diff --git a/docs/features/01-onboarding.md b/docs/features/01-onboarding.md new file mode 100644 index 0000000..d0183b7 --- /dev/null +++ b/docs/features/01-onboarding.md @@ -0,0 +1,98 @@ +# Feature: Onboarding & Account Setup + +**ID:** onboarding +**Role:** User +**Status:** Active + +--- + +## Purpose + +Guides a new user from `/start` through age verification, optional referral code entry, GambleCodez account creation prompt, Runewager username linking, channel/group join confirmations, and finally the persistent main menu. There are 5 ordered steps. + +--- + +## Entry Points + +| Trigger | Label | Callback / Command | +|---------|-------|-------------------| +| Telegram "Start" button | โ€” | `/start` | +| Age gate "Yes" | โœ… I'm 18+ | `age_yes` | +| Age gate "No" | โŒ I'm under 18 | `age_no` | + +--- + +## Flow Steps + +``` +/start + โ””โ”€โ”€ New user โ†’ Age gate (ageGateKeyboard) + โ”œโ”€โ”€ age_yes โ†’ onboard step 1: referral prompt + โ”‚ โ”œโ”€โ”€ onboard_ref_yes โ†’ await_referral_code (text input) + โ”‚ โ”‚ โ””โ”€โ”€ Valid code โ†’ boost applied โ†’ step 2 + โ”‚ โ””โ”€โ”€ onboard_ref_no โ†’ step 2: GambleCodez info + โ”‚ โ””โ”€โ”€ onboard_gcz_continue โ†’ step 3: signup + โ”‚ โ”œโ”€โ”€ onboard_skip_to_link โ†’ step 4: username link + โ”‚ โ””โ”€โ”€ menu_link_runewager โ†’ await_runewager_username + โ”‚ โ””โ”€โ”€ confirm_yes_username / confirm_no_username + โ”‚ โ””โ”€โ”€ step 5: join channel/group + โ”‚ โ”œโ”€โ”€ menu_join_channel โ†’ hasJoinedChannel = true + โ”‚ โ””โ”€โ”€ menu_join_group โ†’ hasJoinedGroup = true + โ”‚ โ””โ”€โ”€ onboarding_next_step โ†’ main menu + โ””โ”€โ”€ age_no โ†’ error message, bot stops +``` + +### Pending Action Types + +| Type | Trigger | Handler | +|------|---------|---------| +| `await_referral_code` | `onboard_ref_yes` | Text router: validates code, applies boost | +| `await_runewager_username` | `menu_link_runewager` | Text router: smart username detection, confirm | +| `await_username_confirm` | Smart detection ambiguous | `confirm_yes_username` / `confirm_no_username` | + +--- + +## Buttons & Callbacks + +| Button Label | Callback Data | Handler Location | +|---|---|---| +| โœ… I'm 18+ | `age_yes` | index.js ~8107 | +| โŒ I'm under 18 | `age_no` | index.js ~8223 | +| โœ… Yes, I have a code | `onboard_ref_yes` | index.js ~8131 | +| โŒ No, skip | `onboard_ref_no` | index.js ~8138 | +| โ–ถ Continue | `onboard_gcz_continue` | index.js ~8176 | +| โญ Skip to Link | `onboard_skip_to_link` | index.js ~8201 | +| ๐Ÿ”— Link Runewager | `menu_link_runewager` | index.js ~8281 | +| โŒ Cancel | `cancel_link` | index.js ~8288 | +| โœ… Yes, that's me | `confirm_yes_username` | index.js ~8367 | +| โŒ No, re-enter | `confirm_no_username` | index.js ~8380 | +| ๐Ÿ“ข Join Channel | `menu_join_channel` | index.js ~8413 | +| ๐Ÿ‘ฅ Join Group | `menu_join_group` | index.js ~8424 | +| โžก Next | `onboarding_next_step` | index.js ~8296 | + +--- + +## Dependencies + +- `showOnboardingPrompt(ctx, user, step)` โ€” sends progress bar + step prompt +- `onboardingProgressBar(step)` โ€” builds 1โ€“5 step progress indicator +- `finaliseUsernameLink(ctx, user, username)` โ€” completes username link, calls `applyOnboardingReferralCode()` +- `applyOnboardingReferralCode(user, code)` โ€” validates referral, assigns 7-day boosts +- `ageGateKeyboard()` โ€” keyboard builder + +--- + +## Edge Cases + +- Referral code is one-time during onboarding only โ€” post-onboarding attempts are rejected. +- No self-referral (checked by `applyOnboardingReferralCode`). +- Smart username detection: if bot finds a probable match from Telegram username, asks to confirm before fully linking. +- `pending_action` timeout (15 min) applies to all text input steps. + +--- + +## File References + +- `index.js`: `/start` handler ~5817, age gate ~8107, onboard callbacks ~8131โ€“8428 +- `index.js`: `showOnboardingPrompt` ~4581, `onboardingProgressBar` ~4555 +- `index.js`: `finaliseUsernameLink` ~8330, `applyOnboardingReferralCode` ~1400+ diff --git a/docs/features/02-user-menu.md b/docs/features/02-user-menu.md new file mode 100644 index 0000000..6eccfee --- /dev/null +++ b/docs/features/02-user-menu.md @@ -0,0 +1,109 @@ +# Feature: User Main Menu & Persistent Menu + +**ID:** user_menu +**Role:** User +**Status:** Active + +--- + +## Purpose + +The user-facing persistent menu is pinned at the bottom of the DM chat and provides access to all major user features: play, promos, giveaways, profile, referrals, settings, and help. A second paginated main menu is accessible via `/menu`. + +--- + +## Entry Points + +| Trigger | Callback / Command | +|---------|-------------------| +| `/menu` command | `/menu` | +| `/start` (after onboarding) | Sends persistent user menu | +| Button: Open Menu | `to_main_menu` | +| Button: Back to Menu | `to_main_menu` | + +--- + +## Persistent Menu Buttons & Callbacks + +| Button Label | Callback Data | Purpose | +|---|---|---| +| ๐ŸŽฎ Play | `menu_qc_play` | Launch play button (browser or miniapp mode) | +| ๐Ÿ’ฐ Claim Bonus | `pmenu_claim_bonus` | Bonus options (new user promo, 30 SC) | +| ๐ŸŽ Promos | (opens promo list) | `menu_claim_bonus` or `pmenu_claim_bonus` | +| ๐ŸŽ‰ Giveaways | `pmenu_giveaways` | Active giveaways paginated list | +| ๐Ÿ‘ค My Profile | `pmenu_my_profile` | Profile card with linked username, badges | +| ๐Ÿ”— Referrals | `pmenu_referral` | Referral boost meter & share link | +| โ“ Help | `pmenu_help` | Help center menu | +| โš™๏ธ Settings | `menu_settings_tab` | Settings toggles | + +--- + +## Submenu: Giveaways (`pmenu_giveaways`) + +- Shows active giveaways, 5 per page. +- Pagination: `user_giveaways_page_{N}` +- Each giveaway card has **Join** button โ†’ `gw_join_{id}` +- Back: returns to main menu. + +--- + +## Submenu: Profile (`pmenu_my_profile`) + +- Shows user stats: username, SC balance, boost status, badges. +- Buttons: + - ๐Ÿ”— Link Account โ†’ `menu_link_runewager` + - โš™๏ธ Settings โ†’ `menu_settings_tab` + - ๐Ÿ† My Status โ†’ `menu_bonus_status` + +--- + +## Submenu: Referral (`pmenu_referral`) + +- Shows referral code, boost meter progress. +- Buttons: + - ๐Ÿ“ค Share Link โ†’ `ref_menu_share` + - ๐Ÿ“‹ My Code โ†’ `ref_menu_code` + - โ“ How It Works โ†’ `ref_menu_how` + +--- + +## Submenu: Help (`pmenu_help`) + +- Shows help menu. +- Buttons: + - ๐Ÿ“– Open Booklet โ†’ `help_open_booklet` โ†’ `help_page_{N}` + - ๐Ÿ› Bug Report โ†’ `help_open_bugreport` โ†’ `await_bugreport` + +--- + +## Submenu: Settings (`menu_settings_tab`) + +| Toggle | Callback | Effect | +|--------|----------|--------| +| ๐ŸŽฎ Play Mode | `settings_toggle_playmode` | Browser โ†” Mini App | +| โŒจ๏ธ Quick Commands | `settings_toggle_quick_commands` | Show/hide quick command labels | +| ๐Ÿ’ก Tooltips | `settings_toggle_tooltips` | Enable/disable random tips in DM | + +--- + +## Keyboards + +- `userMainMenuKeyboard(isAdmin, user)` ~index.js:4048 โ€” main persistent keyboard +- `mainMenuKeyboard(isAdmin, page, user)` ~index.js:2796 โ€” legacy 2-page keyboard +- `settingsKeyboard(user)` ~index.js:3437 โ€” settings toggles + +--- + +## Dependencies + +- `sendPersistentUserMenu(ctx, user)` โ€” updates/pins the persistent menu +- `clearOldMenus(ctx, user)` โ€” deletes previous menus before sending new one +- `getPlayLink(user)` โ€” resolves play URL based on `user.playMode` + +--- + +## File References + +- `index.js`: `/menu` ~5817, `to_main_menu` ~7388 +- `index.js`: `pmenu_*` callbacks ~7411โ€“7641 +- `index.js`: `sendPersistentUserMenu` ~4150+ diff --git a/docs/features/03-admin-menu.md b/docs/features/03-admin-menu.md new file mode 100644 index 0000000..865b3ad --- /dev/null +++ b/docs/features/03-admin-menu.md @@ -0,0 +1,118 @@ +# Feature: Admin Menu & Admin Persistent Menu + +**ID:** admin_menu +**Role:** Admin +**Status:** Active + +--- + +## Purpose + +The admin persistent menu provides one-tap access to all admin operations: bot stats, giveaway management, promo tools, user lookup, system health, and operator tools. Accessed via `/admin` or by being in admin mode. + +--- + +## Entry Points + +| Trigger | Callback / Command | +|---------|-------------------| +| `/admin` command | `/admin` | +| `/on` / `/off` | Toggle admin mode visibility | +| Button: Admin Panel | `pmenu_admin` | + +--- + +## Persistent Admin Menu Buttons & Callbacks + +| Button Label | Callback Data | Purpose | +|---|---|---| +| ๐Ÿ“Š Status | `pamenu_status` | System health panel (memory, uptime, error rate) | +| ๐Ÿ“ˆ Stats | `pamenu_stats` | Stats time-window selector | +| ๐ŸŽ‰ Start Giveaway | `pamenu_start_giveaway` | Launch giveaway wizard | +| ๐ŸŽ Active Giveaways | `pamenu_active_giveaways` | Paginated list of running giveaways | +| ๐Ÿ”ง Tools | `pamenu_tools` | Admin tools submenu | +| โ“ Admin Help | `pamenu_admin_help` | Admin help pages | +| ๐Ÿ› Bug Reports | `pamenu_bug_reports` | View open user bug reports | +| ๐Ÿ‘ค User Menu | `pamenu_back_user` | Switch to user persistent menu | + +--- + +## Submenu: Stats (`pamenu_stats`) + +Time-window selector โ†’ shows stats for chosen window: + +| Button | Callback | Period | +|--------|----------|--------| +| ๐Ÿ“… 24h | `pamenu_stats_24h` | Last 24 hours | +| ๐Ÿ“… 7 Days | `pamenu_stats_7d` | Last 7 days | +| ๐Ÿ“… 30 Days | `pamenu_stats_30d` | Last 30 days | +| ๐Ÿ—‚ Lifetime | `pamenu_stats_lifetime` | All-time totals | +| ๐Ÿ”„ Refresh | (inline refresh) | Re-pull same window | + +--- + +## Submenu: Active Giveaways (`pamenu_active_giveaways`) + +- Lists running giveaways, 5 per page. +- Pagination: `admin_gw_page_{N}` +- Per-giveaway buttons: + - ๐Ÿ End โ†’ `pamenu_gw_end_{id}` โ€” finalize early + - โฑ Extend โ†’ `pamenu_gw_extend_{id}` โ€” add 15 minutes + - โŒ Cancel โ†’ `pamenu_gw_cancel_{id}` โ€” delete without winners + - ๐Ÿ‘ฅ Participants โ†’ `pamenu_gw_participants_{id}` โ€” list entries + +--- + +## Submenu: Tools (`pamenu_tools`) + +| Button | Callback | Effect | +|--------|----------|--------| +| ๐Ÿ”„ Refresh Menus | `pamenu_tools_refresh` | Clear stale menu message IDs | +| ๐Ÿงน Clear Flows | `pamenu_tools_clear_flows` | Cancel all pending user actions | +| ๐Ÿฅ Health | `pamenu_tools_health` | Fetch `/health` HTTP endpoint | +| ๐Ÿ“‹ Logs | `pamenu_tools_logs` | Show recent log lines | + +--- + +## Admin Dashboard (Legacy `/admin` keyboard) + +Accessed via `/admin`; tabbed UI: + +| Tab | Callback | Contents | +|-----|----------|---------| +| Giveaway | `admin_cat_giveaway` | Giveaway manager controls | +| Promo | `admin_cat_promo` | Promo manager controls | +| System | `admin_cat_system` | System toggles, health, deploy | +| Support | `admin_cat_support` | Bug reports, support portal | +| Tests | `admin_cat_tests` | Run testall, diagnostics | +| Users | (admin_cat_users) | Whois, bonus lookup | + +--- + +## Keyboards + +- `adminMainMenuKeyboard(user)` ~index.js:4125 +- `adminKeyboard()` ~index.js:2956 (legacy) +- `adminDashboardKeyboard(page)` ~index.js:3481 +- `adminStatsKeyboard(activeWindow)` ~index.js:3530 +- `adminGiveawayToolsKeyboard()` ~index.js:3565 +- `adminPromoToolsKeyboard()` ~index.js:3602 +- `adminUserToolsKeyboard()` ~index.js:3640 +- `adminSystemToolsKeyboard(user)` ~index.js:3672 +- `adminSupportToolsKeyboard()` ~index.js:3709 + +--- + +## Dependencies + +- `sendPersistentAdminMenu(ctx, user)` โ€” updates/pins admin menu +- `clearOldMenus(ctx, user)` โ€” clears old menus before sending +- `requireAdmin(ctx)` โ€” guard used in all admin callbacks + +--- + +## File References + +- `index.js`: `/admin` ~6340, admin mode `/on`/`/off` ~7250/7259 +- `index.js`: `pamenu_*` callbacks ~7838โ€“8100 +- `index.js`: `sendPersistentAdminMenu` ~4200+ diff --git a/docs/features/04-giveaway.md b/docs/features/04-giveaway.md new file mode 100644 index 0000000..34ab634 --- /dev/null +++ b/docs/features/04-giveaway.md @@ -0,0 +1,145 @@ +# Feature: Giveaway System + +**ID:** giveaway +**Role:** Admin (create), User (join) +**Status:** Active + +--- + +## Purpose + +A full giveaway lifecycle: admins create giveaways via a 9-step wizard, active giveaways are announced to the configured group, users join from the group or DM, a countdown timer fires and picks weighted random winners, then results are announced. + +--- + +## Entry Points + +| Trigger | Callback / Command | Role | +|---------|-------------------|------| +| `/start_giveaway` | Admin DM | Admin | +| `/giveaway` | Admin: view active | Admin | +| Button: Start Giveaway | `pamenu_start_giveaway` | Admin | +| Button: Join (in group) | `gwiz_start_here` | Admin | +| `/join` | User join from DM | User | +| Button: Join | `gw_join_{id}` | User | + +--- + +## Giveaway Wizard Flow (Admin, 9 Steps) + +``` +pamenu_start_giveaway + โ””โ”€โ”€ Step 1: Title (optional) + โ”œโ”€โ”€ gwiz_title_skip โ†’ go to Step 2 + โ””โ”€โ”€ Text input (gwiz_await_title) โ†’ Step 2 + โ””โ”€โ”€ Step 2: SC Per Winner + โ””โ”€โ”€ gwizScKeyboard: 5|10|25|50|Custom (gwiz_sc_*) + โ””โ”€โ”€ Custom โ†’ gwiz_await_custom_sc text input + โ””โ”€โ”€ Step 3: Number of Winners + โ””โ”€โ”€ gwizWinnersKeyboard: 1|2|3|5|Custom (gwiz_winners_*) + โ””โ”€โ”€ Custom โ†’ gwiz_await_custom_winners text input + โ””โ”€โ”€ Step 4: Duration + โ””โ”€โ”€ gwizDurationKeyboard: 5|15|30|60|120|240 min|Custom + โ””โ”€โ”€ Custom โ†’ gwiz_await_custom_duration text input + โ””โ”€โ”€ Step 5: Min Participants + โ””โ”€โ”€ gwizMinPartsKeyboard: 0|5|10|20|Custom + โ””โ”€โ”€ Custom โ†’ gwiz_await_custom_minparts text input + โ””โ”€โ”€ Step 6: Join Surface + โ””โ”€โ”€ gwizSurfaceKeyboard: [Group] [DM] [โœ… Done] + โ””โ”€โ”€ Step 7: Join Info (referral info) + โ””โ”€โ”€ gwiz_joininfo_done โ†’ Step 8 + โ””โ”€โ”€ Step 8โ€“9: Confirm & Create + โ””โ”€โ”€ gw_create_yes โ†’ create + announce + โ””โ”€โ”€ gw_create_no / gwiz_cancel โ†’ abort +``` + +### Pending Action Types (Wizard) + +| Type | Input | Next Step | +|------|-------|-----------| +| `gwiz_await_title` | Giveaway title text | Step 2 | +| `gwiz_step_sc` | (button selection) | Step 3 | +| `gwiz_await_custom_sc` | Number (SC amount) | Step 3 | +| `gwiz_step_winners` | (button selection) | Step 4 | +| `gwiz_await_custom_winners` | Number (winner count) | Step 4 | +| `gwiz_step_duration` | (button selection) | Step 5 | +| `gwiz_await_custom_duration` | Number (minutes) | Step 5 | +| `gwiz_step_minparts` | (button selection) | Step 6 | +| `gwiz_await_custom_minparts` | Number (min entries) | Step 6 | +| `gwiz_step_surface` | (button toggle) | Step 7 | +| `gwiz_step_joininfo` | (button confirm) | Step 8 | +| `gw_confirm` | (button yes/no) | Create or abort | + +--- + +## Join Flow (User) + +1. Giveaway announced in group with **Join** button โ†’ `gw_join_{id}` +2. User clicks Join โ†’ eligibility checked โ†’ joined or error. +3. `/join` command also works from DM. +4. Participants tracked in `gw.participants[]`. + +--- + +## Admin Controls (Active Giveaway) + +| Button | Callback | Effect | +|--------|----------|--------| +| ๐Ÿ End Now | `pamenu_gw_end_{id}` | Finalize early, pick winners | +| โฑ +15 min | `pamenu_gw_extend_{id}` | Extend countdown | +| โŒ Cancel | `pamenu_gw_cancel_{id}` | Delete giveaway, no winners | +| ๐Ÿ‘ฅ Participants | `pamenu_gw_participants_{id}` | List all entries | + +--- + +## Finalization Flow + +1. Timer fires (`finalizeGiveaway(gw)`) at `gw.endTime`. +2. Checks min participants โ€” if not met, cancels with notice. +3. `pickWeightedWinners(gw)` โ€” weighted pool (referral-boosted users have higher weight). +4. Winners announced in group. +5. SC credited to each winner. +6. Giveaway moved to history. + +--- + +## Keyboards + +| Function | Line | Purpose | +|----------|------|---------| +| `gwizScKeyboard()` | ~6604 | SC amount selection | +| `gwizWinnersKeyboard()` | ~6618 | Winner count selection | +| `gwizDurationKeyboard()` | ~6632 | Duration selection | +| `gwizMinPartsKeyboard()` | ~6650 | Min participants selection | +| `gwizSurfaceKeyboard(data)` | ~6667 | Join surface toggles | +| `gwizJoinInfoKeyboard()` | ~6678 | Confirm join info | +| `activeGiveawaysKeyboard(gws, page)` | ~4271 | Admin paginated giveaway list | + +--- + +## Dependencies + +- `finalizeGiveaway(gw)` โ€” winner selection and announcement +- `pickWeightedWinners(gw)` โ€” weighted random selection +- `resetGiveawayTimer(gw)` โ€” reschedule timer after extend +- `computeParticipantWeight(user)` โ€” referral-boost weight +- `getRealGiveaways()` โ€” filters out non-active entries +- `buildGiveawayAnnouncementText(gw)` ~index.js:13795 +- `buildGiveawayAnnouncementKeyboard(gw)` ~index.js:12561 + +--- + +## Edge Cases + +- Min participants not met โ†’ giveaway cancelled automatically. +- Extend: max extensions not enforced (can extend indefinitely). +- `gw_create_yes` also announces to configured group/channel. +- Admin can view participants mid-giveaway via `pamenu_gw_participants_{id}`. + +--- + +## File References + +- `index.js`: `/start_giveaway` ~6823, wizard handlers ~11849โ€“12120 +- `index.js`: `finalizeGiveaway` ~1239/12474, `pickWeightedWinners` ~1260+ +- `index.js`: `activeGiveawaysKeyboard` ~4271, giveaway keyboards ~6604โ€“6678 diff --git a/docs/features/05-bonus-30sc.md b/docs/features/05-bonus-30sc.md new file mode 100644 index 0000000..135c254 --- /dev/null +++ b/docs/features/05-bonus-30sc.md @@ -0,0 +1,116 @@ +# Feature: 30 SC Wager Bonus System + +**ID:** bonus_30sc +**Role:** User (request), Admin (approve/deny) +**Status:** Active + +--- + +## Purpose + +A weekly "30 SC" bonus for users who wager a minimum amount. Users declare their 7-day wager total; admins review the evidence; admins approve or deny with the bonus credited or explanation given. + +--- + +## Entry Points + +| Trigger | Callback / Command | Role | +|---------|-------------------|------| +| `/bonus` | Show bonus options | User | +| Button: Claim Bonus | `pmenu_claim_bonus` | User | +| Button: Request Bonus | `w30_menu_request` | User | +| `/wager30_admin` | Admin bonus manager | Admin | +| `/bonusstatus` | Admin user lookup | Admin | + +--- + +## User Flow + +``` +pmenu_claim_bonus โ†’ bonus options menu + โ””โ”€โ”€ w30_request_start โ†’ eligibility check + request menu + โ”œโ”€โ”€ w30_menu_how โ†’ explain requirements + โ”œโ”€โ”€ w30_menu_eligibility โ†’ show eligibility rules + โ”œโ”€โ”€ w30_bonus_info โ†’ show bonus info card + โ”œโ”€โ”€ w30_rules โ†’ show full rules + โ””โ”€โ”€ w30_menu_request โ†’ check eligible โ†’ submit request + โ””โ”€โ”€ If eligible โ†’ submitBonusRequest() + โ””โ”€โ”€ Sets pendingAction: w30_await_wager_total + โ””โ”€โ”€ User enters wager amount (text) + โ””โ”€โ”€ Request stored โ†’ admin notified +``` + +--- + +## Admin Flow + +``` +/wager30_admin โ†’ admin bonus manager + โ”œโ”€โ”€ w30_admin_pick_approve_{userId} โ†’ approve request โ†’ SC credited + โ”œโ”€โ”€ w30_admin_pick_sent_{userId} โ†’ mark bonus as sent (manual) + โ”œโ”€โ”€ w30_admin_pick_deny_{userId} โ†’ set pendingAction: w30_admin_deny_reason + โ”‚ โ””โ”€โ”€ Admin types denial reason โ†’ user notified + โ”œโ”€โ”€ w30_admin_pick_add_{userId} โ†’ manually add bonus + โ”œโ”€โ”€ w30_admin_lookup โ†’ set pendingAction: w30_admin_lookup + โ”‚ โ””โ”€โ”€ Admin types userId โ†’ show status + โ””โ”€โ”€ w30_admin_reset_{userId} โ†’ reset bonus state for user +``` + +--- + +## Pending Action Types + +| Type | Trigger | Handler Description | +|------|---------|---------------------| +| `w30_await_wager_total` | `w30_menu_request` approved | User declares 7d wager amount | +| `w30_admin_pick_approve` | Admin selects approve | Confirm and credit SC | +| `w30_admin_pick_sent` | Admin selects sent | Mark as manually sent | +| `w30_admin_pick_deny` | Admin selects deny | Collect denial reason text | +| `w30_admin_pick_add` | Admin selects add | Add bonus manually | +| `w30_admin_lookup` | Admin selects lookup | Admin enters userId to check | +| `w30_admin_reset` | Admin selects reset | Reset user's bonus record | +| `w30_admin_link_username` | Admin selects link | Manually link Runewager username | + +--- + +## Buttons & Callbacks + +| Button | Callback | Role | +|--------|----------|------| +| ๐Ÿ’ฐ How Does It Work | `w30_menu_how` | User | +| โœ… Am I Eligible? | `w30_menu_eligibility` | User | +| ๐ŸŽ Bonus Info | `w30_bonus_info` | User | +| ๐Ÿ“‹ Rules | `w30_rules` | User | +| ๐Ÿ“จ Request Bonus | `w30_menu_request` | User | +| ๐Ÿ“Š My Status | `w30_my_status` | User | +| โœ… Approve | `w30_admin_pick_approve_{id}` | Admin | +| โœ… Mark Sent | `w30_admin_pick_sent_{id}` | Admin | +| โŒ Deny | `w30_admin_pick_deny_{id}` | Admin | +| โž• Add Manually | `w30_admin_pick_add_{id}` | Admin | +| ๐Ÿ” Lookup User | `w30_admin_lookup` | Admin | +| ๐Ÿ”„ Reset | `w30_admin_reset_{id}` | Admin | + +--- + +## Dependencies + +- `submitBonusRequest(user)` โ€” creates pending request, notifies admins +- `isNewUserPromoEligible(user)` โ€” eligibility gating function +- Admin approval: credits SC to `user.scBalance` +- `persistRuntimeState()` โ€” saves state after approval + +--- + +## Edge Cases + +- User can only submit one active request at a time. +- Admin denial requires a reason โ€” user receives the reason via DM. +- Weekly cooldown: users cannot re-request until next week. +- `/bonusstatus ` lets admins check any user's bonus state. + +--- + +## File References + +- `index.js`: `/bonus` ~7089, `w30_*` callbacks ~7728โ€“7784, ~8656โ€“8661 +- `index.js`: `submitBonusRequest` ~1600+, admin flow ~6839 diff --git a/docs/features/06-promos.md b/docs/features/06-promos.md new file mode 100644 index 0000000..e8b7a57 --- /dev/null +++ b/docs/features/06-promos.md @@ -0,0 +1,125 @@ +# Feature: Promo Manager & Content Drops + +**ID:** promos +**Role:** User (claim), Admin (create/manage) +**Status:** Active + +--- + +## Purpose + +Admins create promotional bonus offers (Content Drops). Users browse active promos, view eligibility, and claim them. Claims can be auto-approved or require admin manual approval. Admins manage the full promo lifecycle: create, edit, pause, delete, preview. + +--- + +## Entry Points + +| Trigger | Callback / Command | Role | +|---------|-------------------|------| +| `/promo` | Show promo menu | User | +| Button: Claim Bonus | `pmenu_claim_bonus` | User | +| Button: View Promo | `promo_open_{id}` | User | +| Admin: Promo Manager | `admin_promo_manager` | Admin | + +--- + +## User Claim Flow + +``` +pmenu_claim_bonus โ†’ filter eligible promos + โ””โ”€โ”€ promo_open_{id} โ†’ promo detail card + โ”œโ”€โ”€ Show eligibility status + โ”œโ”€โ”€ promo_claim_{id} โ†’ auto-approve or pending review + โ”‚ โ”œโ”€โ”€ Auto-approved โ†’ SC credited + โ”‚ โ”‚ โ””โ”€โ”€ promo_user_claimed_successfully โ†’ confirm + โ”‚ โ””โ”€โ”€ Pending โ†’ admin notified + โ”‚ โ””โ”€โ”€ promo_confirm_claimed_next โ†’ mark as claimed + โ””โ”€โ”€ Back โ†’ promo list +``` + +--- + +## Admin Promo Creation Flow (Multi-Step) + +``` +admin_promo_manager โ†’ promo list + controls + โ””โ”€โ”€ admin_pm_create โ†’ start creation wizard + Step 1: Name (admin_pm_create_name) โ†’ text input + Step 2: Code (admin_pm_create_code) โ†’ text input + Step 3: SC Amount โ†’ text input + Step 4: Claim Limit โ†’ text input (0 = unlimited) + Step 5: Expiry โ†’ text input (date or "none") + Step 6: Eligibility rules โ†’ selection + Step 7: Auto-approve toggle โ†’ yes/no + Step 8: Confirm & Create +``` + +### Pending Action Types (Admin Creation) + +| Type | Description | +|------|-------------| +| `admin_pm_create_name` | Enter promo name | +| `admin_pm_create_code` | Enter promo code | +| `admin_pm_create_amount` | Enter SC amount | +| `admin_pm_create_limit` | Enter claim limit | +| `admin_pm_create_expiry` | Enter expiry date | +| `admin_pm_create_auto_approve` | Toggle auto-approve | +| `admin_pm_edit_select_id` | Select promo to edit | +| `admin_pm_edit_field` | Enter new field value | +| `admin_promo_code_add_input` | Legacy: add promo code | +| `admin_promo_code_toggle_input` | Legacy: toggle promo code | +| `admin_edit_code_input` | Legacy: edit code | +| `admin_edit_amount_input` | Legacy: edit amount | +| `admin_edit_limit_input` | Legacy: edit limit | + +--- + +## Admin Promo Management Callbacks + +| Callback | Purpose | +|----------|---------| +| `admin_promo_manager` | Open promo manager | +| `admin_pm_create` | Start creation wizard | +| `admin_pm_edit` | Select promo to edit | +| `admin_pm_pause_toggle` | Pause/resume promo | +| `admin_pm_delete` | Mark promo as deleted | +| `admin_pm_preview` | Preview promo card | + +--- + +## User Promo Callbacks + +| Callback | Purpose | +|----------|---------| +| `menu_claim_bonus` | Show eligible promos list | +| `promo_open_{id}` | Open promo detail + claim button | +| `promo_claim_{id}` | Submit claim | +| `promo_confirm_claimed_next` | User confirms claim submission | +| `promo_user_claimed_successfully` | Confirm self-claim success | + +--- + +## Dependencies + +- `promoStore` โ€” in-memory store: `{ promos[], logs[], nextPromoId }` +- `isNewUserPromoEligible(user)` โ€” centralized eligibility check +- `adminLog()` โ€” writes to `promoStore.logs` (capped at 200) and `data/admin-events.log` +- `persistRuntimeState()` โ€” saves state + +--- + +## Edge Cases + +- Paused promos: hidden from user list, still editable by admin. +- Deleted promos: removed from user list, kept in logs. +- Claim limit: once `claimsCount >= claimLimit`, promo is auto-closed. +- Expiry: if past expiry date, promo is hidden from users. +- Auto-approve off: admin receives DM notification for each claim. + +--- + +## File References + +- `index.js`: `/promo` ~7330, `promo_*` callbacks ~8435โ€“8525 +- `index.js`: `admin_promo_manager` ~9490, `admin_pm_*` ~9529โ€“9580 +- `index.js`: `promoStore` initialization ~500+ diff --git a/docs/features/07-tooltips.md b/docs/features/07-tooltips.md new file mode 100644 index 0000000..e1332cd --- /dev/null +++ b/docs/features/07-tooltips.md @@ -0,0 +1,202 @@ +# Feature: Helpful Tooltips System + +**ID:** tooltips +**Role:** Admin (manage), User (receive) +**Status:** Active + +--- + +## Purpose + +A configurable system that posts periodic helpful tips to a target group or channel. Admins manage tips via a dashboard: add, edit, toggle on/off, remove, import in bulk (JSON), test-send, and configure interval/target. Users receive tips automatically per the configured interval. + +--- + +## Entry Points + +| Trigger | Callback / Command | Role | +|---------|-------------------|------| +| `/tips` / `/t` / `/tp` | Open tips dashboard | Admin | +| `/tiplist` | List all tips | Admin | +| `/tipadd ` | Add tip via command | Admin | +| `/tipremove ` | Remove tip via command | Admin | +| `/tipedit ` | Edit tip via command | Admin | +| `/tiptoggle ` | Toggle tip via command | Admin | +| `/tiptest` | Test-send random tip | Admin | +| `/tipsettings` | Open settings | Admin | +| Button: Tooltips Manager | `admin_cmd_tips_dashboard` | Admin | + +--- + +## Dashboard Layout (`tipsDashboardKeyboard`) + +| Row | Buttons | +|-----|---------| +| 1 | โž• Add Tooltip ยท โœ๏ธ Edit Tooltip | +| 2 | โŒ Remove Tooltip ยท ๐Ÿ” Toggle System | +| 3 | ๐Ÿ“‹ Show all Helpful Tooltips (N) | +| 4 | ๐Ÿ‘ View Tooltip ยท ๐Ÿงช Test Random | +| 5 | โš™๏ธ Helpful Tooltips Settings | +| 6 | ๐Ÿ“ฅ Import Tooltips (JSON) | +| 7 | โ†ฉ Admin Menu | + +--- + +## Add Tooltip Flow + +``` +tips_cmd_add + โ””โ”€โ”€ pendingAction: await_tip_add_text + โ””โ”€โ”€ User sends tooltip text (HTML/plain text) + โ””โ”€โ”€ Text parsed (body + optional button rows) + โ””โ”€โ”€ Tip saved โ†’ tipsDashboard refreshed +``` + +### Button Syntax (in tip text) +- `[Label - https://url] && [Label2 - https://url2]` โ†’ same row +- New line โ†’ new button row +- `[Open Bot]` โ†’ standard Open Bot button + +--- + +## Edit Tooltip Flow + +``` +tips_cmd_edit โ†’ tipSelectKeyboard('tip_edit_select') + โ””โ”€โ”€ User selects tip โ†’ tip_edit_select_{id} + โ””โ”€โ”€ pendingAction: await_tip_edit_text (with tipId) + โ””โ”€โ”€ User sends new text + โ””โ”€โ”€ tip.text updated โ†’ dashboard refreshed +``` + +--- + +## Remove Tooltip Flow + +``` +tips_cmd_remove โ†’ tipSelectKeyboard('tip_remove') + โ””โ”€โ”€ User selects tip โ†’ tip_remove_{id} + โ””โ”€โ”€ Tip removed immediately โ†’ confirmation reply +``` + +--- + +## Batch Import Flow + +``` +tips_cmd_import_batch + โ””โ”€โ”€ pendingAction: await_tip_import_batch + โ””โ”€โ”€ User pastes JSON array: + [{"text":"Tip one","enabled":true}, ...] + โ””โ”€โ”€ Parsed โ†’ valid entries added โ†’ count reported +``` + +--- + +## Settings Flow (`tipsSettingsKeyboard`) + +| Button | Callback | Effect | +|--------|----------|--------| +| โฑ Change Interval | `tips_set_interval` | await_tip_settings_interval โ†’ set hours | +| ๐Ÿ”— Link Channel/Group | `tips_set_link_target` | await_tip_link_target โ†’ forward message to link | +| โ†ฉ Back to Tooltips | `tips_settings_back` | Return to dashboard | + +--- + +## Pending Action Types + +| Type | Trigger | Handler | +|------|---------|---------| +| `await_tip_add_text` | `tips_cmd_add` | Parse text+buttons, save new tip | +| `await_tip_edit_text` | `tip_edit_select_{id}` | Update tip.text | +| `await_tip_import_batch` | `tips_cmd_import_batch` | Parse JSON array, bulk add | +| `await_tip_settings_interval` | `tips_set_interval` | Set `tipsStore.intervalHours` | +| `await_tip_link_target` | `tips_set_link_target` | Set `tipsStore.targetGroup` from forwarded message | + +--- + +## View Tooltip Flow + +``` +tips_cmd_view โ†’ tipSelectKeyboard('tip_view') + โ””โ”€โ”€ tip_view_{id} โ†’ show rendered HTML preview + โ”œโ”€โ”€ โ—€ Prev / Next โ–ถ โ€” navigate between tips + โ”œโ”€โ”€ โœ๏ธ Edit โ†’ tip_edit_select_{id} + โ”œโ”€โ”€ ๐Ÿ” Toggle โ†’ tip_toggle_{id} + โ”œโ”€โ”€ โŒ Delete โ†’ tip_remove_{id} + โ”œโ”€โ”€ โ†ฉ Back to List โ†’ tips_cmd_view + โ””โ”€โ”€ ๐Ÿ  Admin Menu โ†’ pamenu_back_admin +``` + +## Per-Tip Dynamic Callbacks + +| Pattern | Handler | Description | +|---------|---------|-------------| +| `tip_view_{id}` | index.js (after tips_cmd_remove) | Preview tip with nav/edit/delete | +| `tip_remove_{id}` | index.js ~11835 | Delete tip immediately | +| `tip_edit_select_{id}` | index.js ~11846 | Start edit flow | +| `tip_toggle_{id}` | index.js ~11869 | Toggle enabled/disabled | + +--- + +## Background Timer + +- `tipsTimer` โ€” fires every `tipsStore.intervalHours` hours. +- Picks next tip (skips `lastSentTipId` for variety). +- Posts to `tipsStore.targetGroup` (or `targetChannel`). +- Silent mode: no user ping (disable_notification=true). + +--- + +## Runtime Store (`tipsStore`) + +```javascript +{ + tips: [], // Array of { id, text, enabled } + systemEnabled: bool, + intervalHours: number, + targetGroup: string|null, // Telegram chat_id + targetGroupTitle: string|null, + silentMode: bool, + lastSentTipId: number|null, + nextTipId: number +} +``` + +--- + +## Keyboards + +| Function | Line | Purpose | +|----------|------|---------| +| `tipsDashboardKeyboard()` | ~11463 | Main dashboard 7-row keyboard | +| `tipSelectKeyboard(action)` | ~11477 | Per-tip selector (5 per row + Cancel) | +| `tipsSettingsKeyboard()` | ~11743 | Settings: interval, link target, back | + +--- + +## Dependencies + +- `postTipToConfiguredTarget(tip, telegram)` โ€” sends tip to group/channel +- `parseTipText(text)` โ€” parses text + inline button rows +- `persistRuntimeState()` / `saveHelpfulMessages()` โ€” persists tips +- `data/tooltips.json` โ€” loaded at startup, regenerated by `generate_tooltips.sh` +- `generate_tooltips.sh` โ€” extracts `DEFAULT_TIPS_LIST` from index.js on deploy + +--- + +## Edge Cases + +- If `targetGroup` not set: test fails with "Use Settings โ†’ Link Channel/Group". +- System disabled: timer still runs but posts are skipped. +- Batch import: entries without `text` field are silently skipped. +- Link target: bot must already be a member of the target chat. + +--- + +## File References + +- `index.js`: `/tips` ~11528, dashboard ~11463โ€“11498, `tips_cmd_*` ~11651โ€“11740 +- `index.js`: `tip_remove/edit/toggle` ~11799โ€“11843 +- `index.js`: `tipsSettingsKeyboard` ~11743, settings actions ~11750โ€“11788 +- `generate_tooltips.sh`, `add_tooltip.sh` (shell tooltip utilities) diff --git a/docs/features/08-referral.md b/docs/features/08-referral.md new file mode 100644 index 0000000..759837c --- /dev/null +++ b/docs/features/08-referral.md @@ -0,0 +1,95 @@ +# Feature: Referral System + +**ID:** referral +**Role:** User (share), Admin (manage) +**Status:** Active + +--- + +## Purpose + +Users earn a 7-day referral boost for each friend they onboard with their referral code. Boosts increase weighted giveaway participation chance. Referral codes are one-time use during the friend's onboarding. A leaderboard tracks top referrers. + +--- + +## Entry Points + +| Trigger | Callback / Command | Role | +|---------|-------------------|------| +| `/referral` | Show referral info | User | +| Button: Referrals | `pmenu_referral` | User | +| `/boost_referrals` | Admin: assign boost | Admin | +| `/leaderboard` | Top referrers | Any | + +--- + +## User Referral Flow + +``` +pmenu_referral โ†’ referral menu + โ”œโ”€โ”€ ref_menu_code โ†’ show referral code (copyable) + โ”œโ”€โ”€ ref_menu_share โ†’ share link (Telegram share button) + โ””โ”€โ”€ ref_menu_how โ†’ explain boost mechanism + +Friend uses code during /start onboarding: + onboard_ref_yes โ†’ await_referral_code โ†’ code validated + โ””โ”€โ”€ applyOnboardingReferralCode(user, code) + โ”œโ”€โ”€ Referrer gets 7-day boost + โ”œโ”€โ”€ New user gets 7-day boost + โ””โ”€โ”€ Both parties notified via DM +``` + +--- + +## Boost Mechanics + +- **Boost duration:** 7 days from referral +- **Giveaway weight:** boosted users have `computeParticipantWeight(user)` multiplier +- **Boost expiry:** `expireReferralBoosts()` runs every hour +- **Weekly reset:** `referralStore.weekly` cleared every 7 days (setInterval) + +--- + +## Callbacks + +| Callback | Handler Line | Purpose | +|----------|-------------|---------| +| `pmenu_referral` | ~7514 | Referral boost meter + share link | +| `ref_leaderboard` | ~7240 | Top 10 referrers | +| `menu_referral` | ~8573 | Referral menu (code, share, how) | +| `ref_menu_code` | ~8588 | Show user's referral code | +| `ref_menu_share` | ~8600 | Open Telegram share sheet | +| `ref_menu_how` | ~8595 | Explain boost mechanics | + +--- + +## Pending Action Types + +| Type | Trigger | Handler | +|------|---------|---------| +| `await_referral_code` | `onboard_ref_yes` | Validates code, applies boosts | + +--- + +## Admin Tools + +- `/boost_referrals ` โ€” manually assign boost to a user +- `/leaderboard_weekly` โ€” active users in last 7 days + +--- + +## Rules + +1. Referral code entry is **onboarding-only** โ€” no post-onboarding application. +2. **No self-referral** โ€” code owner cannot use their own code. +3. Each user gets one referral code, generated on first `/start`. +4. Dual boost: both referrer and referee receive 7-day boost. + +--- + +## File References + +- `index.js`: `/referral` ~7223, `pmenu_referral` ~7514 +- `index.js`: `applyOnboardingReferralCode` ~1400+ +- `index.js`: `computeParticipantWeight` ~1350+, `expireReferralBoosts` ~14891 +- `index.js`: `referralStore` initialization ~500+ diff --git a/docs/features/09-sshv.md b/docs/features/09-sshv.md new file mode 100644 index 0000000..ec823f9 --- /dev/null +++ b/docs/features/09-sshv.md @@ -0,0 +1,129 @@ +# Feature: SSHV (Admin VPS Console) + +**ID:** sshv +**Role:** Admin +**Status:** Active + +--- + +## Purpose + +An in-bot terminal that lets admins run shell commands on the VPS from Telegram. Commands are validated, confirmed, and executed via `child_process.execFile`. Sessions expire after inactivity. Supports Ctrl+C/Z signals, a simple file editor, and session locking. + +--- + +## Entry Points + +| Trigger | Callback / Command | Role | +|---------|-------------------|------| +| `/sshv` | Open SSHV console | Admin | +| Button: Open Console | `sshv_open` | Admin | + +--- + +## Console Flow + +``` +/sshv โ†’ renderSshvConsole(ctx) โ†’ show console UI + sshvKeyboard + โ””โ”€โ”€ sshv_run_prompt โ†’ pendingAction: await_sshv_command + โ””โ”€โ”€ User types command + โ””โ”€โ”€ Validation (no null bytes, backticks, $(, ${) + โ””โ”€โ”€ sshv_confirm_run โ†’ show command preview + โ”œโ”€โ”€ Confirm โ†’ execFile(command) โ†’ output shown + โ””โ”€โ”€ sshv_cancel_run โ†’ abort command +``` + +--- + +## File Editor Flow + +``` +Console โ†’ (editor mode) + โ””โ”€โ”€ pendingAction: await_sshv_editor_content + โ””โ”€โ”€ User types file content + โ””โ”€โ”€ sshv_editor_save โ†’ save to file + โ””โ”€โ”€ sshv_editor_cancel โ†’ discard +``` + +--- + +## Console Buttons (`sshvKeyboard`) + +| Button | Callback | Effect | +|--------|----------|--------| +| โ–ถ Run Command | `sshv_run_prompt` | Prompt for command text | +| ๐Ÿ”„ Refresh | `sshv_refresh` | Refresh console output | +| โŒƒC | `sshv_ctrl_c` | Send SIGINT to session process | +| โŒƒZ | `sshv_ctrl_z` | Send SIGTSTP to session process | +| ๐Ÿ”’ Lock | `sshv_lock` | Lock session (blocks new commands) | +| ๐Ÿ”“ Unlock | `sshv_unlock` | Unlock session | +| โœ… Confirm | `sshv_confirm_run` | Execute pending command | +| โŒ Cancel | `sshv_cancel_run` | Abort pending command | +| ๐Ÿ’พ Save | `sshv_editor_save` | Save editor draft | +| โŒ Cancel Edit | `sshv_editor_cancel` | Discard editor draft | +| ๐Ÿšช Exit | `sshv_exit` | Close SSHV session | + +--- + +## Pending Action Types + +| Type | Trigger | Handler | +|------|---------|---------| +| `await_sshv_command` | `sshv_run_prompt` | Store command โ†’ show confirm dialog | +| `await_sshv_editor_content` | Editor mode | Store content โ†’ save on confirm | + +--- + +## Security + +- **Command validation:** rejects null bytes (`\0`), backticks (`` ` ``), `$(`, `${` +- **Execution:** uses `execFile()` (not `exec()`) โ€” no shell expansion +- **Session GC:** `sshvGcTimer` (every 1 min) expires idle sessions +- **Lock mode:** `session.locked = true` blocks new command submissions +- **Admin only:** all handlers check `requireAdmin(ctx)` + +--- + +## Session State + +```javascript +sshvSessions: Map +``` + +--- + +## Keyboards + +| Function | Line | Purpose | +|----------|------|---------| +| `sshvKeyboard(session)` | ~2191 | Dynamic console keyboard (lock/unlock state) | + +--- + +## Dependencies + +- `renderSshvConsole(ctx, user)` โ€” builds and sends console UI +- `child_process.execFile` โ€” safe command execution +- `sshvGcTimer` โ€” garbage collect expired sessions + +--- + +## Edge Cases + +- Session expires after 10 minutes of inactivity. +- Locked sessions cannot run commands until unlocked. +- SIGINT/SIGTSTP only affect the tracked session process PID. +- Output is truncated if too long for a Telegram message. + +--- + +## File References + +- `index.js`: `/sshv` ~6367, `sshv_*` callbacks ~9071โ€“9245 +- `index.js`: `renderSshvConsole` ~2236, `sshvKeyboard` ~2191 +- `index.js`: `sshvGcTimer` setInterval ~14833 diff --git a/docs/features/10-deploy-ops.md b/docs/features/10-deploy-ops.md new file mode 100644 index 0000000..34ddaac --- /dev/null +++ b/docs/features/10-deploy-ops.md @@ -0,0 +1,135 @@ +# Feature: Deploy & Admin Operations + +**ID:** deploy_ops +**Role:** Admin +**Status:** Active + +--- + +## Purpose + +Admin commands and callbacks for deployment, health monitoring, log viewing, version checking, state backup, and bot diagnostics. Deployment is triggered from Telegram or GitHub Actions. + +--- + +## Entry Points + +| Trigger | Callback / Command | Role | +|---------|-------------------|------| +| `/deploy` | Trigger deployment | Admin | +| `/deploy_status` | Show deploy status | Admin | +| `/logs [N]` | Show last N log lines | Admin | +| `/version` | Show bot version | Admin | +| `/health` | Show health metrics | Admin | +| `/admin_notify ` | Send admin notification | Admin | +| `/admin_backup` | Trigger state backup | Admin | +| `/testall` | Run full diagnostic suite | Admin | + +--- + +## Deploy Flow + +``` +/deploy [source] + โ””โ”€โ”€ deploy.sh called (source: github|bot|vps) + 1. Stop systemd service + 2. git fetch + reset --hard origin/main + 3. npm ci --omit=dev + 4. generate_tooltips.sh + 5. Kill port blockers (SIGTERM โ†’ SIGKILL) + 6. systemctl start runewager + 7. Health check + โ””โ”€โ”€ Telegram notify at each step +``` + +### Deploy Safety +- Pre-deploy checks: syntax, tests, npm audit, critical files +- `[skip deploy]` in commit message bypasses GitHub Actions deploy +- Only admin `ADMIN_IDS` can trigger via Telegram + +--- + +## Callbacks + +| Callback | Handler Line | Purpose | +|----------|-------------|---------| +| `admin_cmd_testall` | ~8940 | Run full `/testall` diagnostic | +| `admin_cmd_health` | ~8946 | Fetch HTTP `/health` endpoint | +| `admin_cmd_version` | ~8977 | Show version string | +| `admin_cmd_verify_setup` | ~8999 | Verify bot configuration | +| `admin_backup_action` | ~9005 | Trigger `backup-runtime-state.sh` | +| `admin_cmd_mode_toggle` | ~9017 | Toggle admin mode visibility | +| `admin_cmd_mode_on` | ~9028 | Enable admin mode | +| `admin_cmd_mode_off` | ~9038 | Disable admin mode | + +--- + +## `/testall` Diagnostic Suite + +Runs a structured series of checks and outputs: +``` +TestAll complete โ€” X passed, Y warnings, Z failures. +``` + +Checks include: +- Bot token validity +- Admin ID configuration +- Group/channel link status +- Health endpoint reachability +- Giveaway state consistency +- Promo store integrity +- Tooltip store integrity +- Environment variable completeness + +--- + +## Health Endpoint (`/health` HTTP) + +Returns JSON: +```json +{ + "status": "ok", + "uptime": 12345, + "memoryMB": 87.2, + "diskFreeMB": 4200, + "errorRate": 0, + "activeUsers24h": 15, + "persistAge": 8 +} +``` + +--- + +## Metrics Endpoint (`/metrics` HTTP โ€” Prometheus) + +Key metrics exported: +- `runewager_uptime_seconds` +- `runewager_menu_stale_recoveries` +- `runewager_pending_actions_timed_out` +- `runewager_errors_total` +- `runewager_active_users_24h` + +--- + +## Scripts + +| Script | Purpose | +|--------|---------| +| `deploy.sh` | Full VPS deployment | +| `prod-run.sh` | Idempotent service setup + start | +| `start.sh` | Simple foreground start | +| `dev-run.sh` | Local dev runner | +| `scripts/rollback.sh` | Git-based rollback to prior commit | +| `scripts/self-diagnose.sh` | VPS environment diagnostics | +| `scripts/pre-deploy-checks.sh` | Pre-deploy gate (syntax, tests, audit) | +| `scripts/backup-runtime-state.sh` | Backup `data/runtime-state.json` | +| `scripts/notify-telegram.sh` | Send Telegram message from shell | + +--- + +## File References + +- `index.js`: `/deploy` ~6860, `/deploy_status` ~6993, `/logs` ~7006 +- `index.js`: `/testall` ~13448, `admin_cmd_*` ~8940โ€“9045 +- `deploy.sh`, `prod-run.sh`, `scripts/rollback.sh` +- `.github/workflows/deploy.yml`, `ci.yml` diff --git a/docs/features/11-user-lookup.md b/docs/features/11-user-lookup.md new file mode 100644 index 0000000..ad3cf34 --- /dev/null +++ b/docs/features/11-user-lookup.md @@ -0,0 +1,100 @@ +# Feature: User Lookup & Management + +**ID:** user_lookup +**Role:** Admin +**Status:** Active + +--- + +## Purpose + +Admin tools to look up any user by Telegram ID or Runewager username, view their full state, check bonus status, refresh their schema, and temporarily grant admin access for debugging. + +--- + +## Entry Points + +| Trigger | Callback / Command | Role | +|---------|-------------------|------| +| `/whois ` | Lookup user | Admin | +| `/bonusstatus ` | Check bonus state | Admin | +| `/refreshuser ` | Refresh user schema | Admin | +| Button: Whois | `admin_cmd_whois_prompt` | Admin | +| Button: Bonus Status | `admin_cmd_bonusstatus_prompt` | Admin | +| Button: Refresh User | `admin_cmd_refreshuser_prompt` | Admin | + +--- + +## Lookup Flows + +### `/whois` +``` +/whois + โ””โ”€โ”€ Find user in userStore + โ””โ”€โ”€ Show: userId, username, SC balance, bonus state, + onboarding step, badges, pendingAction, last active +``` + +### `/bonusstatus` +``` +/bonusstatus + โ””โ”€โ”€ Show: bonus type, requested at, approved/denied, + SC amount, reason (if denied), weekly cooldown state +``` + +### `/refreshuser` +``` +/refreshuser + โ””โ”€โ”€ Merge missing schema fields from DEFAULT_USER + โ””โ”€โ”€ Confirm: "Schema refreshed for user X" +``` + +--- + +## Callbacks + +| Callback | Handler Line | Pending Type Triggered | +|----------|-------------|----------------------| +| `admin_cmd_whois_prompt` | ~9047 | `await_admin_whois` | +| `admin_cmd_bonusstatus_prompt` | ~9055 | `await_admin_bonusstatus` | +| `admin_cmd_refreshuser_prompt` | ~9063 | `await_admin_refreshuser` | +| `admin_auth_bypass` | ~9358 | Grant temp admin (debug) | +| `admin_auth_restore` | ~9368 | Remove temp admin | + +--- + +## Pending Action Types + +| Type | Trigger | Handler | +|------|---------|---------| +| `await_admin_whois` | `admin_cmd_whois_prompt` | Look up user by typed ID/username | +| `await_admin_bonusstatus` | `admin_cmd_bonusstatus_prompt` | Show bonus record | +| `await_admin_refreshuser` | `admin_cmd_refreshuser_prompt` | Refresh schema for typed userId | + +--- + +## User State Schema (Key Fields) + +```javascript +{ + id: number, // Telegram user ID + username: string, + runewagerUsername: string|null, + scBalance: number, + bonusState: object, + onboarding: { step, completedAt, completionCardShown }, + pendingAction: { type, data, createdAt } | null, + referralCode: string, + referralBoost: { active, expiresAt } | null, + playMode: 'browser'|'miniapp', + flags: { quickCommands, tooltips, ... } +} +``` + +--- + +## File References + +- `index.js`: `/whois` ~6900, `/bonusstatus` ~6926, `/refreshuser` ~6950 +- `index.js`: `admin_cmd_*` prompt callbacks ~9047โ€“9070 +- `index.js`: `admin_auth_bypass` ~9358, `admin_auth_restore` ~9368 diff --git a/docs/features/12-group-linking.md b/docs/features/12-group-linking.md new file mode 100644 index 0000000..c329e6a --- /dev/null +++ b/docs/features/12-group-linking.md @@ -0,0 +1,97 @@ +# Feature: Group & Channel Linking + +**ID:** group_linking +**Role:** Admin +**Status:** Active + +--- + +## Purpose + +Admins link Telegram groups and channels to the bot for giveaway announcements, tooltip delivery, and join verifications. Links are stored in `linkedGroups[]`. Admins can view, test permissions, and remove linked groups. + +--- + +## Entry Points + +| Trigger | Callback / Command | Role | +|---------|-------------------|------| +| Button: Group Linking (from Admin System Tools) | `admin_sys_group_linking` | Admin | +| Button: Group Linking (from Settings) | `settings_group_linking_tools` | Admin | + +--- + +## Group Linking Flow + +``` +settings_group_linking_tools โ†’ groupLinkingToolsKeyboard + โ”œโ”€โ”€ group_link_start โ†’ pendingAction: await_register_chat_forward + โ”‚ โ””โ”€โ”€ Admin forwards any message from target group/channel + โ”‚ โ””โ”€โ”€ Chat ID extracted โ†’ group added to linkedGroups[] + โ”œโ”€โ”€ group_link_view โ†’ list all linked groups with IDs + โ”œโ”€โ”€ group_link_remove_menu โ†’ show removal picker + โ”‚ โ””โ”€โ”€ group_link_remove_{chatId} โ†’ remove from linkedGroups[] + โ””โ”€โ”€ group_link_test_permissions โ†’ check bot perms in each group +``` + +--- + +## Buttons & Callbacks + +| Button | Callback | Handler Line | +|--------|----------|-------------| +| โž• Link Group/Channel | `group_link_start` | ~7652 | +| ๐Ÿ‘ View Linked | `group_link_view` | ~7660 | +| โŒ Remove Group | `group_link_remove_menu` | ~7666 | +| `group_link_remove_{id}` | Dynamic | ~7679 | +| ๐Ÿ” Test Permissions | `group_link_test_permissions` | ~7688 | +| โ†ฉ Back | (to admin settings) | via `settings_group_linking_tools` | + +--- + +## Pending Action Types + +| Type | Trigger | Handler | +|------|---------|---------| +| `await_register_chat_forward` | `group_link_start` | Extract chat ID from forwarded message | + +--- + +## Linked Groups Data + +```javascript +linkedGroups: [ + { chatId: -1001234567, title: "My Group", linkedAt: timestamp }, + ... +] +``` + +--- + +## Tips Target vs Linked Groups + +- `tipsStore.targetGroup` โ€” single target for tooltip delivery (set via `/tipsettings`) +- `linkedGroups[]` โ€” all known groups for join verification and announcements +- Giveaway announcements use the configured group from giveaway wizard (`gwiz_surface_*`) + +--- + +## Keyboards + +| Function | Line | Purpose | +|----------|------|---------| +| `groupLinkingToolsKeyboard(returnCallback)` | ~8873 | Link, view, remove, test buttons | + +--- + +## Dependencies + +- `renderGroupLinkingTools(ctx, user)` โ€” sends group linking UI +- `clearOldMenus(ctx, user)` โ€” should precede menu send (โš ๏ธ audit flag) + +--- + +## File References + +- `index.js`: `settings_group_linking_tools` ~7643, `group_link_*` ~7652โ€“7700 +- `index.js`: `groupLinkingToolsKeyboard` ~8873, `renderGroupLinkingTools` ~8882 diff --git a/docs/features/13-bug-reports.md b/docs/features/13-bug-reports.md new file mode 100644 index 0000000..4f7291d --- /dev/null +++ b/docs/features/13-bug-reports.md @@ -0,0 +1,88 @@ +# Feature: Bug Reports + +**ID:** bug_reports +**Role:** User (submit), Admin (view/resolve) +**Status:** Active + +--- + +## Purpose + +Users submit bug reports from the help center. Admins review, mark resolved, and export all reports. Reports are stored in memory and persisted to runtime state. + +--- + +## Entry Points + +| Trigger | Callback / Command | Role | +|---------|-------------------|------| +| `/bugreport` | Start bug report | User | +| Button: Bug Report | `help_open_bugreport` | User | +| Button: Bug Report | `menu_bugreport` | User | +| `/bugreports` | List open reports | Admin | +| Button: Bug Reports | `pamenu_bug_reports` | Admin | + +--- + +## User Report Flow + +``` +help_open_bugreport or menu_bugreport + โ””โ”€โ”€ pendingAction: await_bugreport + โ””โ”€โ”€ User types bug description (text input) + โ””โ”€โ”€ Report saved to bugReports[] with timestamp + userId + โ””โ”€โ”€ Admin notified via DM + โ””โ”€โ”€ User receives: "Thank you for your report!" +``` + +--- + +## Admin Report Flow + +``` +pamenu_bug_reports โ†’ list open reports + โ””โ”€โ”€ admin_cmd_viewbugs โ†’ recent reports with userId + text + โ””โ”€โ”€ admin_cmd_resolvebug_prompt โ†’ pendingAction: await_admin_resolvebug + โ””โ”€โ”€ Admin types reportId โ†’ mark as resolved + โ””โ”€โ”€ admin_cmd_exportbugs โ†’ export all reports as text +``` + +--- + +## Callbacks + +| Callback | Handler Line | Role | +|----------|-------------|------| +| `help_open_bugreport` | ~7577 | Set `await_bugreport` | User | +| `menu_bugreport` | ~8609 | Set `await_bugreport` | User | +| `pamenu_bug_reports` | ~7954 | View open reports | Admin | +| `admin_cmd_viewbugs` | ~9251 | List recent reports | Admin | +| `admin_cmd_resolvebug_prompt` | ~9257 | Set `await_admin_resolvebug` | Admin | +| `admin_cmd_exportbugs` | ~9265 | Export all reports | Admin | + +--- + +## Pending Action Types + +| Type | Trigger | Handler | +|------|---------|---------| +| `await_bugreport` | `help_open_bugreport` / `menu_bugreport` | Save report, notify admin | +| `await_admin_resolvebug` | `admin_cmd_resolvebug_prompt` | Mark report resolved | + +--- + +## Bug Report Data + +```javascript +bugReports: [ + { id, userId, text, createdAt, resolved: bool } +] +``` + +--- + +## File References + +- `index.js`: `/bugreport` ~7267, `/bugreports` ~7274 +- `index.js`: `help_open_bugreport` ~7577, `menu_bugreport` ~8609 +- `index.js`: `pamenu_bug_reports` ~7954, `admin_cmd_viewbugs` ~9251 diff --git a/docs/features/14-announcements.md b/docs/features/14-announcements.md new file mode 100644 index 0000000..39b0a1b --- /dev/null +++ b/docs/features/14-announcements.md @@ -0,0 +1,81 @@ +# Feature: Announcements & Broadcast + +**ID:** announcements +**Role:** Admin +**Status:** Active + +--- + +## Purpose + +Admins compose and send mass announcements to all bot users. The broadcast builder supports text, media, inline buttons, and a preview step. A retry mechanism handles failed deliveries. + +--- + +## Entry Points + +| Trigger | Callback / Command | Role | +|---------|-------------------|------| +| `/A` / `/a` / `/announce` | Start announcement | Admin | +| Button: Announce | `admin_cmd_announce_start` | Admin | + +--- + +## Broadcast Build Flow + +``` +admin_cmd_announce_start โ†’ announcement builder UI + โ”œโ”€โ”€ Compose text/media + โ”œโ”€โ”€ Add inline buttons (optional) + โ”œโ”€โ”€ ๐Ÿ‘ Preview โ†’ send preview to admin DM + โ””โ”€โ”€ โœ… Send โ†’ broadcast to all users + โ””โ”€โ”€ One message per user (with setImmediate yield between) + โ””โ”€โ”€ Failed users tracked in broadcastFailedUsers[] + โ””โ”€โ”€ /broadcast_retry โ†’ retry failed + โ””โ”€โ”€ /broadcast_failed โ†’ view failed list +``` + +--- + +## Callbacks + +| Callback | Handler Line | Purpose | +|----------|-------------|---------| +| `admin_cmd_announce_start` | ~7798 | Open announcements UI | +| `admin_broadcast` | ~7824 | Legacy broadcast (disabled) | +| `admin_cancel` | ~7832 | Cancel pending action, return to admin menu | + +--- + +## Pending Action Types + +| Type | Trigger | Handler | +|------|---------|---------| +| (Announcement builder uses in-memory config) | โ€” | โ€” | + +--- + +## Failed Broadcast Handling + +- `broadcastFailedUsers[]` โ€” **no cap** โ€” every failure is recorded (unbounded in memory; also written to `data/admin-events.log` via `adminLog('broadcast_failure', ...)`). +- `/broadcast_retry ` โ€” retries all entries in `broadcastFailedUsers[]` with the provided message text. +- `/broadcast_failed` โ€” shows failed users in chunks of 30 (up to 90 shown inline; full list in `data/admin-events.log`). Also shows a high-failure-rate โš ๏ธ warning if >20% of users failed. +- **High failure warning:** after weekly boost reminders, if >20% of attempted users failed, all `ADMIN_IDS` receive an alert DM with retry instructions. + +--- + +## Commands + +| Command | Line | Purpose | +|---------|------|---------| +| `/A` / `/a` / `/announce` | ~6593โ€“6595 | Start builder | +| `/broadcast_retry` | ~14298 | Retry failed deliveries | +| `/broadcast_failed` | ~14317 | List failed user IDs | + +--- + +## File References + +- `index.js`: `/announce` ~6593, `admin_cmd_announce_start` ~7798 +- `index.js`: `broadcastFailedUsers` persistence ~500+ +- `index.js`: `/broadcast_retry` ~14298, `/broadcast_failed` ~14317 diff --git a/docs/features/15-misc-commands.md b/docs/features/15-misc-commands.md new file mode 100644 index 0000000..29e90f4 --- /dev/null +++ b/docs/features/15-misc-commands.md @@ -0,0 +1,83 @@ +# Feature: Miscellaneous Commands & Flows + +**ID:** misc +**Role:** User / Admin +**Status:** Active + +--- + +## Purpose + +Collection of standalone commands and utility flows that don't belong to a single large subsystem. + +--- + +## User Commands + +| Command | Line | Purpose | Role | +|---------|------|---------|------| +| `/play` | ~7288 | Show play buttons (browser/miniapp) | User | +| `/signup` | ~7299 | Runewager signup links | User | +| `/discord` | ~7319 | Discord invite links | User | +| `/discord_confirm` | ~14531 | Confirm Discord step in onboarding | User | +| `/stuck` | ~14492 | Guided troubleshooting wizard | User | +| `/fixaccount` | ~14514 | Account recovery wizard | User | +| `/mygiveaways` | ~14549 | Personalized giveaway feed | User | +| `/checkin` | ~14601 | Daily check-in with streaks | User | +| `/boostmeter` | ~14648 | Referral boost status meter | User | +| `/eligible` | ~14665 | "Am I eligible?" helper | User | +| `/gwhistory` | ~14695 | Past giveaway history | User | +| `/promocheck` | ~14706 | Promo eligibility check | User | +| `/top` | ~14629 | Multi-metric leaderboard | User | +| `/leaderboard` | ~7158 | Top 10 referrers | Any | +| `/leaderboard_weekly` | ~7171 | Active users in 7d | Any | +| `/status` | ~7204 | Onboarding progress + badges | User | + +--- + +## Admin-Only Commands + +| Command | Line | Purpose | +|---------|------|---------| +| `/funnel` | ~14273 | Conversion funnel stats | +| `/scan_eligibility` | ~14225 | Check giveaway eligibility across users | +| `/pick_winner` | ~14329 | Manually run winner picker | +| `/gw_pause` | ~14154 | Pause a giveaway | +| `/gw_resume` | ~14175 | Resume a paused giveaway | +| `/testgiveaway` | ~13878 | Simulate a giveaway flow | + +--- + +## Walkthrough Flow + +``` +/walkthrough โ†’ 35-step interactive walkthrough + โ””โ”€โ”€ menu_walkthrough โ†’ start/resume walkthrough + โ”œโ”€โ”€ walk_back โ†’ previous step (disabled on step 1) + โ”œโ”€โ”€ walk_next โ†’ next step (disabled on last step) + โ””โ”€โ”€ walk_done โ†’ mark step complete; on last step: finish + return to menu +``` + +**โœ… Fully implemented.** See [`docs/features/16-walkthrough.md`](16-walkthrough.md) for complete documentation. + +--- + +## Background/Recurring Tasks + +| Task | Interval | Purpose | +|------|----------|---------| +| `persistAnalytics` | 15 seconds | Save analytics to disk | +| `alertIfHighErrorRate` | 1 minute | Notify admins if >10 errors/5min | +| `expireReferralBoosts` | 1 hour | Remove expired boosts | +| `runWeeklyBoostReminder` | 7 days | Send boost DMs to boosted users | +| `runWeeklyWagerReminder` | 7 days | Weekly wager reminder DM | +| `smartButtonGc` | 10 minutes | Prune expired smart buttons | +| `sshvGcTimer` | 1 minute | GC expired SSHV sessions | +| `tipsTimer` | configurable | Post tooltips to group | + +--- + +## File References + +- `index.js`: `/play` ~7288, misc commands ~14492โ€“14724 +- `index.js`: background tasks ~14833โ€“14920 diff --git a/docs/features/16-walkthrough.md b/docs/features/16-walkthrough.md new file mode 100644 index 0000000..f10cd66 --- /dev/null +++ b/docs/features/16-walkthrough.md @@ -0,0 +1,96 @@ +# Feature: Walkthrough System + +**ID:** walkthrough +**Role:** User +**Status:** Active + +--- + +## Purpose + +An interactive multi-step onboarding walkthrough (35 steps) that teaches users how to use Runewager. Each step has a title, body text, and optional image. Users can navigate forward/backward, mark steps complete, and finish the walkthrough to return to the main menu. + +--- + +## Entry Points + +| Trigger | Callback / Command | Role | +|---------|-------------------|------| +| `/walkthrough` | Start from beginning | User | +| Button: Walkthrough | `menu_walkthrough` | User | + +--- + +## Flow + +``` +/walkthrough OR menu_walkthrough + โ””โ”€โ”€ sendWalkthroughStep(ctx, user) + โ”œโ”€โ”€ clearOldMenus() called first (no stacking) + โ”œโ”€โ”€ Shows: Step N/35 โ€” title + body (+ image if present) + โ”œโ”€โ”€ Navigation row: + โ”‚ โ”œโ”€โ”€ Step 1: [โœ… Mark Complete] [โžก๏ธ Next] (no Back) + โ”‚ โ”œโ”€โ”€ Mid: [โฌ…๏ธ Back] [โœ… Mark Complete] [โžก๏ธ Next] + โ”‚ โ””โ”€โ”€ Last: [โฌ…๏ธ Back] [โœ… Mark Complete] [๐Ÿ Finish] + โ””โ”€โ”€ [๐Ÿ  Main Menu] always present + +walk_next โ†’ advance step (capped at last) +walk_back โ†’ retreat step (capped at 0) +walk_done (mid step) โ†’ mark complete, advance to next step +walk_done (last step) โ†’ mark complete, set started=false, go to main menu +``` + +--- + +## Buttons & Callbacks + +| Button | Callback | Condition | +|--------|----------|-----------| +| โฌ…๏ธ Back | `walk_back` | Steps 2โ€“35 only | +| โœ… Mark Complete | `walk_done` | All steps (mid) | +| โžก๏ธ Next | `walk_next` | Steps 1โ€“34 only | +| ๐Ÿ Finish | `walk_done` | Step 35 (last) only | +| ๐Ÿ  Main Menu | `to_main_menu` | Always | + +--- + +## State + +```javascript +user.walkthrough = { + currentStep: 0, // 0-indexed + completed: Set, // set of completed step indices + started: bool +} +``` + +- `completed.has(idx)` is shown with โœ… prefix on step title. +- `profileXP += 2` for each marked-complete step. +- On finish (last step `walk_done`): `started = false`. + +--- + +## Analytics Events + +| Event | When | +|-------|------| +| `walkthrough_navigation` | Every next/back/done | +| `walkthrough_step_completed` | On walk_done for a step | +| `walkthrough_completed` | On walk_done for last step | + +--- + +## Walkthrough Catalog + +Initialized via `buildWalkthroughCatalog()` at startup (~line 549). +35 steps covering: account setup, play modes, giveaways, referrals, bonuses, settings, etc. + +Each step: `{ title, body, image? }` where `image` is optional HTTPS URL or absolute path. + +--- + +## File References + +- `index.js`: `/walkthrough` ~6332, `menu_walkthrough` ~9399 +- `index.js`: `walk_(next|back|done)` handler ~9452 +- `index.js`: `sendWalkthroughStep()` ~12123, `buildWalkthroughCatalog()` ~12148 diff --git a/generate_tooltips.sh b/generate_tooltips.sh index 0c72380..875e868 100755 --- a/generate_tooltips.sh +++ b/generate_tooltips.sh @@ -1,33 +1,63 @@ #!/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. +# generate_tooltips.sh โ€” Seed the Helpful Tooltips data file on first run. +# +# SKIP GUARD: If data/tooltips.json already exists and contains entries, +# this script exits immediately without touching the file. This preserves +# any tooltips added at runtime via the bot admin panel (/tips โ†’ Add / Import). +# Use --force to override the guard and regenerate from DEFAULT_TIPS_LIST. +# +# Called automatically by deploy.sh, prod-run.sh, start.sh, dev-run.sh, and +# scripts/rollback.sh before bot restart. Only actually writes on first +# deploy (or after --force). # # Usage: -# ./generate_tooltips.sh [--dry-run] +# ./generate_tooltips.sh [--dry-run] [--force] # --dry-run Print what would be written without making changes. +# --force Overwrite tooltips.json even if it already has entries. set -euo pipefail +# Helper functions defined first so they are available during initialization. +info() { echo "[generate_tooltips] INFO: $*"; } +warn() { echo "[generate_tooltips] WARN: $*" >&2; } +error() { echo "[generate_tooltips] ERROR: $*" >&2; exit 1; } + SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -APP_DIR="${RUNEWAGER_DIR:-$SCRIPT_DIR}" +# Normalize to an absolute path immediately so the Node.js absolute-path +# validation (which requires a path starting with '/') never fails when +# RUNEWAGER_DIR is passed as a relative path. +_RAW_APP_DIR="${RUNEWAGER_DIR:-$SCRIPT_DIR}" +APP_DIR="$(cd "$_RAW_APP_DIR" 2>/dev/null && pwd)" || error "Invalid RUNEWAGER_DIR: $_RAW_APP_DIR" DATA_DIR="$APP_DIR/data" TOOLTIPS_FILE="$DATA_DIR/tooltips.json" TMP_FILE="$TOOLTIPS_FILE.tmp.$$" DRY_RUN=false +FORCE=false for arg in "$@"; do - [[ "$arg" == "--dry-run" ]] && DRY_RUN=true + case "$arg" in + --dry-run) DRY_RUN=true ;; + --force) FORCE=true ;; + *) error "Unknown option: $arg. Usage: ./generate_tooltips.sh [--dry-run] [--force]" ;; + esac 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" +# โ”€โ”€ Skip guard โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +# If tooltips.json already has entries, preserve them (runtime-added tips). +# Pass --force to regenerate from DEFAULT_TIPS_LIST regardless. +if [[ "$FORCE" == "false" && "$DRY_RUN" == "false" && -f "$TOOLTIPS_FILE" ]]; then + _EXISTING=$(TOOLTIPS_FILE_PATH="$TOOLTIPS_FILE" node -e \ + "try{var a=JSON.parse(require('fs').readFileSync(process.env.TOOLTIPS_FILE_PATH,'utf8'));process.stdout.write(String(Array.isArray(a)?a.length:0));}catch(e){process.stdout.write('0');}" \ + 2>/dev/null || echo 0) + if [[ "${_EXISTING:-0}" -gt 0 ]]; then + info "tooltips.json already has $_EXISTING entries โ€” skipping regeneration to preserve runtime tips (use --force to overwrite)." + exit 0 + fi +fi + # 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" @@ -36,7 +66,13 @@ fi info "Extracting DEFAULT_TIPS_LIST from index.js..." TOOLTIP_JSON=$(RUNEWAGER_APP="$APP_DIR/index.js" node - <<'EOF' const fs = require('fs'); -const src = fs.readFileSync(process.env.RUNEWAGER_APP || 'index.js', 'utf8'); +const appFile = process.env.RUNEWAGER_APP || 'index.js'; +// Validate path: must be absolute, end in .js, contain no null bytes or traversal +if (!/^\/[^\0]+\.js$/.test(appFile) || appFile.includes('..')) { + process.stderr.write('Invalid RUNEWAGER_APP path: ' + appFile + '\n'); + process.exit(1); +} +const src = fs.readFileSync(appFile, '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); } @@ -63,11 +99,15 @@ if [[ "$DRY_RUN" == "true" ]]; then exit 0 fi -# Atomic write: write to temp file, validate JSON, then move +# Atomic write: write to temp file, validate JSON, then move. +# Ensure the temp file is removed on any exit (error, interrupt, or normal +# completion). After a successful mv the path no longer exists, so the +# trap's rm -f is a safe no-op on the happy path. +trap 'rm -f "${TMP_FILE:-}"' EXIT INT TERM echo "$TOOLTIP_JSON" > "$TMP_FILE" # Validate JSON before replacing -node -e "JSON.parse(require('fs').readFileSync('$TMP_FILE','utf8'))" 2>/dev/null || { +VALIDATE_FILE="$TMP_FILE" node -e "JSON.parse(require('fs').readFileSync(process.env.VALIDATE_FILE,'utf8'))" 2>/dev/null || { rm -f "$TMP_FILE" # Write placeholder instead of failing warn "Generated JSON failed validation โ€” writing placeholder." @@ -76,4 +116,4 @@ node -e "JSON.parse(require('fs').readFileSync('$TMP_FILE','utf8'))" 2>/dev/null 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)")" +info "Total entries: $(TOOLTIPS_FILE_PATH="$TOOLTIPS_FILE" node -e "console.log(JSON.parse(require('fs').readFileSync(process.env.TOOLTIPS_FILE_PATH,'utf8')).length)")" diff --git a/index.js b/index.js index fe962cc..0f1e2f6 100644 --- a/index.js +++ b/index.js @@ -1219,9 +1219,7 @@ function loadRuntimeState() { for (const id of raw.approvedGroupsStore) approvedGroupsStore.add(Number(id)); } if (Array.isArray(raw.broadcastFailedUsers)) { - // Load only the most recent 500 entries to cap memory usage - const slice = raw.broadcastFailedUsers.slice(-500); - broadcastFailedUsers.push(...slice); + broadcastFailedUsers.push(...raw.broadcastFailedUsers); } if (typeof raw.promoStoreCooldownDays === 'number') { promoStore.cooldownDays = raw.promoStoreCooldownDays; @@ -2223,6 +2221,8 @@ function sshvKeyboard(session) { */ async function renderSshvConsole(ctx, session, note = '') { + const user = getUser(ctx); + await clearOldMenus(ctx, user); const text = [ '๐Ÿ“Ÿ *Runewager VPS Console*', `Path: \`${escapeMarkdownFull(session.cwd)}\``, @@ -2233,7 +2233,11 @@ async function renderSshvConsole(ctx, session, note = '') { note ? escapeMarkdownFull(note) : 'Enter command:', '`reply with command text`', ].join('\n'); - await ctx.reply(text, { parse_mode: 'MarkdownV2', ...sshvKeyboard(session) }); + const sent = await ctx.reply(text, { parse_mode: 'MarkdownV2', ...sshvKeyboard(session) }); + if (user && sent && sent.message_id) { + user.lastMenuMsgId = sent.message_id; + user.lastMenuChatId = sent.chat ? sent.chat.id : getContextChatId(ctx); + } } /** @@ -5790,7 +5794,9 @@ bot.start(safeStepHandler('start', async (ctx) => { // Show a one-time completion card the first time the user reaches the main menu if (!user.onboarding.completionCardShown) { user.onboarding.completionCardShown = true; - const rwName = user.runewagerUsername ? `*${user.runewagerUsername}*` : 'your account'; + const rwName = user.runewagerUsername + ? `*${String(user.runewagerUsername).replace(/[_*`[\]]/g, '\\$&')}*` + : 'your account'; await ctx.reply( `๐ŸŽ‰ *You're all set!*\n\n` + `โ—โ—โ—โ—โ— Onboarding complete!\n\n` @@ -8117,13 +8123,18 @@ async function sendOnboardingReferralPrompt(ctx, user) { await sendGambleCodezVIPStep(ctx, user); return; } - await ctx.reply( + await clearOldMenus(ctx, user); + const sent = await ctx.reply( 'Were you referred by a friend?', Markup.inlineKeyboard([ [Markup.button.callback('โœ… Yes, I was referred', 'onboard_ref_yes')], [Markup.button.callback('โžก๏ธ No, continue', 'onboard_ref_no')], ]), ); + if (sent && sent.message_id) { + user.lastMenuMsgId = sent.message_id; + user.lastMenuChatId = sent.chat ? sent.chat.id : ctx.chat?.id; + } } bot.action('onboard_ref_yes', async (ctx) => { @@ -8880,9 +8891,11 @@ function groupLinkingToolsKeyboard(returnCallback = 'menu_settings_tab') { } async function renderGroupLinkingTools(ctx, returnCallback = 'menu_settings_tab') { + const user = getUser(ctx); + await clearOldMenus(ctx, user); const groups = Array.from(approvedGroupsStore).map((id) => Number(id)).filter((id) => Number.isFinite(id)); const lines = groups.length ? groups.map((id) => `โ€ข ${id}`).join('\n') : 'โ€ข None linked yet.'; - await ctx.reply( + const sent = await ctx.reply( `๐Ÿ”— *Group Linking Tools* Linked groups: @@ -8891,6 +8904,10 @@ ${lines} Choose an action below.`, { parse_mode: 'Markdown', ...groupLinkingToolsKeyboard(returnCallback) }, ); + if (user && sent && sent.message_id) { + user.lastMenuMsgId = sent.message_id; + user.lastMenuChatId = sent.chat ? sent.chat.id : getContextChatId(ctx); + } } bot.action('admin_gw_group_linking', async (ctx) => { @@ -9451,12 +9468,27 @@ bot.action(/^walk_(next|back|done)$/, async (ctx) => { const user = getUser(ctx); const [, action] = ctx.match; const prevStep = user.walkthrough.currentStep; - if (action === 'next') user.walkthrough.currentStep = Math.min(walkthroughCatalog.length - 1, user.walkthrough.currentStep + 1); - if (action === 'back') user.walkthrough.currentStep = Math.max(0, user.walkthrough.currentStep - 1); - if (action === 'done') { - user.walkthrough.completed.add(user.walkthrough.currentStep); - trackAnalytics('walkthrough_step_completed', { userId: user.id, step: user.walkthrough.currentStep }); - user.profileXP += 2; + const isLastStep = prevStep === walkthroughCatalog.length - 1; + + if (action === 'next') { + user.walkthrough.currentStep = Math.min(walkthroughCatalog.length - 1, prevStep + 1); + } else if (action === 'back') { + user.walkthrough.currentStep = Math.max(0, prevStep - 1); + } else if (action === 'done') { + user.walkthrough.completed.add(prevStep); + user.profileXP = (user.profileXP || 0) + 2; + trackAnalytics('walkthrough_step_completed', { userId: user.id, step: prevStep }); + // On the last step, mark walkthrough fully complete and return to main menu + if (isLastStep) { + user.walkthrough.started = false; + trackAnalytics('walkthrough_completed', { userId: user.id, totalSteps: walkthroughCatalog.length }); + await ctx.answerCbQuery('๐ŸŽ‰ Walkthrough complete!'); + await clearOldMenus(ctx, user); + await sendPersistentUserMenu(ctx, user); + return; + } + // Otherwise advance to the next step + user.walkthrough.currentStep = Math.min(walkthroughCatalog.length - 1, prevStep + 1); } trackAnalytics('walkthrough_navigation', { userId: user.id, from: prevStep, to: user.walkthrough.currentStep, action }); await ctx.answerCbQuery(); @@ -11466,7 +11498,7 @@ function tipsDashboardKeyboard() { [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('๐Ÿ‘ View Tooltip', 'tips_cmd_view'), Markup.button.callback('๐Ÿงช Test Random', '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')], @@ -11666,16 +11698,73 @@ bot.action('tips_cmd_add', async (ctx) => { bot.action('tips_cmd_edit', async (ctx) => { if (!requireAdmin(ctx)) return; + const user = getUser(ctx); await ctx.answerCbQuery(); + await clearOldMenus(ctx, user); if (!tipsStore.tips.length) { await ctx.reply('No tooltips to edit.'); return; } - await ctx.reply('Edit which tooltip?', tipSelectKeyboard('tip_edit_select')); + const sent = await ctx.reply('โœ๏ธ Edit which tooltip?', tipSelectKeyboard('tip_edit_select')); + if (sent && sent.message_id) { user.lastMenuMsgId = sent.message_id; user.lastMenuChatId = sent.chat?.id ?? getContextChatId(ctx); } }); bot.action('tips_cmd_remove', async (ctx) => { if (!requireAdmin(ctx)) return; + const user = getUser(ctx); await ctx.answerCbQuery(); + await clearOldMenus(ctx, user); if (!tipsStore.tips.length) { await ctx.reply('No tooltips to remove.'); return; } - await ctx.reply('Remove which tooltip?', tipSelectKeyboard('tip_remove')); + const sent = await ctx.reply('โŒ Remove which tooltip?', tipSelectKeyboard('tip_remove')); + if (sent && sent.message_id) { user.lastMenuMsgId = sent.message_id; user.lastMenuChatId = sent.chat?.id ?? getContextChatId(ctx); } +}); + +bot.action('tips_cmd_view', async (ctx) => { + if (!requireAdmin(ctx)) return; + const user = getUser(ctx); + await ctx.answerCbQuery(); + await clearOldMenus(ctx, user); + if (!tipsStore.tips.length) { await ctx.reply('No tooltips to view.'); return; } + const sent = await ctx.reply('๐Ÿ‘ Preview which tooltip?', tipSelectKeyboard('tip_view')); + if (sent && sent.message_id) { user.lastMenuMsgId = sent.message_id; user.lastMenuChatId = sent.chat?.id ?? getContextChatId(ctx); } +}); + +bot.action(/^tip_view_(\d+)$/, async (ctx) => { + if (!requireAdmin(ctx)) return; + const user = getUser(ctx); + await ctx.answerCbQuery(); + const tipId = Number(ctx.match[1]); + const tip = tipsStore.tips.find((t) => t.id === tipId); + if (!tip) { await ctx.reply('Tooltip not found.'); return; } + + // Find adjacent tips for prev/next navigation + const tipIdx = tipsStore.tips.findIndex((t) => t.id === tipId); + const prevTip = tipIdx > 0 ? tipsStore.tips[tipIdx - 1] : null; + const nextTip = tipIdx < tipsStore.tips.length - 1 ? tipsStore.tips[tipIdx + 1] : null; + + const statusLabel = tip.enabled ? 'โœ… Enabled' : '๐Ÿ”‡ Disabled'; + const previewText = `๐Ÿ‘ *Tooltip #${tip.id} Preview* โ€” ${statusLabel}\n\n${tip.text}`; + + const navRow = []; + if (prevTip) navRow.push(Markup.button.callback(`โ—€ #${prevTip.id}`, `tip_view_${prevTip.id}`)); + if (nextTip) navRow.push(Markup.button.callback(`#${nextTip.id} โ–ถ`, `tip_view_${nextTip.id}`)); + + const keyboard = Markup.inlineKeyboard([ + ...(navRow.length ? [navRow] : []), + [ + Markup.button.callback('โœ๏ธ Edit', `tip_edit_select_${tipId}`), + Markup.button.callback('๐Ÿ” Toggle', `tip_toggle_${tipId}`), + Markup.button.callback('โŒ Delete', `tip_remove_${tipId}`), + ], + [ + Markup.button.callback('โ†ฉ Back to List', 'tips_cmd_view'), + Markup.button.callback('๐Ÿ  Admin Menu', 'pamenu_back_admin'), + ], + ]); + + await clearOldMenus(ctx, user); + const sent = await ctx.reply(previewText, { parse_mode: 'Markdown', ...keyboard }); + if (sent && sent.message_id) { + user.lastMenuMsgId = sent.message_id; + user.lastMenuChatId = sent.chat?.id ?? getContextChatId(ctx); + } }); bot.action('tips_cmd_toggle', async (ctx) => { @@ -12119,26 +12208,44 @@ bot.action('gw_create_yes', async (ctx) => { * System fit: This function is part of the Runewager command/callback/state orchestration pipeline. */ async function sendWalkthroughStep(ctx, user) { + await clearOldMenus(ctx, user); const idx = user.walkthrough.currentStep; const step = walkthroughCatalog[idx]; if (!step) { - await ctx.reply('Walkthrough complete!', Markup.inlineKeyboard([[Markup.button.callback('โฌ…๏ธ Main Menu', 'to_main_menu')]])); + // Past end โ€” walkthrough complete, go to main menu + user.walkthrough.started = false; + await sendPersistentUserMenu(ctx, user); return; } + const isFirst = idx === 0; + const isLast = idx === walkthroughCatalog.length - 1; + + // Build navigation row: Back disabled on first step, Nextโ†’Finish on last step + const navRow = []; + if (!isFirst) navRow.push(Markup.button.callback('โฌ…๏ธ Back', 'walk_back')); + navRow.push(Markup.button.callback('โœ… Mark Complete', 'walk_done')); + if (!isLast) { + navRow.push(Markup.button.callback('โžก๏ธ Next', 'walk_next')); + } else { + navRow.push(Markup.button.callback('๐Ÿ Finish', 'walk_done')); + } + const nav = Markup.inlineKeyboard([ - [Markup.button.callback('โฌ…๏ธ Back', 'walk_back'), Markup.button.callback('โœ… Mark Complete', 'walk_done'), Markup.button.callback('โžก๏ธ Next', 'walk_next')], - [Markup.button.callback('โฌ…๏ธ Main Menu', 'to_main_menu')], + navRow, + [Markup.button.callback('๐Ÿ  Main Menu', 'to_main_menu')], ]); - const header = `๐Ÿงญ Walkthrough ${idx + 1}/${walkthroughCatalog.length}\n${step.title}\n\n${step.body}`; + const doneIcon = user.walkthrough.completed.has(idx) ? 'โœ… ' : ''; + const header = `๐Ÿงญ Walkthrough โ€” Step ${idx + 1}/${walkthroughCatalog.length}\n${doneIcon}${step.title}\n\n${step.body}`; - // image: step.image can be an HTTPS URL or local absolute path โ€” both handled by sendPhoto() - if (step.image) { - await sendPhoto(ctx, step.image, header, nav); - return; + const sent = await (step.image + ? sendPhoto(ctx, step.image, header, nav) + : ctx.reply(header, nav)); + if (sent && sent.message_id) { + user.lastMenuMsgId = sent.message_id; + user.lastMenuChatId = sent.chat ? sent.chat.id : getContextChatId(ctx); } - await ctx.reply(header, nav); } /** @@ -14315,12 +14422,27 @@ bot.command('broadcast_retry', safeAdminHandler('broadcast_retry', { usage: '/br bot.command('broadcast_failed', safeAdminHandler('broadcast_failed', { usage: '/broadcast_failed', example: '/broadcast_failed' }, async (ctx) => { if (!requireAdmin(ctx)) return; if (!broadcastFailedUsers.length) { await ctx.reply('โœ… No permanently failed broadcast users.'); return; } - const lines = broadcastFailedUsers.slice(0, 30).map((e) => { - const user = userStore.get(e.userId); - const name = user ? (user.tgUsername ? `@${user.tgUsername}` : `user${e.userId}`) : `id:${e.userId}`; - return `โ€ข ${name} โ€” ${(e.lastError || '').slice(0, 60)}`; - }); - await ctx.reply(`๐Ÿ“‹ *Broadcast Failed Users (${broadcastFailedUsers.length})*\n\n${lines.join('\n')}`, { parse_mode: 'Markdown' }); + const total = broadcastFailedUsers.length; + const CHUNK = 30; + // Send first chunk immediately; subsequent chunks sent as follow-up messages + for (let i = 0; i < Math.min(total, 90); i += CHUNK) { + const chunk = broadcastFailedUsers.slice(i, i + CHUNK); + const lines = chunk.map((e) => { + const u = userStore.get(e.userId); + const name = u ? (u.tgUsername ? `@${u.tgUsername}` : `user${e.userId}`) : `id:${e.userId}`; + return `โ€ข ${name} โ€” ${(e.lastError || '').slice(0, 60)}`; + }); + const header = i === 0 + ? `๐Ÿ“‹ *Broadcast Failed Users (${total})*${total > 90 ? ` โ€” showing first 90; full list in data/admin-events.log` : ''}\n\n` + : `๐Ÿ“‹ *โ€ฆcontinued (${i + 1}โ€“${Math.min(i + CHUNK, total)})*\n\n`; + // eslint-disable-next-line no-await-in-loop + await ctx.reply(header + lines.join('\n'), { parse_mode: 'Markdown' }); + } + // Warn if failure rate is high (>20% of userStore) + const totalUsers = userStore.size; + if (totalUsers > 0 && total / totalUsers > 0.20) { + await ctx.reply(`โš ๏ธ *High failure rate:* ${total} failed out of ${totalUsers} users (${Math.round((total / totalUsers) * 100)}%). Check bot permissions, user blocks, and Telegram limits.`, { parse_mode: 'Markdown' }); + } })); // โ”€โ”€ Feature 7: Internal Winner Picker (cryptographic seed) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ @@ -14768,14 +14890,29 @@ async function runWeeklyBoostReminder() { } catch (e) { user.unreachable = true; broadcastFailedUsers.push({ userId: user.id, lastError: e.message, failedAt: now }); - // Cap at 500 entries to prevent unbounded growth - if (broadcastFailedUsers.length > 500) broadcastFailedUsers.splice(0, broadcastFailedUsers.length - 500); + // Log each failure to admin-events.log so no entry is ever silently dropped + adminLog('broadcast_failure', { userId: user.id, error: e.message }); } // Yield between sends to avoid blocking the event loop on large user bases // eslint-disable-next-line no-await-in-loop await new Promise((r) => setImmediate(r)); } - logEvent('info', 'Weekly boost reminder sent', { sent }); + const failed = broadcastFailedUsers.length; + logEvent('info', 'Weekly boost reminder sent', { sent, failed }); + // Warn admins if more than 20% of attempted users failed + const attempted = sent + failed; + if (attempted > 0 && failed / attempted > 0.20) { + const adminIds = (process.env.ADMIN_IDS || '').split(',').map((id) => id.trim()).filter(Boolean); + for (const adminId of adminIds) { + try { + await bot.telegram.sendMessage( + adminId, + `โš ๏ธ *Broadcast Warning*\n\n${failed} of ${attempted} weekly boost DMs failed (${Math.round((failed / attempted) * 100)}%).\nUse /broadcast_failed to view and /broadcast_retry to retry.`, + { parse_mode: 'Markdown' }, + ); + } catch (_) { /* non-fatal */ } + } + } } // ========================= diff --git a/scripts/helpers/free_port.sh b/scripts/helpers/free_port.sh new file mode 100755 index 0000000..a19aae2 --- /dev/null +++ b/scripts/helpers/free_port.sh @@ -0,0 +1,49 @@ +#!/usr/bin/env bash +# scripts/helpers/free_port.sh โ€” Kill any process blocking a TCP port. +# +# Source this file and call: free_port +# Or run directly: bash scripts/helpers/free_port.sh +# +# Strategy: SIGTERM all blocking PIDs, wait 2 s, then re-query the port for +# survivors and SIGKILL only those. Re-querying avoids killing an unrelated +# process that may have reused a PID during the wait window. +# Uses lsof (preferred) or fuser as fallback. Both are treated as optional: +# if neither tool is present the function exits silently (no-op). + +_query_port_pids() { + local port="$1" + if command -v lsof >/dev/null 2>&1; then + lsof -ti :"$port" 2>/dev/null || true + elif command -v fuser >/dev/null 2>&1; then + fuser -n tcp "$port" 2>/dev/null | tr ' ' '\n' | sed '/^$/d' || true + fi +} + +free_port() { + local port="${1:-3000}" + local _pids="" + _pids="$(_query_port_pids "$port")" + if [[ -n "$_pids" ]]; then + echo "[free_port] Port $port blocked โ€” sending SIGTERM then SIGKILL..." + # Graceful shutdown: SIGTERM all original blockers. + for _p in $_pids; do kill "$_p" 2>/dev/null || true; done + sleep 2 + # Re-query the port for survivors; only SIGKILL processes that are still + # listening on this port (guards against PID reuse in the sleep window). + local _still="" + _still="$(_query_port_pids "$port")" + for _p in $_still; do + kill -0 "$_p" 2>/dev/null && kill -9 "$_p" 2>/dev/null || true + done + sleep 1 + fi +} + +# When invoked directly (not sourced), free the port given as $1. +if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then + if [[ -z "${1:-}" ]]; then + echo "Usage: $0 " >&2 + exit 1 + fi + free_port "$1" +fi diff --git a/scripts/rollback.sh b/scripts/rollback.sh index ed16254..4a3c5d0 100755 --- a/scripts/rollback.sh +++ b/scripts/rollback.sh @@ -102,17 +102,9 @@ echo "rollback from=${CURRENT_SHA} to=${RESOLVED_SHA} at=$(date -u +%Y%m%dT%H%M% > "${PROJECT_DIR}/.last_rollback" # โ”€โ”€ Kill anything blocking port before restart โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ -_RB_BLOCKING="" -if command -v lsof >/dev/null 2>&1; then - _RB_BLOCKING="$(lsof -ti :"$PORT" 2>/dev/null || true)" -elif command -v fuser >/dev/null 2>&1; then - _RB_BLOCKING="$(fuser -n tcp "$PORT" 2>/dev/null | tr ' ' '\n' | sed '/^$/d' || true)" -fi -if [[ -n "$_RB_BLOCKING" ]]; then - say "Port $PORT blocked โ€” killing before start..." - for _pid in $_RB_BLOCKING; do kill -9 "$_pid" 2>/dev/null || true; done - sleep 1 -fi +# shellcheck source=scripts/helpers/free_port.sh +. "$SCRIPT_DIR/helpers/free_port.sh" +free_port "$PORT" # โ”€โ”€ Restart service โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ say "Starting ${SERVICE_NAME}.service..." diff --git a/start.sh b/start.sh index 471a1d5..6594460 100644 --- a/start.sh +++ b/start.sh @@ -41,12 +41,23 @@ if [[ ! -f "$PROJECT_DIR/.env" ]]; then fi ############################################### -# Pull latest code before starting -############################################### -echo "๐Ÿ“ฅ Pulling latest code from origin main..." -git -C "$PROJECT_DIR" fetch origin main 2>&1 \ - && git -C "$PROJECT_DIR" reset --hard origin/main 2>&1 \ - || echo "โš ๏ธ git pull failed โ€” starting with local copy" +# Optionally pull latest code before starting. +# Reads RUNEWAGER_AUTO_UPDATE from environment first, then from .env as +# fallback so the flag works even when .env is not exported into the shell. +# Default: 1 (enabled) โ€” set to 0 in .env or environment to disable. +############################################### +_AUTO_UPD_DOTENV="$(grep -E '^RUNEWAGER_AUTO_UPDATE=' "$PROJECT_DIR/.env" 2>/dev/null | head -1 | cut -d= -f2 | cut -d'#' -f1 | tr -d '"' | tr -d "'" | tr -d ' ' | tr -d $'\r' || true)" +_AUTO_UPDATE="${RUNEWAGER_AUTO_UPDATE:-${_AUTO_UPD_DOTENV:-1}}" +if [[ "$_AUTO_UPDATE" == "1" ]]; then + echo "๐Ÿ“ฅ Pulling latest code from origin main..." + if git -C "$PROJECT_DIR" fetch origin main 2>&1 && git -C "$PROJECT_DIR" reset --hard origin/main 2>&1; then + echo "โœ… Code updated to $(git -C "$PROJECT_DIR" rev-parse --short HEAD)" + else + echo "โš ๏ธ git pull failed โ€” starting with local copy" + fi +else + echo "โ„น๏ธ RUNEWAGER_AUTO_UPDATE=0 โ€” skipping git pull" +fi ############################################### # Refresh tooltips from updated index.js @@ -61,18 +72,11 @@ fi ############################################### # Kill anything blocking port 3000 (or PORT) ############################################### -BOT_PORT="$(grep -E '^PORT=' "$PROJECT_DIR/.env" 2>/dev/null | head -1 | cut -d= -f2 | tr -d '"' | tr -d "'" | tr -d $'\r')" +BOT_PORT="$(grep -E '^PORT=' "$PROJECT_DIR/.env" 2>/dev/null | head -1 | cut -d= -f2 | cut -d'#' -f1 | tr -d '"' | tr -d "'" | tr -d ' ' | tr -d $'\r')" BOT_PORT="${BOT_PORT:-3000}" -if command -v lsof >/dev/null 2>&1; then - _PIDS="$(lsof -ti :"$BOT_PORT" 2>/dev/null || true)" -elif command -v fuser >/dev/null 2>&1; then - _PIDS="$(fuser -n tcp "$BOT_PORT" 2>/dev/null | tr ' ' '\n' | sed '/^$/d' || true)" -fi -if [[ -n "${_PIDS:-}" ]]; then - echo "โš ๏ธ Port $BOT_PORT blocked โ€” killing before start..." - for _pid in $_PIDS; do kill -9 "$_pid" 2>/dev/null || true; done - sleep 1 -fi +# shellcheck source=scripts/helpers/free_port.sh +. "$PROJECT_DIR/scripts/helpers/free_port.sh" +free_port "$BOT_PORT" ############################################### # Kill any stale bot instance diff --git a/test/smoke.test.js b/test/smoke.test.js index dee6f28..85b189c 100644 --- a/test/smoke.test.js +++ b/test/smoke.test.js @@ -449,6 +449,8 @@ test('catch-all detection recognizes supported regex forms', () => { const catchAllCases = [ '.*', '^.*$', '.+', '^.+$', '(.*)', '(.+)', '(?:.*)', '(?:.+)', '^(?:.*)$', '(.+)?', '(.|\n)*', '(.|\n)+', '(\\.|[\\s\\S])*', + // Post-whitespace-strip forms (isCatchAllRegexPattern strips \s before comparing) + '(.|)*', '(.|)+', ]; for (const c of catchAllCases) assert.equal(isCatchAllRegexPattern(c), true, `expected "${c}" to be catch-all`); // Specific patterns with capture groups are NOT catch-all