From 446f5db2c09f23df58bfc6a2e3832740cd2a10f8 Mon Sep 17 00:00:00 2001 From: GambleCodez Affiliates Date: Thu, 26 Feb 2026 15:24:56 -0600 Subject: [PATCH] Full implementation + menu integrity + fallback completion + help booklet completion + map sync --- .env.example | 5 +- .gitignore | 13 +- CLAUDE.md | 19 + RUNEWAGER_FUNCTIONALITY_MAP.md | 335 ++ index.js | 5739 ++++++++++++++++++++++++++------ promo-message.js | 60 + test/runtime.test.js | 20 + test/smoke.test.js | 360 +- test/unit.test.js | 120 + 9 files changed, 5587 insertions(+), 1084 deletions(-) create mode 100644 RUNEWAGER_FUNCTIONALITY_MAP.md diff --git a/.env.example b/.env.example index 8da3d32..346eb1f 100644 --- a/.env.example +++ b/.env.example @@ -3,7 +3,9 @@ ANNOUNCE_CHANNEL="-1002648883359" DEVICE=vps ADMIN_IDS=YOUR_TELEGRAM_USER_ID BOT_TOKEN= -WEBAPP_HMAC_KEY= +# WEBAPP_HMAC_KEY: 32-byte secret for HMAC-signing webapp payloads/CSRF tokens. +# Generate with: openssl rand -hex 32 (or openssl rand -base64 32). Keep secret; never commit real values. +WEBAPP_HMAC_KEY="" TELEGRAM_BOT_TOKEN= DISCORD_CODE_GENERATION_IMAGE_URL=https://raw.githubusercontent.com/gamblecodezcom/Runewager/main/images/discord_code_generation.png DISCORD_VERIFY_IMAGE_URL=https://raw.githubusercontent.com/gamblecodezcom/Runewager/main/images/discord_verify.png @@ -14,6 +16,7 @@ MAX_ONBOARDING_STEPS_HISTORY=500 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 +# LOG_LEVEL valid values: debug, info, warn, error (default: info) LOG_LEVEL=info PORT=3000 RW_DISCORD_JOIN=https://discord.gg/runewagers diff --git a/.gitignore b/.gitignore index 19ef187..a9a87fb 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,9 @@ .env .env.* *.env +!.env.example +!.env.sample +!.env.template # Logs logs/ @@ -25,10 +28,6 @@ Thumbs.db .last_rollback data/admin-events.log -# Runtime state files -data/analytics.json -data/dashboard.json -data/helpful_messages.json -data/promo-history.json -data/runtime-state.json -data/sshv-sessions.json +# Runtime generated data + backups +data/*.json +data/backups/** diff --git a/CLAUDE.md b/CLAUDE.md index c607fad..bf7405a 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -140,3 +140,22 @@ You are the Runewager Bot Ops Commander. You manage the full lifecycle of the Ru - Bonus state machine with transition guards is solid - Structured JSON logging throughout - Graceful SIGTERM/SIGINT with state persist before exit + +--- + +## AI Coder Contract Requirements (Mandatory) + +Before writing or modifying any code, the AI must always read and familiarize itself with `RUNEWAGER_FUNCTIONALITY_MAP.md`. This file is the authoritative source of truth for all bot functionality. The AI must keep this file updated with full, easy-to-understand descriptions whenever functionality is added, changed, or removed. Updating this file is a required step at the end of every coding session. + +After generating or modifying any functionality, the AI must run a follow-up audit to ensure the `RUNEWAGER_FUNCTIONALITY_MAP.md` file is fully updated and accurate. No coding session is complete until the map is updated and verified. + + +Every coding session must end with a verification pass to detect and upgrade any missing functionality, followed by an update to RUNEWAGER_FUNCTIONALITY_MAP.md. No work is considered complete until the map is fully updated and verified. + + +Every coding session must begin by reading RUNEWAGER_FUNCTIONALITY_MAP.md and must end with a full 50‑point verification pass. No work is complete until the map is updated and all 50 checks pass with zero failures. + + +Every coding session must begin by reading RUNEWAGER_FUNCTIONALITY_MAP.md and must end with a full docstring verification pass. No work is complete until every function has a complete docstring and the map is fully updated. + +All AI agent instruction files in this repo must enforce this baseline workflow: (1) read and understand `RUNEWAGER_FUNCTIONALITY_MAP.md` before changing code, (2) update the map after any code change, (3) run a full verification pass after the map update, and (4) do not consider work complete until docstrings, the map, and agent instruction files are synchronized. diff --git a/RUNEWAGER_FUNCTIONALITY_MAP.md b/RUNEWAGER_FUNCTIONALITY_MAP.md new file mode 100644 index 0000000..b73b57d --- /dev/null +++ b/RUNEWAGER_FUNCTIONALITY_MAP.md @@ -0,0 +1,335 @@ +# RUNEWAGER_FUNCTIONALITY_MAP.md + +_Last audited: 2026-02-26_ +_Source of truth files: `index.js`, `test/*.test.js`, scripts under `scripts/`, deployment/runtime docs in repo root._ + +--- + +## 1. High‑Level Overview + +Runewager is a Telegraf-based Telegram bot that provides: +- User onboarding (age gate, account/Discord guidance, username linking). +- Promo flows (DB-backed promo manager + eligibility + claim lifecycle). +- Giveaway flows (creation, join, eligibility checks, auto finalization, admin controls). +- Content Drops (scheduled/random posts to configured target chat). +- Admin operations (broadcasts, diagnostics, SSHV console, bug triage, backups). +- Runtime health and deploy tooling (`/health`, scripts, systemd template). + +Navigation is driven by inline menus plus command aliases. Persistent user/admin menu headers are used in DMs for fast access. + +## 2. System Architecture Summary + +### Core runtime +- Single runtime app in `index.js`. +- Telegraf command handlers + callback handlers. +- Per-user mutable state stored in memory and persisted to JSON runtime snapshots. + +### State/storage layers +- In-memory stores: users, giveaway state, analytics, promo manager store, content drops store, broadcast config, SSHV sessions. +- File persistence under `data/` (runtime snapshots + promo DB + optional backups). +- Periodic persistence timer + startup restore. + +### Routing model +- `bot.command(...)` for slash commands. +- `bot.action(...)` for inline button callbacks (literal + regex handlers). +- `bot.on('text')` for pending “await input” state machine. + +### Utility subsystems +- Eligibility checks, validation helpers, markdown escaping, analytics/event logging. +- Admin audit logging (`adminLog` and NDJSON appends). +- SSHV command execution safety checks and state restoration. + +## 3. Global Bot Behaviors + +- Unknown command handling routes to a structured error block with guidance. +- `requireAdmin(ctx)` enforces admin-only access without implicitly toggling admin mode. +- `sendCommandError(...)` standardizes command failure messaging. +- Menus use callback “back” paths (`to_main_menu`, `open_admin_dashboard`, `pamenu_back_*`). +- Many menu-style responses use `replaceCallbackPanel(...)` to avoid stale stacked cards. + +## 4. User Menu Tree + +### Main User Menu +- Core entry via `/start`, `/menu`, or persistent header. +- Includes onboarding/account actions, promo/bonus entry points, community links, giveaways, help, settings. + +### Key user branches +- **Profile & status**: `/profile`, quick status callbacks. +- **Promo**: `menu_claim_bonus` → filtered eligible promos only. +- **Giveaways**: `menu_giveaways` and `/mygiveaways` for active and personalized entries. +- **Walkthrough/help**: paginated booklet (`help_page_N`) and walkthrough steps (`walk_next/back/done`). +- **Settings**: play mode + quick commands + tooltip toggles. + +### Expected input & fallback +- Non-command text when no pending state is mostly ignored except smart username detect. +- Invalid/unknown commands get standard help guidance. + +## 5. Admin Menu Tree + +### Persistent Admin Main Menu +- User Ops & Stats +- Giveaway Ops +- Promo Manager +- Broadcasts & Drops +- System & Health +- Tests & Bugs +- VPS Console (/sshv) +- Content Drops manager shortcut + +### Admin category menus +- `admin_cat_giveaway`: start/test/status + persistent navigation row (`Admin Dashboard`, `Main Menu`, `Cancel`). +- `admin_cat_promo`: full promo manager actions + guide + persistent navigation row (`Admin Dashboard`, `Main Menu`, `Cancel`). +- `admin_cat_system`: health/version/verify/setup/backup/admin mode/testall/sshv + persistent navigation row (`Admin Dashboard`, `Main Menu`, `Cancel`). +- `admin_cat_tests`: bug tools + test tools + sshv shortcut + return navigation controls. +- `admin_cat_support`: bug report management shortcuts + persistent navigation row (`Admin Dashboard`, `Main Menu`, `Cancel`). + +### Restrictions +- All above require `requireAdmin(ctx)`. +- Many actions also require pending state sequencing and validation checks. + +## 6. Button‑to‑Action Map + +### Dynamic callback families (regex handlers) +- `help_page_` +- `page_giveaways_` +- `promo_open_`, `promo_claim_` +- `gw_join_`, `gw_details_`, `gw_elig_` and giveaway admin variants +- `pamenu_gw_end_`, `pamenu_gw_extend_`, `pamenu_gw_cancel_`, `pamenu_gw_participants_` +- `tip_edit_select_`, `tip_remove_`, `tip_toggle_` + +### Representative literal callbacks +- Menu routing: `to_main_menu`, `open_admin_dashboard`, `pamenu_back_admin`, `pamenu_back_user` +- Announce builder: `announce_toggle_dm/channel/group/mode`, `announce_send_now`, `announce_edit` +- Promo manager: `admin_pm_create/edit/pause_toggle/delete/stats/preview/queue/help` +- Content drops: `tips_cmd_add/edit/remove/toggle/list/test/settings/import_batch` +- SSHV: `sshv_open`, `sshv_run_prompt`, `sshv_ctrl_c`, `sshv_ctrl_z`, `sshv_editor_save`, `sshv_editor_cancel` + +## 7. Wait‑For‑Input States (with timeouts) + +The bot uses `user.pendingAction.type` as its input state machine. Key families: + +- **Onboarding/account**: `await_runewager_username`, `await_username_confirm`. +- **Announcements**: `await_announcement_text`, `await_announcement_action`. +- **Promo manager wizard**: + - `admin_pm_create_name` → `..._code` → `..._domain` → `..._description` → `..._image` → `..._requirement` → optional wager substeps → claim/cooldown/approval. +- **Giveaway creation/edit**: `gwiz_*`, legacy `gw_*` chains, edit/extend states. +- **Content drops**: `await_tip_add_text`, `await_tip_edit_text`, `await_tip_settings_interval`. +- **Admin utility prompts**: whois/refresh/resolve bug/bonus prompts. +- **SSHV**: `await_sshv_command`, `await_sshv_editor_content`, `await_sshv_danger_confirm`. +- **Forward registration**: `await_register_chat_forward`. + +### Timeouts +- Global pending-action timeout now enforced in text flow: `PENDING_ACTION_TIMEOUT_MS = 15 minutes`; expired wait states are reset with a user-facing timeout message and `Main Menu` recovery buttons. +- SSHV sessions have TTL-based GC (`SSHV_SESSION_TTL_MS`), with stale-state normalization and recovery. + +## 8. Fallback Logic & Error Handling + +- Permission failures: `requireAdmin` + `sendCommandError`. +- Pending slash escapes: when a pending state exists, `/cancel` clears state; `/menu`, `/start`, and `/help` clear pending state and return user to main menu flow. +- Invalid pending input: explicit prompts to retry (numeric checks, yes/no checks, IDs). +- Missing entities (promo/giveaway not found): user-visible explanatory replies. +- Announce/test flows return actionable follow-up messages. +- SSHV errors include sandbox hints for EROFS/EACCES/privilege constraints. + +## 9. Onboarding Flow (full step map) + +1. `/start` intro + age confirmation gate. +2. Account setup guidance (Runewager + Discord verification steps). +3. Username linking via `/link` or text detect + confirm callbacks. +4. Promo and bonus prompts depending on linked state and eligibility. +5. Community join prompts (channel/group flags). +6. Walkthrough progression tracking (`user.walkthrough`, onboarding milestones). + +Recovery: +- `confirm_no_username` returns to username entry. +- `/stuck` and `/fixaccount` provide guided recovery paths. + +## 10. Group Commands + +Group-aware commands include giveaway interaction and linking shortcuts: +- `/giveaway` (admin wizard in group context). +- `/join` and `gw_join_` for participant entry. +- `/eligible [id]` checks eligibility. +- `/link ` from group attempts DM handoff for confirmation. + +Restrictions: +- Admin-only commands require admin identity even in groups. +- Join surface checks enforce giveaway surface constraints (group vs DM). + +## 11. User Help Booklet + +- Multi-page booklet with pagination callbacks (`help_page_`), including safe page-to-page transitions. +- Covers: overview, account setup, promo/bonus usage, giveaways/community, support. +- Navigation exits: back/menu controls on each page; settings submenu now also includes explicit cancel exit (`to_main_menu`). + +## 12. Admin Help Booklet + +- Admin page includes admin command references and points operators to dashboard categories. +- Bugs/tests/system-heavy workflows are now surfaced in dashboard menus (Tests & Bugs / System Tools) to reduce command overload. +- Admin category submenus explicitly provide `Admin Dashboard` return plus `Main Menu`/`Cancel` safety exits to avoid dead-end navigation. + +## 13. Full Command Index (User + Admin) + +### User-facing commands (also includes aliases) +_Note: unknown-command guard uses `REGISTERED_COMMANDS`; parity is now validated by smoke test to prevent silent command breakage._ +`affiliate, bonus, bugreport, cancel, checkin, claim_history, commands, discord, discord_confirm, eligible, fixaccount, giveaway, gwhistory, health, help, join, leaderboard, leaderboard_weekly, link, linkaccount, linkrunewager, menu, mygiveaways, play, profile, promo, promocheck, referral, settings, signup, startapp, status, stuck, support, top, walkthrough` + +### Admin commands +`A, a, announce, admin, admin_backup, admin_log, admin_notify, approve_group, bonusstatus, boost_referrals, boostmeter, broadcast_failed, broadcast_retry, deploy, deploy_status, exportbugs, gw_graphic, gw_pause, gw_resume, list_groups, logs, off, on, pick_winner, pmapprove, pmdeny, promo_cooldown, refreshuser, register_chat, resolvebug, scan_eligibility, setpromo, sshv, start_giveaway, testall, testgiveaway, tipadd, tipedit, tiplist, tipremove, tips, tipsettings, tiptest, tiptoggle, t, tp, unapprove_group, verify_bot_setup, version, wager30_admin, whois` + +Notes: +- Several commands are dual-purpose aliases. +- Many admin buttons call the same underlying command logic. + +## 14. Permissions & Access Control + +- Admin identity is based on `ADMIN_IDS` env list. +- `requireAdmin(ctx)` gates command/callback execution. +- Admin mode UI toggle changes visibility of admin UI, not true authorization. +- Group approvals list controls where certain broadcast/group operations target. + +## 15. State Machine & Session Logic + +Primary per-user state fields: +- `pendingAction` (typed-input router) +- onboarding/walkthrough progress +- promo/giveaway participation flags +- settings (play mode, quick commands, tooltips) +- admin mode display preference + +SSHV session state: +- cwd, output buffer, lock, running process refs, editor mode draft, last activity timestamps. + +## 16. Timeout & Auto‑Cancel Rules + +Configured/observed time behaviors: +- Smart button dedupe TTL: ~2 minutes. +- SSHV session TTL GC: 10 minutes (`SSHV_SESSION_TTL_MS`). +- Periodic persistence intervals and scheduler timers (content drops, maintenance timers). +- Giveaway timers drive auto finalization and countdown behavior. + +No universal pendingAction timeout/auto-cancel was detected for all user/admin input flows. + +## 17. Validation Rules (username, discord, etc.) + +- Username normalization + format checks for Runewager username linking. +- Numeric validations for wager/giveaway/promo fields. +- Yes/no parsing for legacy wizard decisions. +- Promo creation expects base-domain style input for casino field. +- Forward registration validates forwarded chat presence and chat id. + +## 18. Rate Limits & Cooldowns + +### Verification controls +- Smoke test now enforces `REGISTERED_COMMANDS` parity with `bot.command(...)` handlers (except `start`, handled by `bot.start`). + + +- Promo-level cooldown and claim-limit checks in eligibility evaluation. +- Bonus request lifecycle enforces transition constraints and attempt limits. +- Smart button anti-duplicate TTL reduces repeated callback abuse. +- Broadcast failure tracking caps and log caps exist to bound memory growth. + +## 19. Safety & Abuse Prevention Logic + +- Admin gating via `requireAdmin` on sensitive flows. +- SSHV command blocking/confirmation for dangerous shell patterns. +- SSHV sandbox error hinting to clarify denied operations. +- Markdown escaping utilities for user/path/output interpolation. +- Command error wrappers to reduce undefined behavior on invalid usage. + +## 20. Edge Cases & Special Conditions + +- Empty states (no giveaways/promos/bugs) return explicit panels. +- Missing/expired SSHV editor sessions recover to command mode. +- Missing pending context for confirm callbacks returns “Nothing to confirm.” +- Group DM-handoff fallback for `/link` when private DM unavailable. +- Auto-registration accepts forwarded-origin/sender chat variants when available. + +## 21. External Integrations (if any) + +- Telegram Bot API via Telegraf. +- Optional Runewager-related URLs for mini app/profile/promo entry. +- Optional health HTTP/HTTPS server (TLS cert paths env-driven). +- OS/system interactions for logs/deploy/health and SSHV command execution. + +## 22. Internal Utilities & Shared Modules + +Repo modules: +- `index.js` main app. +- `promo-message.js` promo copy helper. +- `scripts/*.sh` deployment/runtime ops (backup, restore, smoke, rollback, diagnostics). +- `test/*.test.js` smoke/unit/runtime tests. + +Key shared helper families in `index.js`: +- Markdown escape helpers. +- command error helper. +- persistence load/save utilities. +- eligibility evaluators (promo/giveaway). +- menu/panel replacement helpers. + +## 23. Feature‑Level Flowcharts + +### Promo claim flow +```text +/promo or menu_claim_bonus + -> load active promos + -> evaluate eligibility per promo + -> eligible list shown + -> user taps promo_open_ + -> details + eligibility result + -> promo_claim_ + -> auto_approve? yes => claimed + -> no => pending_approval queue +``` + +### Announcement flow +```text +admin_cmd_announce_start or /announce + -> await_announcement_text + -> preview + toggles (DM/Channel/Group + parse mode) + -> announce_send_now + -> sendAnnouncementTargets + -> summary result +``` + +### Content drop target registration +```text +Admin forwards message from channel/group + -> autoRegisterForwardedChatIfPresent + -> approvedGroupsStore add(chatId) + -> tipsStore.targetGroup = chatId + -> broadcastConfigStore.targetGroup = chatId +``` + +### Giveaway start/join +```text +Admin starts wizard (gwiz) + -> collect config steps + -> createGiveaway + announceGiveaway + -> users click gw_join_ + -> evaluateEligibility + -> join accepted/rejected + -> timer expires -> finalizeGiveaway +``` + +## 24. Future Updates Log + +- 2026-02-26: Created initial comprehensive functionality map and synchronized with current menu/state architecture. +- 2026-02-26: Added explicit note that template-literal callback IDs are excluded from literal smoke checks. +- 2026-02-26: Documented Tests & Bugs admin category, promo step guide, and forwarded-chat auto-registration behavior. +- 2026-02-26: Added command-registry verification notes and synchronized missing admin commands (`/pmapprove`, `/pmdeny`, `/register_chat`, `/verify_bot_setup`). + +- 2026-02-26: Completed a 50-point verification pass, synchronized command registry coverage, and extended smoke checks for command-registration parity. + +- 2026-02-26: Added repo-wide baseline docstrings to named function declarations (runtime, helper, and test modules) and re-verified command/callback coverage. +- 2026-02-26: Re-verified removal of `/language` command/callback surface, reran smoke/full tests, and synchronized agent-contract wording in `CLAUDE.md` with map-first/update-map/verify requirements. +- 2026-02-26: Completed a menu-integrity pass by adding explicit `Main Menu` + `Cancel` exits to admin category keyboards and settings submenu, then revalidated smoke/full tests. +- 2026-02-26: Added global 15-minute pending-action timeout and slash-command escape fallbacks (`/cancel`, `/menu`, `/start`, `/help`) for wait-for-input state safety. + +## 25. AI Coder Contract Requirements + +Mandatory rules for any AI agent touching this repo: + +1. **Before writing or modifying any code, the AI must always read and familiarize itself with `RUNEWAGER_FUNCTIONALITY_MAP.md`. This file is the authoritative source of truth for all bot functionality. The AI must keep this file updated with full, easy‑to‑understand descriptions whenever functionality is added, changed, or removed. Updating this file is a required step at the end of every coding session.** + +2. **After generating or modifying any functionality, the AI must run a follow-up audit to ensure the `RUNEWAGER_FUNCTIONALITY_MAP.md` file is fully updated and accurate. No coding session is complete until the map is updated and verified.** diff --git a/index.js b/index.js index 3215ada..e2cd4f9 100644 --- a/index.js +++ b/index.js @@ -34,8 +34,8 @@ const LINKS = { rwDiscordJoin: process.env.RW_DISCORD_JOIN || 'https://discord.gg/runewagers', rwDiscordLink: process.env.RW_DISCORD_LINK || 'https://discord.com/channels/1100486422395355197/1249181934811349052', // Telegram channels/groups — external links - gczChannel: 'https://t.me/GambleCodezDrops', - gczGroup: 'https://t.me/GambleCodezPrizeHub', + gczChannel: process.env.TELEGRAM_CHANNEL_LINK || 'https://t.me/GambleCodezDrops', + gczGroup: process.env.TELEGRAM_GROUP_LINK || 'https://t.me/GambleCodezPrizeHub', // Mini App links — always open INSIDE Telegram miniAppPlay: process.env.MINI_APP_PLAY_URL || 'https://t.me/RuneWager_bot/Play', miniAppProfile: process.env.MINI_APP_PROFILE_URL || 'https://t.me/RuneWager_bot/profile', @@ -43,12 +43,20 @@ const LINKS = { }; const DEFAULT_BONUS_RULE = `Wager 3,000 SC in any rolling 7-day period → 30 SC bonus (once per account). Need help? Contact @GambleCodez on Telegram or open a support ticket: ${LINKS.rwDiscordSupport}`; +const PROMO_STATUS = { ACTIVE: 'active', PAUSED: 'paused', DELETED: 'deleted' }; +const PROMO_REQUIREMENT = { + NEW_USER_ONLY: 'new_user_only', + EXISTING_USER: 'existing_user', + EXISTING_USER_WITH_WAGER: 'existing_user_with_wager_requirement', +}; // Channel username (or chat_id) for admin announcements via /announce -const ANNOUNCE_CHANNEL = process.env.ANNOUNCE_CHANNEL || '@GambleCodezDrops'; +const TELEGRAM_CHANNEL_ID = process.env.TELEGRAM_CHANNEL_ID || process.env.ANNOUNCE_CHANNEL || ''; +const TELEGRAM_GROUP_ID = process.env.TELEGRAM_GROUP_ID || ''; +const ANNOUNCE_CHANNEL = TELEGRAM_CHANNEL_ID; -// Group username (or chat_id) for automatic silent tips -const TIPS_GROUP = process.env.TIPS_GROUP || '@GambleCodezPrizeHub'; +// Group chat_id for automatic Content Drops +const TIPS_GROUP = process.env.TIPS_GROUP || TELEGRAM_GROUP_ID || ''; // ── Images ───────────────────────────────────────────────────────────────── // Images live in /images/ next to index.js. @@ -148,47 +156,23 @@ bot.catch((err, ctx) => { // ========================= const userStore = new Map(); const promoStore = { - active: true, - code: process.env.PROMO_CODE || 'Sports3.5', - amountSC: Number(process.env.PROMO_AMOUNT_SC) || 3.5, - totalClaimLimit: Number(process.env.PROMO_CLAIM_LIMIT) || 600, - remainingClaims: Number(process.env.PROMO_CLAIM_LIMIT) || 600, - bonusRule: process.env.PROMO_BONUS_RULE || DEFAULT_BONUS_RULE, - claimsByUser: new Set(), + bonusRule: DEFAULT_BONUS_RULE, + bugreports: [], logs: [], - cooldownDays: 0, // Feature 10: 0 = no cooldown; >0 = days between claims per user -}; - -const PROMO_AUDIENCE = { - NEW_USER: 'new_user', - EXISTING_USER: 'existing_user', }; -const PROMO_REQUIREMENT_TYPE = { - WAGER: 'wager', - NO_REQUIREMENT: 'no_requirement', +const promoManagerStore = { + nextPromoId: 1, + nextClaimId: 1, + promos: [], + claims: [], + eligibilityFailures: [], }; -const PROMO_GLOBAL_COOLDOWN_MS = 24 * 60 * 60 * 1000; - -const DEFAULT_PROMO_CODES = [ - { - id: 1, - code: process.env.PROMO_CODE || 'Sports3.5', - audience: PROMO_AUDIENCE.NEW_USER, - requirementType: PROMO_REQUIREMENT_TYPE.WAGER, - amountSC: Number(process.env.PROMO_AMOUNT_SC) || 3.5, - notes: 'New user welcome bonus', - active: true, - createdAt: Date.now(), - updatedAt: Date.now(), - }, -]; - -const promoCodeStore = { - nextId: 2, - codes: DEFAULT_PROMO_CODES.map((code) => ({ ...code })), -}; +// Legacy compatibility placeholders (promo logic now lives in promoManagerStore only) +const PROMO_AUDIENCE = { NEW_USER: 'new_user', EXISTING_USER: 'existing_user' }; +const PROMO_REQUIREMENT_TYPE = { WAGER: 'wager', NO_REQUIREMENT: 'no_requirement' }; +const promoCodeStore = { nextId: 1, codes: [] }; const giveawayStore = { running: new Map(), @@ -226,6 +210,11 @@ const tipsStore = { lastSentTipId: null, }; +const broadcastConfigStore = { + targetChannel: ANNOUNCE_CHANNEL, + targetGroup: TIPS_GROUP, +}; + let tipsTimerRef = null; const dataDir = path.join(__dirname, 'data'); @@ -233,10 +222,41 @@ const promoHistoryFile = path.join(dataDir, 'promo-history.json'); const analyticsFile = path.join(dataDir, 'analytics.json'); const dashboardFile = path.join(dataDir, 'dashboard.json'); const runtimeStateFile = path.join(dataDir, 'runtime-state.json'); +const promoManagerDbFile = path.join(dataDir, 'promo-manager-db.json'); const helpfulMessagesFile = path.join(dataDir, 'helpful_messages.json'); const sshvSessionsFile = path.join(dataDir, 'sshv-sessions.json'); +/** + + + * normalizeHelpfulMessageEntry executes its scoped Runewager logic and participates in menu/command or utility flow composition. + + + * Parameters: See the function signature for exact argument names and accepted values. + + + * Returns: Returns the computed value or a Promise resolving to the operation result; may return void for side-effect handlers. + + + * Side effects: May mutate runtime stores, pendingAction state, menu state, persistence files, logs, and callback progression. + + + * Validation/safety: Uses existing guard utilities (admin checks, input checks, path checks, cooldown checks) where applicable. + + + * Timeouts/fallbacks: Timeout and fallback behavior are controlled by the calling flow and global handler/state machine conventions. + + + * Errors: Surfaces user-facing error replies and/or logs when inputs, permissions, or dependencies are invalid. + + + * System fit: This function is part of the Runewager command/callback/state orchestration pipeline. + + + */ + + function normalizeHelpfulMessageEntry(entry, idHint = null) { if (!entry || typeof entry !== 'object') return null; const text = typeof entry.text === 'string' ? entry.text.trim() : ''; @@ -246,11 +266,51 @@ function normalizeHelpfulMessageEntry(entry, idHint = null) { return { id: Number(id), text, enabled: entry.enabled !== false }; } +/** + + * saveHelpfulMessages executes its scoped Runewager logic and participates in menu/command or utility flow composition. + + * Parameters: See the function signature for exact argument names and accepted values. + + * Returns: Returns the computed value or a Promise resolving to the operation result; may return void for side-effect handlers. + + * Side effects: May mutate runtime stores, pendingAction state, menu state, persistence files, logs, and callback progression. + + * Validation/safety: Uses existing guard utilities (admin checks, input checks, path checks, cooldown checks) where applicable. + + * Timeouts/fallbacks: Timeout and fallback behavior are controlled by the calling flow and global handler/state machine conventions. + + * Errors: Surfaces user-facing error replies and/or logs when inputs, permissions, or dependencies are invalid. + + * System fit: This function is part of the Runewager command/callback/state orchestration pipeline. + + */ + function saveHelpfulMessages() { const snapshot = tipsStore.tips.map((tip) => ({ text: tip.text, enabled: tip.enabled !== false })); saveJson(helpfulMessagesFile, snapshot); } +/** + + * loadHelpfulMessages executes its scoped Runewager logic and participates in menu/command or utility flow composition. + + * Parameters: See the function signature for exact argument names and accepted values. + + * Returns: Returns the computed value or a Promise resolving to the operation result; may return void for side-effect handlers. + + * Side effects: May mutate runtime stores, pendingAction state, menu state, persistence files, logs, and callback progression. + + * Validation/safety: Uses existing guard utilities (admin checks, input checks, path checks, cooldown checks) where applicable. + + * Timeouts/fallbacks: Timeout and fallback behavior are controlled by the calling flow and global handler/state machine conventions. + + * Errors: Surfaces user-facing error replies and/or logs when inputs, permissions, or dependencies are invalid. + + * System fit: This function is part of the Runewager command/callback/state orchestration pipeline. + + */ + function loadHelpfulMessages() { const raw = loadJson(helpfulMessagesFile, null); if (!Array.isArray(raw)) return; @@ -318,6 +378,8 @@ const NON_USERNAME_WORDS = new Set([ ]); // Slash commands implemented in this file. Used for unknown-command fallback. +const PENDING_ACTION_TIMEOUT_MS = 15 * 60 * 1000; // 15m timeout for wait-for-input states + const REGISTERED_COMMANDS = new Set([ 'A', 'a', @@ -357,7 +419,6 @@ const REGISTERED_COMMANDS = new Set([ 'health', 'help', 'join', - 'language', 'leaderboard', 'leaderboard_weekly', 'link', @@ -407,6 +468,10 @@ const REGISTERED_COMMANDS = new Set([ 'wager30_admin', 'walkthrough', 'whois', + 'pmapprove', + 'pmdeny', + 'register_chat', + 'verify_bot_setup', ]); const WAGER_BONUS_RULES_TEXT = @@ -427,6 +492,26 @@ const WAGER_BONUS_RULES_TEXT = const WAGER_BONUS_IN_FLIGHT_STATUSES = new Set(['pending', 'approved']); const WAGER_BONUS_NON_REQUESTABLE_STATUSES = new Set(['bonus_sent', ...WAGER_BONUS_IN_FLIGHT_STATUSES]); +/** + + * isWagerBonusRequestLocked executes its scoped Runewager logic and participates in menu/command or utility flow composition. + + * Parameters: See the function signature for exact argument names and accepted values. + + * Returns: Returns the computed value or a Promise resolving to the operation result; may return void for side-effect handlers. + + * Side effects: May mutate runtime stores, pendingAction state, menu state, persistence files, logs, and callback progression. + + * Validation/safety: Uses existing guard utilities (admin checks, input checks, path checks, cooldown checks) where applicable. + + * Timeouts/fallbacks: Timeout and fallback behavior are controlled by the calling flow and global handler/state machine conventions. + + * Errors: Surfaces user-facing error replies and/or logs when inputs, permissions, or dependencies are invalid. + + * System fit: This function is part of the Runewager command/callback/state orchestration pipeline. + + */ + function isWagerBonusRequestLocked(status) { return WAGER_BONUS_NON_REQUESTABLE_STATUSES.has(status); } @@ -438,6 +523,26 @@ const _LOG_MIN_RANK = LOG_LEVEL_RANK[String(process.env.LOG_LEVEL || 'info').toL // Rolling error rate tracker — alerts admins when errors spike const _errorRate = { count: 0, windowStart: Date.now(), alerted: false }; +/** + + * logEvent executes its scoped Runewager logic and participates in menu/command or utility flow composition. + + * Parameters: See the function signature for exact argument names and accepted values. + + * Returns: Returns the computed value or a Promise resolving to the operation result; may return void for side-effect handlers. + + * Side effects: May mutate runtime stores, pendingAction state, menu state, persistence files, logs, and callback progression. + + * Validation/safety: Uses existing guard utilities (admin checks, input checks, path checks, cooldown checks) where applicable. + + * Timeouts/fallbacks: Timeout and fallback behavior are controlled by the calling flow and global handler/state machine conventions. + + * Errors: Surfaces user-facing error replies and/or logs when inputs, permissions, or dependencies are invalid. + + * System fit: This function is part of the Runewager command/callback/state orchestration pipeline. + + */ + function logEvent(level, message, extra = {}) { const rank = LOG_LEVEL_RANK[level] ?? 1; if (rank < _LOG_MIN_RANK) return; // filtered out by LOG_LEVEL @@ -465,12 +570,52 @@ const ALLOWED_BONUS_STATUS_TRANSITIONS = { bonus_sent: new Set(), }; +/** + + * isValidBonusTransition executes its scoped Runewager logic and participates in menu/command or utility flow composition. + + * Parameters: See the function signature for exact argument names and accepted values. + + * Returns: Returns the computed value or a Promise resolving to the operation result; may return void for side-effect handlers. + + * Side effects: May mutate runtime stores, pendingAction state, menu state, persistence files, logs, and callback progression. + + * Validation/safety: Uses existing guard utilities (admin checks, input checks, path checks, cooldown checks) where applicable. + + * Timeouts/fallbacks: Timeout and fallback behavior are controlled by the calling flow and global handler/state machine conventions. + + * Errors: Surfaces user-facing error replies and/or logs when inputs, permissions, or dependencies are invalid. + + * System fit: This function is part of the Runewager command/callback/state orchestration pipeline. + + */ + function isValidBonusTransition(fromStatus, toStatus) { if (fromStatus === toStatus) return true; const allowed = ALLOWED_BONUS_STATUS_TRANSITIONS[fromStatus] || new Set(); return allowed.has(toStatus); } +/** + + * runUserMutation executes its scoped Runewager logic and participates in menu/command or utility flow composition. + + * Parameters: See the function signature for exact argument names and accepted values. + + * Returns: Returns the computed value or a Promise resolving to the operation result; may return void for side-effect handlers. + + * Side effects: May mutate runtime stores, pendingAction state, menu state, persistence files, logs, and callback progression. + + * Validation/safety: Uses existing guard utilities (admin checks, input checks, path checks, cooldown checks) where applicable. + + * Timeouts/fallbacks: Timeout and fallback behavior are controlled by the calling flow and global handler/state machine conventions. + + * Errors: Surfaces user-facing error replies and/or logs when inputs, permissions, or dependencies are invalid. + + * System fit: This function is part of the Runewager command/callback/state orchestration pipeline. + + */ + async function runUserMutation(userId, fn) { const previous = userMutationQueue.get(userId) || Promise.resolve(); const next = previous @@ -482,6 +627,26 @@ async function runUserMutation(userId, fn) { return next; } +/** + + * serializeUser executes its scoped Runewager logic and participates in menu/command or utility flow composition. + + * Parameters: See the function signature for exact argument names and accepted values. + + * Returns: Returns the computed value or a Promise resolving to the operation result; may return void for side-effect handlers. + + * Side effects: May mutate runtime stores, pendingAction state, menu state, persistence files, logs, and callback progression. + + * Validation/safety: Uses existing guard utilities (admin checks, input checks, path checks, cooldown checks) where applicable. + + * Timeouts/fallbacks: Timeout and fallback behavior are controlled by the calling flow and global handler/state machine conventions. + + * Errors: Surfaces user-facing error replies and/or logs when inputs, permissions, or dependencies are invalid. + + * System fit: This function is part of the Runewager command/callback/state orchestration pipeline. + + */ + function serializeUser(user) { return { ...user, @@ -494,6 +659,26 @@ function serializeUser(user) { }; } +/** + + * deserializeUser executes its scoped Runewager logic and participates in menu/command or utility flow composition. + + * Parameters: See the function signature for exact argument names and accepted values. + + * Returns: Returns the computed value or a Promise resolving to the operation result; may return void for side-effect handlers. + + * Side effects: May mutate runtime stores, pendingAction state, menu state, persistence files, logs, and callback progression. + + * Validation/safety: Uses existing guard utilities (admin checks, input checks, path checks, cooldown checks) where applicable. + + * Timeouts/fallbacks: Timeout and fallback behavior are controlled by the calling flow and global handler/state machine conventions. + + * Errors: Surfaces user-facing error replies and/or logs when inputs, permissions, or dependencies are invalid. + + * System fit: This function is part of the Runewager command/callback/state orchestration pipeline. + + */ + function deserializeUser(rawUser) { const user = { ...rawUser }; user.badges = new Set(rawUser.badges || []); @@ -505,6 +690,26 @@ function deserializeUser(rawUser) { return user; } +/** + + * createRuntimeStateSnapshot executes its scoped Runewager logic and participates in menu/command or utility flow composition. + + * Parameters: See the function signature for exact argument names and accepted values. + + * Returns: Returns the computed value or a Promise resolving to the operation result; may return void for side-effect handlers. + + * Side effects: May mutate runtime stores, pendingAction state, menu state, persistence files, logs, and callback progression. + + * Validation/safety: Uses existing guard utilities (admin checks, input checks, path checks, cooldown checks) where applicable. + + * Timeouts/fallbacks: Timeout and fallback behavior are controlled by the calling flow and global handler/state machine conventions. + + * Errors: Surfaces user-facing error replies and/or logs when inputs, permissions, or dependencies are invalid. + + * System fit: This function is part of the Runewager command/callback/state orchestration pipeline. + + */ + function createRuntimeStateSnapshot() { return { version: 1, @@ -548,9 +753,33 @@ function createRuntimeStateSnapshot() { targetGroup: tipsStore.targetGroup, nextTipId: tipsStore.nextTipId, }, + broadcastConfigStore: { + targetChannel: broadcastConfigStore.targetChannel, + targetGroup: broadcastConfigStore.targetGroup, + }, }; } +/** + + * serializeGiveaway executes its scoped Runewager logic and participates in menu/command or utility flow composition. + + * Parameters: See the function signature for exact argument names and accepted values. + + * Returns: Returns the computed value or a Promise resolving to the operation result; may return void for side-effect handlers. + + * Side effects: May mutate runtime stores, pendingAction state, menu state, persistence files, logs, and callback progression. + + * Validation/safety: Uses existing guard utilities (admin checks, input checks, path checks, cooldown checks) where applicable. + + * Timeouts/fallbacks: Timeout and fallback behavior are controlled by the calling flow and global handler/state machine conventions. + + * Errors: Surfaces user-facing error replies and/or logs when inputs, permissions, or dependencies are invalid. + + * System fit: This function is part of the Runewager command/callback/state orchestration pipeline. + + */ + function serializeGiveaway(giveaway) { return { id: Number(giveaway.id), @@ -588,6 +817,26 @@ function serializeGiveaway(giveaway) { }; } +/** + + * deserializeGiveaway executes its scoped Runewager logic and participates in menu/command or utility flow composition. + + * Parameters: See the function signature for exact argument names and accepted values. + + * Returns: Returns the computed value or a Promise resolving to the operation result; may return void for side-effect handlers. + + * Side effects: May mutate runtime stores, pendingAction state, menu state, persistence files, logs, and callback progression. + + * Validation/safety: Uses existing guard utilities (admin checks, input checks, path checks, cooldown checks) where applicable. + + * Timeouts/fallbacks: Timeout and fallback behavior are controlled by the calling flow and global handler/state machine conventions. + + * Errors: Surfaces user-facing error replies and/or logs when inputs, permissions, or dependencies are invalid. + + * System fit: This function is part of the Runewager command/callback/state orchestration pipeline. + + */ + function deserializeGiveaway(rawGiveaway) { const sanitized = serializeGiveaway(rawGiveaway || {}); return { @@ -598,9 +847,66 @@ function deserializeGiveaway(rawGiveaway) { }; } +/** + + * normalizePromoCodeEntry executes its scoped Runewager logic and participates in menu/command or utility flow composition. + + * Parameters: See the function signature for exact argument names and accepted values. + + * Returns: Returns the computed value or a Promise resolving to the operation result; may return void for side-effect handlers. + + * Side effects: May mutate runtime stores, pendingAction state, menu state, persistence files, logs, and callback progression. + + * Validation/safety: Uses existing guard utilities (admin checks, input checks, path checks, cooldown checks) where applicable. + + * Timeouts/fallbacks: Timeout and fallback behavior are controlled by the calling flow and global handler/state machine conventions. + + * Errors: Surfaces user-facing error replies and/or logs when inputs, permissions, or dependencies are invalid. + + * System fit: This function is part of the Runewager command/callback/state orchestration pipeline. + + */ + +function normalizePromoCodeEntry(raw, idHint = null) { + if (!raw || typeof raw !== 'object') return null; + const code = String(raw.code || '').trim(); + if (!code) return null; + return { + id: Number(raw.id) || Number(idHint || 0), + code, + audience: raw.audience || PROMO_AUDIENCE.NEW_USER, + requirementType: raw.requirementType || PROMO_REQUIREMENT_TYPE.NO_REQUIREMENT, + amountSC: Number(raw.amountSC) || 0, + notes: String(raw.notes || ''), + active: raw.active !== false, + createdAt: Number(raw.createdAt) || Date.now(), + updatedAt: Number(raw.updatedAt) || Date.now(), + }; +} + // Startup warnings deferred until after bot.launch() so Telegram API is available const _startupWarnings = []; +/** + + * loadRuntimeState executes its scoped Runewager logic and participates in menu/command or utility flow composition. + + * Parameters: See the function signature for exact argument names and accepted values. + + * Returns: Returns the computed value or a Promise resolving to the operation result; may return void for side-effect handlers. + + * Side effects: May mutate runtime stores, pendingAction state, menu state, persistence files, logs, and callback progression. + + * Validation/safety: Uses existing guard utilities (admin checks, input checks, path checks, cooldown checks) where applicable. + + * Timeouts/fallbacks: Timeout and fallback behavior are controlled by the calling flow and global handler/state machine conventions. + + * Errors: Surfaces user-facing error replies and/or logs when inputs, permissions, or dependencies are invalid. + + * System fit: This function is part of the Runewager command/callback/state orchestration pipeline. + + */ + function loadRuntimeState() { // Differentiate "file missing" (normal first start) from "file corrupt" (needs alert) let raw = null; @@ -645,7 +951,7 @@ function loadRuntimeState() { : []; promoCodeStore.codes = loadedCodes.length > 0 ? loadedCodes - : DEFAULT_PROMO_CODES.map((code) => ({ ...code })); + : []; const maxId = promoCodeStore.codes.reduce((acc, code) => Math.max(acc, Number(code.id) || 0), 0); promoCodeStore.nextId = Math.max(Number(raw.promoCodeStore.nextId) || 0, maxId + 1, 2); } @@ -700,14 +1006,64 @@ function loadRuntimeState() { if (typeof raw.tipsStore.nextTipId === 'number') tipsStore.nextTipId = raw.tipsStore.nextTipId; } tipsStore.nextTipId = Math.max(tipsStore.nextTipId, tipsStore.tips.reduce((m, t) => Math.max(m, Number(t.id) || 0), 0) + 1); + if (raw.broadcastConfigStore) { + if (typeof raw.broadcastConfigStore.targetChannel === 'string' && raw.broadcastConfigStore.targetChannel.trim()) { + broadcastConfigStore.targetChannel = raw.broadcastConfigStore.targetChannel.trim(); + } + if (typeof raw.broadcastConfigStore.targetGroup === 'string' && raw.broadcastConfigStore.targetGroup.trim()) { + broadcastConfigStore.targetGroup = raw.broadcastConfigStore.targetGroup.trim(); + tipsStore.targetGroup = raw.broadcastConfigStore.targetGroup.trim(); + } + } } +/** + + * persistRuntimeState executes its scoped Runewager logic and participates in menu/command or utility flow composition. + + * Parameters: See the function signature for exact argument names and accepted values. + + * Returns: Returns the computed value or a Promise resolving to the operation result; may return void for side-effect handlers. + + * Side effects: May mutate runtime stores, pendingAction state, menu state, persistence files, logs, and callback progression. + + * Validation/safety: Uses existing guard utilities (admin checks, input checks, path checks, cooldown checks) where applicable. + + * Timeouts/fallbacks: Timeout and fallback behavior are controlled by the calling flow and global handler/state machine conventions. + + * Errors: Surfaces user-facing error replies and/or logs when inputs, permissions, or dependencies are invalid. + + * System fit: This function is part of the Runewager command/callback/state orchestration pipeline. + + */ + function persistRuntimeState() { const snapshot = createRuntimeStateSnapshot(); saveJson(runtimeStateFile, snapshot); + savePromoManagerDb(); lastPersistAt = Date.now(); } +/** + + * createRuntimeBackup executes its scoped Runewager logic and participates in menu/command or utility flow composition. + + * Parameters: See the function signature for exact argument names and accepted values. + + * Returns: Returns the computed value or a Promise resolving to the operation result; may return void for side-effect handlers. + + * Side effects: May mutate runtime stores, pendingAction state, menu state, persistence files, logs, and callback progression. + + * Validation/safety: Uses existing guard utilities (admin checks, input checks, path checks, cooldown checks) where applicable. + + * Timeouts/fallbacks: Timeout and fallback behavior are controlled by the calling flow and global handler/state machine conventions. + + * Errors: Surfaces user-facing error replies and/or logs when inputs, permissions, or dependencies are invalid. + + * System fit: This function is part of the Runewager command/callback/state orchestration pipeline. + + */ + function createRuntimeBackup() { ensureDataDir(); if (!fs.existsSync(runtimeStateFile)) { @@ -724,20 +1080,60 @@ function createRuntimeBackup() { return target; } +/** + + * consumeAdminAction executes its scoped Runewager logic and participates in menu/command or utility flow composition. + + * Parameters: See the function signature for exact argument names and accepted values. + + * Returns: Returns the computed value or a Promise resolving to the operation result; may return void for side-effect handlers. + + * Side effects: May mutate runtime stores, pendingAction state, menu state, persistence files, logs, and callback progression. + + * Validation/safety: Uses existing guard utilities (admin checks, input checks, path checks, cooldown checks) where applicable. + + * Timeouts/fallbacks: Timeout and fallback behavior are controlled by the calling flow and global handler/state machine conventions. + + * Errors: Surfaces user-facing error replies and/or logs when inputs, permissions, or dependencies are invalid. + + * System fit: This function is part of the Runewager command/callback/state orchestration pipeline. + + */ + function consumeAdminAction(adminId, actionKey) { return consumeSmartButton(adminId, `admin:${actionKey}`); } -function createDefaultUser(user) { - return { - id: user.id, - tgUsername: user.username || '', - firstName: user.first_name || '', - ageConfirmed: false, - sawGambleCodezStep: false, // true after new VIP code step is shown/dismissed - existingRwUser: false, // true if user self-identified as existing Runewager user - runewagerUsername: '', - verified: false, +/** + + * createDefaultUser executes its scoped Runewager logic and participates in menu/command or utility flow composition. + + * Parameters: See the function signature for exact argument names and accepted values. + + * Returns: Returns the computed value or a Promise resolving to the operation result; may return void for side-effect handlers. + + * Side effects: May mutate runtime stores, pendingAction state, menu state, persistence files, logs, and callback progression. + + * Validation/safety: Uses existing guard utilities (admin checks, input checks, path checks, cooldown checks) where applicable. + + * Timeouts/fallbacks: Timeout and fallback behavior are controlled by the calling flow and global handler/state machine conventions. + + * Errors: Surfaces user-facing error replies and/or logs when inputs, permissions, or dependencies are invalid. + + * System fit: This function is part of the Runewager command/callback/state orchestration pipeline. + + */ + +function createDefaultUser(user) { + return { + id: user.id, + tgUsername: user.username || '', + firstName: user.first_name || '', + ageConfirmed: false, + sawGambleCodezStep: false, // true after new VIP code step is shown/dismissed + existingRwUser: false, // true if user self-identified as existing Runewager user + runewagerUsername: '', + verified: false, hasJoinedChannel: false, hasJoinedGroup: false, claimedPromo: false, @@ -797,6 +1193,26 @@ function createDefaultUser(user) { }; } +/** + + * getUser executes its scoped Runewager logic and participates in menu/command or utility flow composition. + + * Parameters: See the function signature for exact argument names and accepted values. + + * Returns: Returns the computed value or a Promise resolving to the operation result; may return void for side-effect handlers. + + * Side effects: May mutate runtime stores, pendingAction state, menu state, persistence files, logs, and callback progression. + + * Validation/safety: Uses existing guard utilities (admin checks, input checks, path checks, cooldown checks) where applicable. + + * Timeouts/fallbacks: Timeout and fallback behavior are controlled by the calling flow and global handler/state machine conventions. + + * Errors: Surfaces user-facing error replies and/or logs when inputs, permissions, or dependencies are invalid. + + * System fit: This function is part of the Runewager command/callback/state orchestration pipeline. + + */ + function getUser(ctx) { const { id } = ctx.from; if (!userStore.has(id)) userStore.set(id, createDefaultUser(ctx.from)); @@ -862,6 +1278,7 @@ function getUser(ctx) { if (user.supportTicket === undefined) user.supportTicket = null; if (!user.claimedPromoCodes) user.claimedPromoCodes = []; if (!Array.isArray(user.claimedPromoCodes)) user.claimedPromoCodes = []; + if (user.hasClaimedNewUserPromo === undefined) user.hasClaimedNewUserPromo = false; if (!user.claimedPromoAt) user.claimedPromoAt = 0; if (!user.lastAnyPromoClaimAt) user.lastAnyPromoClaimAt = 0; if (user.claimedPromo && !user.claimedPromoAt) user.claimedPromoAt = Date.now(); @@ -876,21 +1293,101 @@ function getUser(ctx) { return user; } +/** + + * isAdmin executes its scoped Runewager logic and participates in menu/command or utility flow composition. + + * Parameters: See the function signature for exact argument names and accepted values. + + * Returns: Returns the computed value or a Promise resolving to the operation result; may return void for side-effect handlers. + + * Side effects: May mutate runtime stores, pendingAction state, menu state, persistence files, logs, and callback progression. + + * Validation/safety: Uses existing guard utilities (admin checks, input checks, path checks, cooldown checks) where applicable. + + * Timeouts/fallbacks: Timeout and fallback behavior are controlled by the calling flow and global handler/state machine conventions. + + * Errors: Surfaces user-facing error replies and/or logs when inputs, permissions, or dependencies are invalid. + + * System fit: This function is part of the Runewager command/callback/state orchestration pipeline. + + */ + function isAdmin(ctx) { return ADMIN_IDS.includes(Number(ctx.from.id)); } +/** + + * persistAdminMode executes its scoped Runewager logic and participates in menu/command or utility flow composition. + + * Parameters: See the function signature for exact argument names and accepted values. + + * Returns: Returns the computed value or a Promise resolving to the operation result; may return void for side-effect handlers. + + * Side effects: May mutate runtime stores, pendingAction state, menu state, persistence files, logs, and callback progression. + + * Validation/safety: Uses existing guard utilities (admin checks, input checks, path checks, cooldown checks) where applicable. + + * Timeouts/fallbacks: Timeout and fallback behavior are controlled by the calling flow and global handler/state machine conventions. + + * Errors: Surfaces user-facing error replies and/or logs when inputs, permissions, or dependencies are invalid. + + * System fit: This function is part of the Runewager command/callback/state orchestration pipeline. + + */ + function persistAdminMode(user, enabled) { user.adminModeOn = enabled; persistRuntimeState(); } +/** + + * refreshAdminMenuHeader executes its scoped Runewager logic and participates in menu/command or utility flow composition. + + * Parameters: See the function signature for exact argument names and accepted values. + + * Returns: Returns the computed value or a Promise resolving to the operation result; may return void for side-effect handlers. + + * Side effects: May mutate runtime stores, pendingAction state, menu state, persistence files, logs, and callback progression. + + * Validation/safety: Uses existing guard utilities (admin checks, input checks, path checks, cooldown checks) where applicable. + + * Timeouts/fallbacks: Timeout and fallback behavior are controlled by the calling flow and global handler/state machine conventions. + + * Errors: Surfaces user-facing error replies and/or logs when inputs, permissions, or dependencies are invalid. + + * System fit: This function is part of the Runewager command/callback/state orchestration pipeline. + + */ + async function refreshAdminMenuHeader(ctx, user) { const chatType = ctx.chat && ctx.chat.type ? ctx.chat.type : null; if (chatType !== 'private') return; await sendPersistentAdminMenu(ctx, user); } +/** + + * requireAdmin executes its scoped Runewager logic and participates in menu/command or utility flow composition. + + * Parameters: See the function signature for exact argument names and accepted values. + + * Returns: Returns the computed value or a Promise resolving to the operation result; may return void for side-effect handlers. + + * Side effects: May mutate runtime stores, pendingAction state, menu state, persistence files, logs, and callback progression. + + * Validation/safety: Uses existing guard utilities (admin checks, input checks, path checks, cooldown checks) where applicable. + + * Timeouts/fallbacks: Timeout and fallback behavior are controlled by the calling flow and global handler/state machine conventions. + + * Errors: Surfaces user-facing error replies and/or logs when inputs, permissions, or dependencies are invalid. + + * System fit: This function is part of the Runewager command/callback/state orchestration pipeline. + + */ + function requireAdmin(ctx) { const cmd = ctx.message ? String(ctx.message.text || '').split(' ')[0].replace('/', '') @@ -926,6 +1423,36 @@ async function sendCommandError(ctx, { command, reason, howToUse, example }) { } +/** + + + * normalizeSshvBufferLines executes its scoped Runewager logic and participates in menu/command or utility flow composition. + + + * Parameters: See the function signature for exact argument names and accepted values. + + + * Returns: Returns the computed value or a Promise resolving to the operation result; may return void for side-effect handlers. + + + * Side effects: May mutate runtime stores, pendingAction state, menu state, persistence files, logs, and callback progression. + + + * Validation/safety: Uses existing guard utilities (admin checks, input checks, path checks, cooldown checks) where applicable. + + + * Timeouts/fallbacks: Timeout and fallback behavior are controlled by the calling flow and global handler/state machine conventions. + + + * Errors: Surfaces user-facing error replies and/or logs when inputs, permissions, or dependencies are invalid. + + + * System fit: This function is part of the Runewager command/callback/state orchestration pipeline. + + + */ + + function normalizeSshvBufferLines(bufferValue) { if (Array.isArray(bufferValue)) { return bufferValue.map((line) => escapeMarkdownFull(String(line))).slice(-SSHV_MAX_BUFFER_LINES); @@ -940,10 +1467,50 @@ function normalizeSshvBufferLines(bufferValue) { return []; } +/** + + * stringifySshvBuffer executes its scoped Runewager logic and participates in menu/command or utility flow composition. + + * Parameters: See the function signature for exact argument names and accepted values. + + * Returns: Returns the computed value or a Promise resolving to the operation result; may return void for side-effect handlers. + + * Side effects: May mutate runtime stores, pendingAction state, menu state, persistence files, logs, and callback progression. + + * Validation/safety: Uses existing guard utilities (admin checks, input checks, path checks, cooldown checks) where applicable. + + * Timeouts/fallbacks: Timeout and fallback behavior are controlled by the calling flow and global handler/state machine conventions. + + * Errors: Surfaces user-facing error replies and/or logs when inputs, permissions, or dependencies are invalid. + + * System fit: This function is part of the Runewager command/callback/state orchestration pipeline. + + */ + function stringifySshvBuffer(bufferValue) { return normalizeSshvBufferLines(bufferValue).join('\n'); } +/** + + * persistSshvSessions executes its scoped Runewager logic and participates in menu/command or utility flow composition. + + * Parameters: See the function signature for exact argument names and accepted values. + + * Returns: Returns the computed value or a Promise resolving to the operation result; may return void for side-effect handlers. + + * Side effects: May mutate runtime stores, pendingAction state, menu state, persistence files, logs, and callback progression. + + * Validation/safety: Uses existing guard utilities (admin checks, input checks, path checks, cooldown checks) where applicable. + + * Timeouts/fallbacks: Timeout and fallback behavior are controlled by the calling flow and global handler/state machine conventions. + + * Errors: Surfaces user-facing error replies and/or logs when inputs, permissions, or dependencies are invalid. + + * System fit: This function is part of the Runewager command/callback/state orchestration pipeline. + + */ + function persistSshvSessions() { const entries = []; for (const [adminId, session] of sshvSessions.entries()) { @@ -971,6 +1538,26 @@ function persistSshvSessions() { saveJson(sshvSessionsFile, entries); } +/** + + * validateAndFixSshvSession executes its scoped Runewager logic and participates in menu/command or utility flow composition. + + * Parameters: See the function signature for exact argument names and accepted values. + + * Returns: Returns the computed value or a Promise resolving to the operation result; may return void for side-effect handlers. + + * Side effects: May mutate runtime stores, pendingAction state, menu state, persistence files, logs, and callback progression. + + * Validation/safety: Uses existing guard utilities (admin checks, input checks, path checks, cooldown checks) where applicable. + + * Timeouts/fallbacks: Timeout and fallback behavior are controlled by the calling flow and global handler/state machine conventions. + + * Errors: Surfaces user-facing error replies and/or logs when inputs, permissions, or dependencies are invalid. + + * System fit: This function is part of the Runewager command/callback/state orchestration pipeline. + + */ + function validateAndFixSshvSession(user, session, { onStartup = false } = {}) { const corrections = []; if (!session || !user) return corrections; @@ -1049,6 +1636,26 @@ function validateAndFixSshvSession(user, session, { onStartup = false } = {}) { return corrections; } +/** + + * loadSshvSessions executes its scoped Runewager logic and participates in menu/command or utility flow composition. + + * Parameters: See the function signature for exact argument names and accepted values. + + * Returns: Returns the computed value or a Promise resolving to the operation result; may return void for side-effect handlers. + + * Side effects: May mutate runtime stores, pendingAction state, menu state, persistence files, logs, and callback progression. + + * Validation/safety: Uses existing guard utilities (admin checks, input checks, path checks, cooldown checks) where applicable. + + * Timeouts/fallbacks: Timeout and fallback behavior are controlled by the calling flow and global handler/state machine conventions. + + * Errors: Surfaces user-facing error replies and/or logs when inputs, permissions, or dependencies are invalid. + + * System fit: This function is part of the Runewager command/callback/state orchestration pipeline. + + */ + function loadSshvSessions() { const raw = loadJson(sshvSessionsFile, []); if (!Array.isArray(raw)) return; @@ -1097,6 +1704,26 @@ function loadSshvSessions() { } } +/** + + * refreshSshvSessionForUser executes its scoped Runewager logic and participates in menu/command or utility flow composition. + + * Parameters: See the function signature for exact argument names and accepted values. + + * Returns: Returns the computed value or a Promise resolving to the operation result; may return void for side-effect handlers. + + * Side effects: May mutate runtime stores, pendingAction state, menu state, persistence files, logs, and callback progression. + + * Validation/safety: Uses existing guard utilities (admin checks, input checks, path checks, cooldown checks) where applicable. + + * Timeouts/fallbacks: Timeout and fallback behavior are controlled by the calling flow and global handler/state machine conventions. + + * Errors: Surfaces user-facing error replies and/or logs when inputs, permissions, or dependencies are invalid. + + * System fit: This function is part of the Runewager command/callback/state orchestration pipeline. + + */ + function refreshSshvSessionForUser(user) { const existing = getSshvSession(user.id); if (existing) validateAndFixSshvSession(user, existing, { onStartup: false }); @@ -1110,6 +1737,26 @@ function refreshSshvSessionForUser(user) { return session; } +/** + + * getSshvSession executes its scoped Runewager logic and participates in menu/command or utility flow composition. + + * Parameters: See the function signature for exact argument names and accepted values. + + * Returns: Returns the computed value or a Promise resolving to the operation result; may return void for side-effect handlers. + + * Side effects: May mutate runtime stores, pendingAction state, menu state, persistence files, logs, and callback progression. + + * Validation/safety: Uses existing guard utilities (admin checks, input checks, path checks, cooldown checks) where applicable. + + * Timeouts/fallbacks: Timeout and fallback behavior are controlled by the calling flow and global handler/state machine conventions. + + * Errors: Surfaces user-facing error replies and/or logs when inputs, permissions, or dependencies are invalid. + + * System fit: This function is part of the Runewager command/callback/state orchestration pipeline. + + */ + function getSshvSession(adminId, { createIfMissing = false } = {}) { const existing = sshvSessions.get(adminId); if (existing) { @@ -1142,6 +1789,26 @@ function getSshvSession(adminId, { createIfMissing = false } = {}) { return session; } +/** + + * destroySshvSession executes its scoped Runewager logic and participates in menu/command or utility flow composition. + + * Parameters: See the function signature for exact argument names and accepted values. + + * Returns: Returns the computed value or a Promise resolving to the operation result; may return void for side-effect handlers. + + * Side effects: May mutate runtime stores, pendingAction state, menu state, persistence files, logs, and callback progression. + + * Validation/safety: Uses existing guard utilities (admin checks, input checks, path checks, cooldown checks) where applicable. + + * Timeouts/fallbacks: Timeout and fallback behavior are controlled by the calling flow and global handler/state machine conventions. + + * Errors: Surfaces user-facing error replies and/or logs when inputs, permissions, or dependencies are invalid. + + * System fit: This function is part of the Runewager command/callback/state orchestration pipeline. + + */ + function destroySshvSession(adminId) { const session = sshvSessions.get(adminId); if (!session) return; @@ -1152,12 +1819,52 @@ function destroySshvSession(adminId) { persistSshvSessions(); } +/** + + * sshvOutputText executes its scoped Runewager logic and participates in menu/command or utility flow composition. + + * Parameters: See the function signature for exact argument names and accepted values. + + * Returns: Returns the computed value or a Promise resolving to the operation result; may return void for side-effect handlers. + + * Side effects: May mutate runtime stores, pendingAction state, menu state, persistence files, logs, and callback progression. + + * Validation/safety: Uses existing guard utilities (admin checks, input checks, path checks, cooldown checks) where applicable. + + * Timeouts/fallbacks: Timeout and fallback behavior are controlled by the calling flow and global handler/state machine conventions. + + * Errors: Surfaces user-facing error replies and/or logs when inputs, permissions, or dependencies are invalid. + + * System fit: This function is part of the Runewager command/callback/state orchestration pipeline. + + */ + function sshvOutputText(text) { const raw = (text || '').trim() || '[no output yet]'; const clipped = raw.length > 3000 ? `${raw.slice(0, 3000)}\n... (truncated)` : raw; return clipped.replace(/```/g, "'''"); } +/** + + * sshvKeyboard executes its scoped Runewager logic and participates in menu/command or utility flow composition. + + * Parameters: See the function signature for exact argument names and accepted values. + + * Returns: Returns the computed value or a Promise resolving to the operation result; may return void for side-effect handlers. + + * Side effects: May mutate runtime stores, pendingAction state, menu state, persistence files, logs, and callback progression. + + * Validation/safety: Uses existing guard utilities (admin checks, input checks, path checks, cooldown checks) where applicable. + + * Timeouts/fallbacks: Timeout and fallback behavior are controlled by the calling flow and global handler/state machine conventions. + + * Errors: Surfaces user-facing error replies and/or logs when inputs, permissions, or dependencies are invalid. + + * System fit: This function is part of the Runewager command/callback/state orchestration pipeline. + + */ + function sshvKeyboard(session) { if (session.locked) { return Markup.inlineKeyboard([ @@ -1172,6 +1879,26 @@ function sshvKeyboard(session) { ]); } +/** + + * renderSshvConsole executes its scoped Runewager logic and participates in menu/command or utility flow composition. + + * Parameters: See the function signature for exact argument names and accepted values. + + * Returns: Returns the computed value or a Promise resolving to the operation result; may return void for side-effect handlers. + + * Side effects: May mutate runtime stores, pendingAction state, menu state, persistence files, logs, and callback progression. + + * Validation/safety: Uses existing guard utilities (admin checks, input checks, path checks, cooldown checks) where applicable. + + * Timeouts/fallbacks: Timeout and fallback behavior are controlled by the calling flow and global handler/state machine conventions. + + * Errors: Surfaces user-facing error replies and/or logs when inputs, permissions, or dependencies are invalid. + + * System fit: This function is part of the Runewager command/callback/state orchestration pipeline. + + */ + async function renderSshvConsole(ctx, session, note = '') { const text = [ '📟 *Runewager VPS Console*', @@ -1186,6 +1913,26 @@ async function renderSshvConsole(ctx, session, note = '') { await ctx.reply(text, { parse_mode: 'MarkdownV2', ...sshvKeyboard(session) }); } +/** + + * parseCdTarget executes its scoped Runewager logic and participates in menu/command or utility flow composition. + + * Parameters: See the function signature for exact argument names and accepted values. + + * Returns: Returns the computed value or a Promise resolving to the operation result; may return void for side-effect handlers. + + * Side effects: May mutate runtime stores, pendingAction state, menu state, persistence files, logs, and callback progression. + + * Validation/safety: Uses existing guard utilities (admin checks, input checks, path checks, cooldown checks) where applicable. + + * Timeouts/fallbacks: Timeout and fallback behavior are controlled by the calling flow and global handler/state machine conventions. + + * Errors: Surfaces user-facing error replies and/or logs when inputs, permissions, or dependencies are invalid. + + * System fit: This function is part of the Runewager command/callback/state orchestration pipeline. + + */ + function parseCdTarget(commandText) { const m = commandText.match(/^cd(?:\s+(.*))?$/); if (!m) return null; @@ -1193,29 +1940,175 @@ function parseCdTarget(commandText) { return target || '~'; } +/** + + * resolveCdPath executes its scoped Runewager logic and participates in menu/command or utility flow composition. + + * Parameters: See the function signature for exact argument names and accepted values. + + * Returns: Returns the computed value or a Promise resolving to the operation result; may return void for side-effect handlers. + + * Side effects: May mutate runtime stores, pendingAction state, menu state, persistence files, logs, and callback progression. + + * Validation/safety: Uses existing guard utilities (admin checks, input checks, path checks, cooldown checks) where applicable. + + * Timeouts/fallbacks: Timeout and fallback behavior are controlled by the calling flow and global handler/state machine conventions. + + * Errors: Surfaces user-facing error replies and/or logs when inputs, permissions, or dependencies are invalid. + + * System fit: This function is part of the Runewager command/callback/state orchestration pipeline. + + */ + function resolveCdPath(currentCwd, target) { if (target === '~') return SSHV_DEFAULT_CWD; return path.resolve(currentCwd, target); } -function commandNeedsConfirmation(commandText) { - const text = String(commandText || ''); - const patterns = [ - /\b(rm|mv|dd|shred|mkfs|chown|chmod|sudo|shutdown|reboot|kill)\b/i, - /\b(curl|wget)\b[^\n]*\|[^\n]*\bsh\b/i, - />{1,2}/, - /[;&`$(){}<>]/, - /\|/, +/** + + * commandNeedsConfirmation executes its scoped Runewager logic and participates in menu/command or utility flow composition. + + * Parameters: See the function signature for exact argument names and accepted values. + + * Returns: Returns the computed value or a Promise resolving to the operation result; may return void for side-effect handlers. + + * Side effects: May mutate runtime stores, pendingAction state, menu state, persistence files, logs, and callback progression. + + * Validation/safety: Uses existing guard utilities (admin checks, input checks, path checks, cooldown checks) where applicable. + + * Timeouts/fallbacks: Timeout and fallback behavior are controlled by the calling flow and global handler/state machine conventions. + + * Errors: Surfaces user-facing error replies and/or logs when inputs, permissions, or dependencies are invalid. + + * System fit: This function is part of the Runewager command/callback/state orchestration pipeline. + + */ + +function commandNeedsConfirmation(commandText) { + const text = String(commandText || ''); + const patterns = [ + /\b(rm|mv|dd|shred|mkfs|chown|chmod|sudo|shutdown|reboot|kill)\b/i, + /\b(curl|wget)\b[^\n]*\|[^\n]*\bsh\b/i, + />{1,2}/, + /[;&`$(){}<>]/, + /\|/, ]; return patterns.some((re) => re.test(text)); } +/** + + * commandBlocked executes its scoped Runewager logic and participates in menu/command or utility flow composition. + + * Parameters: See the function signature for exact argument names and accepted values. + + * Returns: Returns the computed value or a Promise resolving to the operation result; may return void for side-effect handlers. + + * Side effects: May mutate runtime stores, pendingAction state, menu state, persistence files, logs, and callback progression. + + * Validation/safety: Uses existing guard utilities (admin checks, input checks, path checks, cooldown checks) where applicable. + + * Timeouts/fallbacks: Timeout and fallback behavior are controlled by the calling flow and global handler/state machine conventions. + + * Errors: Surfaces user-facing error replies and/or logs when inputs, permissions, or dependencies are invalid. + + * System fit: This function is part of the Runewager command/callback/state orchestration pipeline. + + */ + function commandBlocked(commandText) { const text = String(commandText || ''); const blockedPatterns = [/(^|\s)&(\s|$)/, /&&/, /\|\|/, /\bnohup\b/i, /\bbg\b/i]; return blockedPatterns.some((re) => re.test(text)); } +/** + + * isHealthTlsEnabled executes its scoped Runewager logic and participates in menu/command or utility flow composition. + + * Parameters: See the function signature for exact argument names and accepted values. + + * Returns: Returns the computed value or a Promise resolving to the operation result; may return void for side-effect handlers. + + * Side effects: May mutate runtime stores, pendingAction state, menu state, persistence files, logs, and callback progression. + + * Validation/safety: Uses existing guard utilities (admin checks, input checks, path checks, cooldown checks) where applicable. + + * Timeouts/fallbacks: Timeout and fallback behavior are controlled by the calling flow and global handler/state machine conventions. + + * Errors: Surfaces user-facing error replies and/or logs when inputs, permissions, or dependencies are invalid. + + * System fit: This function is part of the Runewager command/callback/state orchestration pipeline. + + */ + +function isHealthTlsEnabled() { + const tlsKeyPath = process.env.HTTPS_KEY_PATH; + const tlsCertPath = process.env.HTTPS_CERT_PATH; + if (!tlsKeyPath || !tlsCertPath) return false; + try { + validateSafePath(tlsKeyPath, PROJECT_DIR); + validateSafePath(tlsCertPath, PROJECT_DIR); + return true; + } catch (_) { + return false; + } +} + +/** + + * buildSshvSandboxRestrictionHint executes its scoped Runewager logic and participates in menu/command or utility flow composition. + + * Parameters: See the function signature for exact argument names and accepted values. + + * Returns: Returns the computed value or a Promise resolving to the operation result; may return void for side-effect handlers. + + * Side effects: May mutate runtime stores, pendingAction state, menu state, persistence files, logs, and callback progression. + + * Validation/safety: Uses existing guard utilities (admin checks, input checks, path checks, cooldown checks) where applicable. + + * Timeouts/fallbacks: Timeout and fallback behavior are controlled by the calling flow and global handler/state machine conventions. + + * Errors: Surfaces user-facing error replies and/or logs when inputs, permissions, or dependencies are invalid. + + * System fit: This function is part of the Runewager command/callback/state orchestration pipeline. + + */ + +function buildSshvSandboxRestrictionHint(err) { + const msg = String((err && (err.stderr || err.message)) || '').toLowerCase(); + const code = String((err && (err.code || err.errno)) || '').toUpperCase(); + const sandboxLike = code === 'EROFS' || code === 'EACCES' + || msg.includes('read-only file system') + || msg.includes('permission denied') + || msg.includes('operation not permitted') + || msg.includes('nonewprivileges') + || msg.includes('private tmp'); + if (!sandboxLike) return null; + return 'Sandbox restriction hit. This service runs with ProtectSystem/PrivateTmp/NoNewPrivileges. Write operations are typically limited to /var/www/html/Runewager/logs and /var/www/html/Runewager/data unless runewager.service ReadWritePaths is expanded.'; +} + +/** + + * executeSshvCommand executes its scoped Runewager logic and participates in menu/command or utility flow composition. + + * Parameters: See the function signature for exact argument names and accepted values. + + * Returns: Returns the computed value or a Promise resolving to the operation result; may return void for side-effect handlers. + + * Side effects: May mutate runtime stores, pendingAction state, menu state, persistence files, logs, and callback progression. + + * Validation/safety: Uses existing guard utilities (admin checks, input checks, path checks, cooldown checks) where applicable. + + * Timeouts/fallbacks: Timeout and fallback behavior are controlled by the calling flow and global handler/state machine conventions. + + * Errors: Surfaces user-facing error replies and/or logs when inputs, permissions, or dependencies are invalid. + + * System fit: This function is part of the Runewager command/callback/state orchestration pipeline. + + */ + async function executeSshvCommand(ctx, user, session, commandText) { const command = String(commandText || '').trim(); if (!command) { @@ -1257,7 +2150,7 @@ async function executeSshvCommand(ctx, user, session, commandText) { 'Reply with the full new file content, then tap Save or Cancel.', ].join('\n'), { - parse_mode: 'MarkdownV2', + parse_mode: 'Markdown', ...Markup.inlineKeyboard([ [Markup.button.callback('💾 Save', 'sshv_editor_save'), Markup.button.callback('❌ Cancel', 'sshv_editor_cancel')], ]), @@ -1320,6 +2213,10 @@ async function executeSshvCommand(ctx, user, session, commandText) { const child = exec(command, { cwd: session.cwd, timeout: 8000, maxBuffer: 2 * 1024 * 1024 }, async (err, stdout, stderr) => { const out = `${stdout || ''}${stderr || ''}`.trim(); session.buffer = out || (err ? err.message : '[no output]'); + const sandboxHint = buildSshvSandboxRestrictionHint(err); + if (sandboxHint) session.buffer = `${session.buffer} + +${sandboxHint}`; session.runningProcess = null; session.lastActivity = Date.now(); adminLog('sshv_command_finish', { adminId: user.id, command, error: err ? err.message : null }); @@ -1447,14 +2344,74 @@ async function showBonusStatus(ctx, user) { await ctx.reply(lines.join('\n'), { parse_mode: 'Markdown', ...Markup.inlineKeyboard(kbRows) }); } +/** + + * signupButton executes its scoped Runewager logic and participates in menu/command or utility flow composition. + + * Parameters: See the function signature for exact argument names and accepted values. + + * Returns: Returns the computed value or a Promise resolving to the operation result; may return void for side-effect handlers. + + * Side effects: May mutate runtime stores, pendingAction state, menu state, persistence files, logs, and callback progression. + + * Validation/safety: Uses existing guard utilities (admin checks, input checks, path checks, cooldown checks) where applicable. + + * Timeouts/fallbacks: Timeout and fallback behavior are controlled by the calling flow and global handler/state machine conventions. + + * Errors: Surfaces user-facing error replies and/or logs when inputs, permissions, or dependencies are invalid. + + * System fit: This function is part of the Runewager command/callback/state orchestration pipeline. + + */ + function signupButton() { return Markup.button.url('🔗 Sign Up on Runewager', LINKS.runewagerSignup); } +/** + + * promoButton executes its scoped Runewager logic and participates in menu/command or utility flow composition. + + * Parameters: See the function signature for exact argument names and accepted values. + + * Returns: Returns the computed value or a Promise resolving to the operation result; may return void for side-effect handlers. + + * Side effects: May mutate runtime stores, pendingAction state, menu state, persistence files, logs, and callback progression. + + * Validation/safety: Uses existing guard utilities (admin checks, input checks, path checks, cooldown checks) where applicable. + + * Timeouts/fallbacks: Timeout and fallback behavior are controlled by the calling flow and global handler/state machine conventions. + + * Errors: Surfaces user-facing error replies and/or logs when inputs, permissions, or dependencies are invalid. + + * System fit: This function is part of the Runewager command/callback/state orchestration pipeline. + + */ + function promoButton() { return Markup.button.url('📋 Open Promo Entry Page', LINKS.promoInput); } +/** + + * miniAppPlayButton executes its scoped Runewager logic and participates in menu/command or utility flow composition. + + * Parameters: See the function signature for exact argument names and accepted values. + + * Returns: Returns the computed value or a Promise resolving to the operation result; may return void for side-effect handlers. + + * Side effects: May mutate runtime stores, pendingAction state, menu state, persistence files, logs, and callback progression. + + * Validation/safety: Uses existing guard utilities (admin checks, input checks, path checks, cooldown checks) where applicable. + + * Timeouts/fallbacks: Timeout and fallback behavior are controlled by the calling flow and global handler/state machine conventions. + + * Errors: Surfaces user-facing error replies and/or logs when inputs, permissions, or dependencies are invalid. + + * System fit: This function is part of the Runewager command/callback/state orchestration pipeline. + + */ + function miniAppPlayButton() { return Markup.button.url('🎮 Open Runewager (Mini App)', LINKS.miniAppPlay); } @@ -1553,6 +2510,26 @@ function mainMenuKeyboard(isAdminUser = false, page = 1, user = null) { return Markup.inlineKeyboard(rows); } +/** + + * ageGateKeyboard executes its scoped Runewager logic and participates in menu/command or utility flow composition. + + * Parameters: See the function signature for exact argument names and accepted values. + + * Returns: Returns the computed value or a Promise resolving to the operation result; may return void for side-effect handlers. + + * Side effects: May mutate runtime stores, pendingAction state, menu state, persistence files, logs, and callback progression. + + * Validation/safety: Uses existing guard utilities (admin checks, input checks, path checks, cooldown checks) where applicable. + + * Timeouts/fallbacks: Timeout and fallback behavior are controlled by the calling flow and global handler/state machine conventions. + + * Errors: Surfaces user-facing error replies and/or logs when inputs, permissions, or dependencies are invalid. + + * System fit: This function is part of the Runewager command/callback/state orchestration pipeline. + + */ + function ageGateKeyboard() { // Note: parse_mode: 'Markdown' must be passed with this keyboard since COPY.ageGate uses Markdown return Markup.inlineKeyboard([ @@ -1561,6 +2538,26 @@ function ageGateKeyboard() { ]); } +/** + + * linkPrefKeyboard executes its scoped Runewager logic and participates in menu/command or utility flow composition. + + * Parameters: See the function signature for exact argument names and accepted values. + + * Returns: Returns the computed value or a Promise resolving to the operation result; may return void for side-effect handlers. + + * Side effects: May mutate runtime stores, pendingAction state, menu state, persistence files, logs, and callback progression. + + * Validation/safety: Uses existing guard utilities (admin checks, input checks, path checks, cooldown checks) where applicable. + + * Timeouts/fallbacks: Timeout and fallback behavior are controlled by the calling flow and global handler/state machine conventions. + + * Errors: Surfaces user-facing error replies and/or logs when inputs, permissions, or dependencies are invalid. + + * System fit: This function is part of the Runewager command/callback/state orchestration pipeline. + + */ + function linkPrefKeyboard() { return Markup.inlineKeyboard([ [Markup.button.url('🎮 Open Mini App', LINKS.miniAppPlay)], @@ -1569,6 +2566,26 @@ function linkPrefKeyboard() { ]); } +/** + + * adminKeyboard executes its scoped Runewager logic and participates in menu/command or utility flow composition. + + * Parameters: See the function signature for exact argument names and accepted values. + + * Returns: Returns the computed value or a Promise resolving to the operation result; may return void for side-effect handlers. + + * Side effects: May mutate runtime stores, pendingAction state, menu state, persistence files, logs, and callback progression. + + * Validation/safety: Uses existing guard utilities (admin checks, input checks, path checks, cooldown checks) where applicable. + + * Timeouts/fallbacks: Timeout and fallback behavior are controlled by the calling flow and global handler/state machine conventions. + + * Errors: Surfaces user-facing error replies and/or logs when inputs, permissions, or dependencies are invalid. + + * System fit: This function is part of the Runewager command/callback/state orchestration pipeline. + + */ + function adminKeyboard() { return Markup.inlineKeyboard([ [Markup.button.callback('📄 View Promo', 'admin_view')], @@ -1582,139 +2599,520 @@ function adminKeyboard() { ]); } +/** + + * promoText executes its scoped Runewager logic and participates in menu/command or utility flow composition. + + * Parameters: See the function signature for exact argument names and accepted values. + + * Returns: Returns the computed value or a Promise resolving to the operation result; may return void for side-effect handlers. + + * Side effects: May mutate runtime stores, pendingAction state, menu state, persistence files, logs, and callback progression. + + * Validation/safety: Uses existing guard utilities (admin checks, input checks, path checks, cooldown checks) where applicable. + + * Timeouts/fallbacks: Timeout and fallback behavior are controlled by the calling flow and global handler/state machine conventions. + + * Errors: Surfaces user-facing error replies and/or logs when inputs, permissions, or dependencies are invalid. + + * System fit: This function is part of the Runewager command/callback/state orchestration pipeline. + + */ + function promoText() { - return `🎁 NEW USER BONUS — ONCE PER ACCOUNT/IP\nPromo Code: ${promoStore.code}\nAmount: ${promoStore.amountSC} SC each\nRemaining Claims: ${promoStore.remainingClaims}\nTotal Claims: ${promoStore.totalClaimLimit}\n\n${promoStore.bonusRule}`; + return promoManagerSummaryText ? promoManagerSummaryText() : 'Promo Manager enabled.'; } -function normalizePromoCodeEntry(raw, idHint = null) { +/** + + * normalizePromoManagerPromo executes its scoped Runewager logic and participates in menu/command or utility flow composition. + + * Parameters: See the function signature for exact argument names and accepted values. + + * Returns: Returns the computed value or a Promise resolving to the operation result; may return void for side-effect handlers. + + * Side effects: May mutate runtime stores, pendingAction state, menu state, persistence files, logs, and callback progression. + + * Validation/safety: Uses existing guard utilities (admin checks, input checks, path checks, cooldown checks) where applicable. + + * Timeouts/fallbacks: Timeout and fallback behavior are controlled by the calling flow and global handler/state machine conventions. + + * Errors: Surfaces user-facing error replies and/or logs when inputs, permissions, or dependencies are invalid. + + * System fit: This function is part of the Runewager command/callback/state orchestration pipeline. + + */ + +function normalizePromoManagerPromo(raw, idHint = null) { if (!raw || typeof raw !== 'object') return null; - const code = String(raw.code || '').trim(); - if (!code) return null; - const audience = raw.audience === PROMO_AUDIENCE.EXISTING_USER - ? PROMO_AUDIENCE.EXISTING_USER - : PROMO_AUDIENCE.NEW_USER; - const requirementType = raw.requirementType === PROMO_REQUIREMENT_TYPE.NO_REQUIREMENT - ? PROMO_REQUIREMENT_TYPE.NO_REQUIREMENT - : PROMO_REQUIREMENT_TYPE.WAGER; - const parsedAmount = Number(raw.amountSC); - const amountSC = Number.isFinite(parsedAmount) && parsedAmount > 0 ? parsedAmount : 0; - const parsedId = Number(raw.id); - const id = Number.isFinite(parsedId) && parsedId > 0 ? parsedId : Number(idHint || 0); - if (!id) return null; + const parsedId = Number(raw.promo_id); + const promoId = Number.isFinite(parsedId) && parsedId > 0 ? parsedId : Number(idHint || 0); + if (!promoId) return null; + const requirementType = Object.values(PROMO_REQUIREMENT).includes(raw.requirement_type) + ? raw.requirement_type + : PROMO_REQUIREMENT.NEW_USER_ONLY; return { - id, - code, - audience, - requirementType, - amountSC, - notes: typeof raw.notes === 'string' ? raw.notes.trim() : '', - active: raw.active !== false, - createdAt: Number(raw.createdAt) || Date.now(), - updatedAt: Number(raw.updatedAt) || Date.now(), + promo_id: promoId, + name: String(raw.name || '').trim() || `Promo #${promoId}`, + code_or_link: String(raw.code_or_link || '').trim(), + casino_base_url: String(raw.casino_base_url || '').trim(), + description: String(raw.description || '').trim(), + image_url: String(raw.image_url || '').trim(), + requirement_type: requirementType, + required_wager_amount: Number(raw.required_wager_amount) || 0, + required_wager_days: Number(raw.required_wager_days) || 0, + new_user_only: requirementType === PROMO_REQUIREMENT.NEW_USER_ONLY, + existing_user_only: requirementType !== PROMO_REQUIREMENT.NEW_USER_ONLY, + claim_limit: Number(raw.claim_limit) > 0 ? Number(raw.claim_limit) : null, + cooldown_hours: Number(raw.cooldown_hours) > 0 ? Number(raw.cooldown_hours) : 0, + auto_approve: raw.auto_approve !== false, + created_by: Number(raw.created_by) || 0, + created_at: Number(raw.created_at) || Date.now(), + updated_at: Number(raw.updated_at) || Date.now(), + status: Object.values(PROMO_STATUS).includes(raw.status) ? raw.status : PROMO_STATUS.ACTIVE, }; } -function getActivePromoCodeForUser(user) { - const wantsNewUser = !user.claimedPromo; - const audience = wantsNewUser ? PROMO_AUDIENCE.NEW_USER : PROMO_AUDIENCE.EXISTING_USER; - const candidates = promoCodeStore.codes - .filter((code) => code.active && code.audience === audience) - .sort((a, b) => b.updatedAt - a.updatedAt); - return candidates[0] || null; -} +/** -function getPromoClaimCooldownRemainingMs(user) { - if (!user.lastAnyPromoClaimAt) return 0; - const elapsed = Date.now() - Number(user.lastAnyPromoClaimAt || 0); - return Math.max(0, PROMO_GLOBAL_COOLDOWN_MS - elapsed); -} + * savePromoManagerDb executes its scoped Runewager logic and participates in menu/command or utility flow composition. -function formatCooldown(ms) { - const totalMinutes = Math.ceil(ms / 60000); - const hours = Math.floor(totalMinutes / 60); - const minutes = totalMinutes % 60; - if (hours <= 0) return `${minutes}m`; - return `${hours}h ${minutes}m`; -} + * Parameters: See the function signature for exact argument names and accepted values. -function formatPromoCodeLine(code) { - const audience = code.audience === PROMO_AUDIENCE.NEW_USER ? 'new-user' : 'existing-user'; - const requirement = code.requirementType === PROMO_REQUIREMENT_TYPE.NO_REQUIREMENT ? 'no-requirement' : 'wager'; - return `#${code.id} | ${code.code} | ${audience} | ${requirement} | ${code.active ? 'active' : 'paused'}`; -} + * Returns: Returns the computed value or a Promise resolving to the operation result; may return void for side-effect handlers. -function promoCodeManagerText() { - const lines = promoCodeStore.codes - .slice() - .sort((a, b) => a.id - b.id) - .map((code) => `• ${formatPromoCodeLine(code)}`); - return [ - '🧩 *Promo Code Manager*', - '', - '*Global Rules:*', - '• User must be registered under *GambleCodez* affiliate', - '• 24h cooldown between any promo claims', - '', - '*Configured Codes:*', - lines.length ? lines.join('\n') : 'No promo codes configured yet.', - '', - 'Use buttons below to add or toggle codes.', - ].join('\n'); -} + * Side effects: May mutate runtime stores, pendingAction state, menu state, persistence files, logs, and callback progression. + * Validation/safety: Uses existing guard utilities (admin checks, input checks, path checks, cooldown checks) where applicable. -function wager30AdminKeyboard() { - return Markup.inlineKeyboard([ - [Markup.button.callback('📋 Open (Pending) Requests', 'w30_admin_pending')], - [Markup.button.callback('✅ Completed Requests', 'w30_admin_completed')], - [Markup.button.callback('👍 Approve Request', 'w30_admin_approve_pick')], - [Markup.button.callback('❌ Deny Request', 'w30_admin_deny_pick')], - [Markup.button.callback('📤 Mark Tip Sent', 'w30_admin_sent_pick')], - [Markup.button.callback('🔗 Add Username Manually', 'w30_admin_link_username')], - [Markup.button.callback('➕ Add Manual Record (bonus_sent)', 'w30_admin_add_pick')], - [Markup.button.callback('🔍 Lookup User', 'w30_admin_lookup')], - [Markup.button.callback('📊 Stats', 'w30_admin_stats')], - [Markup.button.callback('♻️ Reset User', 'w30_admin_reset')], - [Markup.button.callback('⬅️ Return to Admin Menu', 'open_admin_dashboard')], - ]); -} + * Timeouts/fallbacks: Timeout and fallback behavior are controlled by the calling flow and global handler/state machine conventions. -function wagerReminderKeyboard() { - return Markup.inlineKeyboard([ - [Markup.button.callback('🎯 Request My 30 SC Bonus', 'w30_request_start')], - [Markup.button.callback('ℹ️ Tell me more about the 30 SC bonus', 'w30_bonus_info')], - [Markup.button.callback('📋 Bonus Rules', 'w30_rules')], - ]); + * Errors: Surfaces user-facing error replies and/or logs when inputs, permissions, or dependencies are invalid. + + * System fit: This function is part of the Runewager command/callback/state orchestration pipeline. + + */ + +function savePromoManagerDb() { + saveJson(promoManagerDbFile, promoManagerStore); } /** - * Build the settings submenu keyboard for a given user. + + * loadPromoManagerDb executes its scoped Runewager logic and participates in menu/command or utility flow composition. + + * Parameters: See the function signature for exact argument names and accepted values. + + * Returns: Returns the computed value or a Promise resolving to the operation result; may return void for side-effect handlers. + + * Side effects: May mutate runtime stores, pendingAction state, menu state, persistence files, logs, and callback progression. + + * Validation/safety: Uses existing guard utilities (admin checks, input checks, path checks, cooldown checks) where applicable. + + * Timeouts/fallbacks: Timeout and fallback behavior are controlled by the calling flow and global handler/state machine conventions. + + * Errors: Surfaces user-facing error replies and/or logs when inputs, permissions, or dependencies are invalid. + + * System fit: This function is part of the Runewager command/callback/state orchestration pipeline. + */ -function settingsKeyboard(user) { - const s = user.settings; - const playLabel = s.playMode === 'browser' - ? '🌐 Play Mode: Browser (tap to switch)' - : '📱 Play Mode: Mini App (tap to switch)'; - const quickLabel = s.showQuickCommands - ? '✅ Quick Commands: On (tap to turn off)' - : '☐ Quick Commands: Off (tap to turn on)'; - const tooltipLabel = s.tooltipsEnabled - ? '✅ Tooltips: On (tap to turn off)' - : '☐ Tooltips: Off (tap to turn on)'; - return Markup.inlineKeyboard([ - [Markup.button.callback(playLabel, 'settings_toggle_playmode')], - [Markup.button.callback(quickLabel, 'settings_toggle_quick_commands')], - [Markup.button.callback(tooltipLabel, 'settings_toggle_tooltips')], - [Markup.button.callback('⬅️ Back to Menu', 'to_main_menu')], - ]); +function loadPromoManagerDb() { + const raw = loadJson(promoManagerDbFile, null); + if (!raw || typeof raw !== 'object') return; + promoManagerStore.nextPromoId = Math.max(1, Number(raw.nextPromoId) || 1); + promoManagerStore.nextClaimId = Math.max(1, Number(raw.nextClaimId) || 1); + promoManagerStore.promos = Array.isArray(raw.promos) + ? raw.promos.map((promo) => normalizePromoManagerPromo(promo, promo && promo.promo_id)).filter(Boolean) + : []; + promoManagerStore.claims = Array.isArray(raw.claims) ? raw.claims : []; + promoManagerStore.eligibilityFailures = Array.isArray(raw.eligibilityFailures) ? raw.eligibilityFailures.slice(-2000) : []; + const maxPromoId = promoManagerStore.promos.reduce((acc, promo) => Math.max(acc, Number(promo.promo_id) || 0), 0); + promoManagerStore.nextPromoId = Math.max(promoManagerStore.nextPromoId, maxPromoId + 1); + const maxClaimId = promoManagerStore.claims.reduce((acc, claim) => Math.max(acc, Number(claim.claim_id) || 0), 0); + promoManagerStore.nextClaimId = Math.max(promoManagerStore.nextClaimId, maxClaimId + 1); } -// ── Admin dashboard keyboards ────────────────────────────────────────────── +/** -function adminDashboardKeyboard(page = 1) { - if (page === 2) { - return Markup.inlineKeyboard([ + * listActivePromos executes its scoped Runewager logic and participates in menu/command or utility flow composition. + + * Parameters: See the function signature for exact argument names and accepted values. + + * Returns: Returns the computed value or a Promise resolving to the operation result; may return void for side-effect handlers. + + * Side effects: May mutate runtime stores, pendingAction state, menu state, persistence files, logs, and callback progression. + + * Validation/safety: Uses existing guard utilities (admin checks, input checks, path checks, cooldown checks) where applicable. + + * Timeouts/fallbacks: Timeout and fallback behavior are controlled by the calling flow and global handler/state machine conventions. + + * Errors: Surfaces user-facing error replies and/or logs when inputs, permissions, or dependencies are invalid. + + * System fit: This function is part of the Runewager command/callback/state orchestration pipeline. + + */ + +function listActivePromos() { + return promoManagerStore.promos.filter((promo) => promo.status === PROMO_STATUS.ACTIVE); +} + +/** + + * getPromoById executes its scoped Runewager logic and participates in menu/command or utility flow composition. + + * Parameters: See the function signature for exact argument names and accepted values. + + * Returns: Returns the computed value or a Promise resolving to the operation result; may return void for side-effect handlers. + + * Side effects: May mutate runtime stores, pendingAction state, menu state, persistence files, logs, and callback progression. + + * Validation/safety: Uses existing guard utilities (admin checks, input checks, path checks, cooldown checks) where applicable. + + * Timeouts/fallbacks: Timeout and fallback behavior are controlled by the calling flow and global handler/state machine conventions. + + * Errors: Surfaces user-facing error replies and/or logs when inputs, permissions, or dependencies are invalid. + + * System fit: This function is part of the Runewager command/callback/state orchestration pipeline. + + */ + +function getPromoById(promoId) { + return promoManagerStore.promos.find((promo) => promo.promo_id === Number(promoId)) || null; +} + +/** + + * hasRedeemedAnyPromo executes its scoped Runewager logic and participates in menu/command or utility flow composition. + + * Parameters: See the function signature for exact argument names and accepted values. + + * Returns: Returns the computed value or a Promise resolving to the operation result; may return void for side-effect handlers. + + * Side effects: May mutate runtime stores, pendingAction state, menu state, persistence files, logs, and callback progression. + + * Validation/safety: Uses existing guard utilities (admin checks, input checks, path checks, cooldown checks) where applicable. + + * Timeouts/fallbacks: Timeout and fallback behavior are controlled by the calling flow and global handler/state machine conventions. + + * Errors: Surfaces user-facing error replies and/or logs when inputs, permissions, or dependencies are invalid. + + * System fit: This function is part of the Runewager command/callback/state orchestration pipeline. + + */ + +function hasRedeemedAnyPromo(userId) { + return promoManagerStore.claims.some((claim) => claim.user_id === Number(userId) && ['claimed', 'approved'].includes(claim.status)); +} + +/** + + * countPromoClaims executes its scoped Runewager logic and participates in menu/command or utility flow composition. + + * Parameters: See the function signature for exact argument names and accepted values. + + * Returns: Returns the computed value or a Promise resolving to the operation result; may return void for side-effect handlers. + + * Side effects: May mutate runtime stores, pendingAction state, menu state, persistence files, logs, and callback progression. + + * Validation/safety: Uses existing guard utilities (admin checks, input checks, path checks, cooldown checks) where applicable. + + * Timeouts/fallbacks: Timeout and fallback behavior are controlled by the calling flow and global handler/state machine conventions. + + * Errors: Surfaces user-facing error replies and/or logs when inputs, permissions, or dependencies are invalid. + + * System fit: This function is part of the Runewager command/callback/state orchestration pipeline. + + */ + +function countPromoClaims(promoId) { + return promoManagerStore.claims.filter((claim) => claim.promo_id === Number(promoId) && ['claimed', 'approved'].includes(claim.status)).length; +} + +/** + + * getRollingWagerForUser executes its scoped Runewager logic and participates in menu/command or utility flow composition. + + * Parameters: See the function signature for exact argument names and accepted values. + + * Returns: Returns the computed value or a Promise resolving to the operation result; may return void for side-effect handlers. + + * Side effects: May mutate runtime stores, pendingAction state, menu state, persistence files, logs, and callback progression. + + * Validation/safety: Uses existing guard utilities (admin checks, input checks, path checks, cooldown checks) where applicable. + + * Timeouts/fallbacks: Timeout and fallback behavior are controlled by the calling flow and global handler/state machine conventions. + + * Errors: Surfaces user-facing error replies and/or logs when inputs, permissions, or dependencies are invalid. + + * System fit: This function is part of the Runewager command/callback/state orchestration pipeline. + + */ + +async function getRollingWagerForUser(user, days) { + if (Number(days) === 7 && user.wagerBonus && Number(user.wagerBonus.wager7dayTotal) > 0) return Number(user.wagerBonus.wager7dayTotal); + return Number(user.wagerBonus && user.wagerBonus.wager7dayTotal) || 0; +} + +/** + + * evaluatePromoEligibility executes its scoped Runewager logic and participates in menu/command or utility flow composition. + + * Parameters: See the function signature for exact argument names and accepted values. + + * Returns: Returns the computed value or a Promise resolving to the operation result; may return void for side-effect handlers. + + * Side effects: May mutate runtime stores, pendingAction state, menu state, persistence files, logs, and callback progression. + + * Validation/safety: Uses existing guard utilities (admin checks, input checks, path checks, cooldown checks) where applicable. + + * Timeouts/fallbacks: Timeout and fallback behavior are controlled by the calling flow and global handler/state machine conventions. + + * Errors: Surfaces user-facing error replies and/or logs when inputs, permissions, or dependencies are invalid. + + * System fit: This function is part of the Runewager command/callback/state orchestration pipeline. + + */ + +async function evaluatePromoEligibility(user, promo, { logFailure = false } = {}) { + const reasons = []; + const userId = Number(user.id); + const anyRedeemed = hasRedeemedAnyPromo(userId); + if (promo.new_user_only && (anyRedeemed || user.hasClaimedNewUserPromo)) reasons.push('New-user promo only. You already redeemed a promo.'); + if (promo.existing_user_only && !promo.new_user_only && !anyRedeemed) reasons.push('This promo is only for existing promo users.'); + if (promo.requirement_type === PROMO_REQUIREMENT.EXISTING_USER_WITH_WAGER) { + const wager = await getRollingWagerForUser(user, promo.required_wager_days || 7); + if (wager < Number(promo.required_wager_amount || 0)) { + reasons.push(`Requires ${promo.required_wager_amount} SC wager in ${promo.required_wager_days} days.`); + } + } + if (promo.claim_limit && countPromoClaims(promo.promo_id) >= promo.claim_limit) reasons.push('Claim limit reached for this promo.'); + const recentClaim = promoManagerStore.claims + .filter((claim) => claim.promo_id === promo.promo_id && claim.user_id === userId && ['claimed', 'approved'].includes(claim.status)) + .sort((a, b) => Number(b.claimed_at || 0) - Number(a.claimed_at || 0))[0]; + if (promo.cooldown_hours > 0 && recentClaim) { + const remaining = (promo.cooldown_hours * 3600 * 1000) - (Date.now() - Number(recentClaim.claimed_at || 0)); + if (remaining > 0) reasons.push(`Cooldown active (${formatCooldown(remaining)} remaining).`); + } + if (reasons.length && logFailure) { + promoManagerStore.eligibilityFailures.push({ promo_id: promo.promo_id, user_id: userId, reasons, ts: Date.now() }); + if (promoManagerStore.eligibilityFailures.length > 3000) promoManagerStore.eligibilityFailures = promoManagerStore.eligibilityFailures.slice(-3000); + } + return { eligible: reasons.length === 0, reasons }; +} + +/** + + * renderPromoForUser executes its scoped Runewager logic and participates in menu/command or utility flow composition. + + * Parameters: See the function signature for exact argument names and accepted values. + + * Returns: Returns the computed value or a Promise resolving to the operation result; may return void for side-effect handlers. + + * Side effects: May mutate runtime stores, pendingAction state, menu state, persistence files, logs, and callback progression. + + * Validation/safety: Uses existing guard utilities (admin checks, input checks, path checks, cooldown checks) where applicable. + + * Timeouts/fallbacks: Timeout and fallback behavior are controlled by the calling flow and global handler/state machine conventions. + + * Errors: Surfaces user-facing error replies and/or logs when inputs, permissions, or dependencies are invalid. + + * System fit: This function is part of the Runewager command/callback/state orchestration pipeline. + + */ + +function renderPromoForUser(promo) { + return [ + `🎁 *${promo.name}*`, + promo.description || '_No description provided._', + '', + `Code/Link: ${promo.code_or_link}`, + `Casino: ${promo.casino_base_url}`, + `Requirement: ${promo.requirement_type}`, + promo.claim_limit ? `Claim limit: ${promo.claim_limit}` : 'Claim limit: none', + promo.cooldown_hours ? `Cooldown: ${promo.cooldown_hours}h` : 'Cooldown: none', + `Approval: ${promo.auto_approve ? 'Automatic' : 'Admin review'}`, + ].join('\n'); +} + + +/** + + + * getActivePromoCodeForUser executes its scoped Runewager logic and participates in menu/command or utility flow composition. + + + * Parameters: See the function signature for exact argument names and accepted values. + + + * Returns: Returns the computed value or a Promise resolving to the operation result; may return void for side-effect handlers. + + + * Side effects: May mutate runtime stores, pendingAction state, menu state, persistence files, logs, and callback progression. + + + * Validation/safety: Uses existing guard utilities (admin checks, input checks, path checks, cooldown checks) where applicable. + + + * Timeouts/fallbacks: Timeout and fallback behavior are controlled by the calling flow and global handler/state machine conventions. + + + * Errors: Surfaces user-facing error replies and/or logs when inputs, permissions, or dependencies are invalid. + + + * System fit: This function is part of the Runewager command/callback/state orchestration pipeline. + + + */ + + +function getActivePromoCodeForUser(user) { + const promos = listActivePromos(); + const first = promos[0] || null; + if (!first) return null; + return { + id: first.promo_id, + code: first.code_or_link, + audience: first.new_user_only ? PROMO_AUDIENCE.NEW_USER : PROMO_AUDIENCE.EXISTING_USER, + requirementType: first.requirement_type === PROMO_REQUIREMENT.EXISTING_USER_WITH_WAGER ? PROMO_REQUIREMENT_TYPE.WAGER : PROMO_REQUIREMENT_TYPE.NO_REQUIREMENT, + }; +} + + + +/** + + + + * wager30AdminKeyboard executes its scoped Runewager logic and participates in menu/command or utility flow composition. + + + + * Parameters: See the function signature for exact argument names and accepted values. + + + + * Returns: Returns the computed value or a Promise resolving to the operation result; may return void for side-effect handlers. + + + + * Side effects: May mutate runtime stores, pendingAction state, menu state, persistence files, logs, and callback progression. + + + + * Validation/safety: Uses existing guard utilities (admin checks, input checks, path checks, cooldown checks) where applicable. + + + + * Timeouts/fallbacks: Timeout and fallback behavior are controlled by the calling flow and global handler/state machine conventions. + + + + * Errors: Surfaces user-facing error replies and/or logs when inputs, permissions, or dependencies are invalid. + + + + * System fit: This function is part of the Runewager command/callback/state orchestration pipeline. + + + + */ + + + +function wager30AdminKeyboard() { + return Markup.inlineKeyboard([ + [Markup.button.callback('📋 Open (Pending) Requests', 'w30_admin_pending')], + [Markup.button.callback('✅ Completed Requests', 'w30_admin_completed')], + [Markup.button.callback('👍 Approve Request', 'w30_admin_approve_pick')], + [Markup.button.callback('❌ Deny Request', 'w30_admin_deny_pick')], + [Markup.button.callback('📤 Mark Tip Sent', 'w30_admin_sent_pick')], + [Markup.button.callback('🔗 Add Username Manually', 'w30_admin_link_username')], + [Markup.button.callback('➕ Add Manual Record (bonus_sent)', 'w30_admin_add_pick')], + [Markup.button.callback('🔍 Lookup User', 'w30_admin_lookup')], + [Markup.button.callback('📊 Stats', 'w30_admin_stats')], + [Markup.button.callback('♻️ Reset User', 'w30_admin_reset')], + [Markup.button.callback('⬅️ Return to Admin Menu', 'open_admin_dashboard')], + ]); +} + +/** + + * wagerReminderKeyboard executes its scoped Runewager logic and participates in menu/command or utility flow composition. + + * Parameters: See the function signature for exact argument names and accepted values. + + * Returns: Returns the computed value or a Promise resolving to the operation result; may return void for side-effect handlers. + + * Side effects: May mutate runtime stores, pendingAction state, menu state, persistence files, logs, and callback progression. + + * Validation/safety: Uses existing guard utilities (admin checks, input checks, path checks, cooldown checks) where applicable. + + * Timeouts/fallbacks: Timeout and fallback behavior are controlled by the calling flow and global handler/state machine conventions. + + * Errors: Surfaces user-facing error replies and/or logs when inputs, permissions, or dependencies are invalid. + + * System fit: This function is part of the Runewager command/callback/state orchestration pipeline. + + */ + +function wagerReminderKeyboard() { + return Markup.inlineKeyboard([ + [Markup.button.callback('🎯 Request My 30 SC Bonus', 'w30_request_start')], + [Markup.button.callback('ℹ️ Tell me more about the 30 SC bonus', 'w30_bonus_info')], + [Markup.button.callback('📋 Bonus Rules', 'w30_rules')], + ]); +} + +/** + * Build the settings submenu keyboard for a given user. + */ +function settingsKeyboard(user) { + const s = user.settings; + const playLabel = s.playMode === 'browser' + ? '🌐 Play Mode: Browser (tap to switch)' + : '📱 Play Mode: Mini App (tap to switch)'; + const quickLabel = s.showQuickCommands + ? '✅ Quick Commands: On (tap to turn off)' + : '☐ Quick Commands: Off (tap to turn on)'; + const tooltipLabel = s.tooltipsEnabled + ? '✅ Tooltips: On (tap to turn off)' + : '☐ Tooltips: Off (tap to turn on)'; + + return Markup.inlineKeyboard([ + [Markup.button.callback(playLabel, 'settings_toggle_playmode')], + [Markup.button.callback(quickLabel, 'settings_toggle_quick_commands')], + [Markup.button.callback(tooltipLabel, 'settings_toggle_tooltips')], + [Markup.button.callback('⬅️ Back to Menu', 'to_main_menu')], + [Markup.button.callback('❌ Cancel', 'to_main_menu')], + ]); +} + +// ── Admin dashboard keyboards ────────────────────────────────────────────── + +/** + + * adminDashboardKeyboard executes its scoped Runewager logic and participates in menu/command or utility flow composition. + + * Parameters: See the function signature for exact argument names and accepted values. + + * Returns: Returns the computed value or a Promise resolving to the operation result; may return void for side-effect handlers. + + * Side effects: May mutate runtime stores, pendingAction state, menu state, persistence files, logs, and callback progression. + + * Validation/safety: Uses existing guard utilities (admin checks, input checks, path checks, cooldown checks) where applicable. + + * Timeouts/fallbacks: Timeout and fallback behavior are controlled by the calling flow and global handler/state machine conventions. + + * Errors: Surfaces user-facing error replies and/or logs when inputs, permissions, or dependencies are invalid. + + * System fit: This function is part of the Runewager command/callback/state orchestration pipeline. + + */ + +function adminDashboardKeyboard(page = 1) { + if (page === 2) { + return Markup.inlineKeyboard([ [ Markup.button.callback('🎁 Start Giveaway', 'admin_cmd_start_giveaway'), Markup.button.callback('📊 Giveaway Status', 'admin_cmd_giveaway_status'), @@ -1741,7 +3139,7 @@ function adminDashboardKeyboard(page = 1) { return Markup.inlineKeyboard([ [ Markup.button.callback('🎉 Giveaway Tools', 'admin_cat_giveaway'), - Markup.button.callback('📣 Promo Tools', 'admin_cat_promo'), + Markup.button.callback('🎛 Promo Manager', 'admin_cat_promo'), ], [ Markup.button.callback('👤 User Tools', 'admin_cat_user'), @@ -1749,10 +3147,10 @@ function adminDashboardKeyboard(page = 1) { ], [ Markup.button.callback('🛟 Support Tools', 'admin_cat_support'), - Markup.button.callback('📈 Stats', 'admin_stats_menu'), + Markup.button.callback('🧪 Tests & Bugs', 'admin_cat_tests'), ], [ - Markup.button.callback('📄 Admin Help', 'help_tab_admin'), + Markup.button.callback('📈 Stats', 'admin_stats_menu'), Markup.button.callback('▶️ Quick Actions', 'admin_dash_page_2'), ], [Markup.button.callback('⬅️ Back to User Menu', 'to_main_menu')], @@ -1771,34 +3169,98 @@ function adminStatsKeyboard() { Markup.button.callback('♾️ Lifetime', 'admin_stats_lifetime'), ], [Markup.button.callback('⬅️ Admin Dashboard', 'open_admin_dashboard')], + [Markup.button.callback('🏠 Main Menu', 'to_main_menu'), Markup.button.callback('❌ Cancel', 'to_main_menu')], ]); } +/** + + * adminGiveawayToolsKeyboard executes its scoped Runewager logic and participates in menu/command or utility flow composition. + + * Parameters: See the function signature for exact argument names and accepted values. + + * Returns: Returns the computed value or a Promise resolving to the operation result; may return void for side-effect handlers. + + * Side effects: May mutate runtime stores, pendingAction state, menu state, persistence files, logs, and callback progression. + + * Validation/safety: Uses existing guard utilities (admin checks, input checks, path checks, cooldown checks) where applicable. + + * Timeouts/fallbacks: Timeout and fallback behavior are controlled by the calling flow and global handler/state machine conventions. + + * Errors: Surfaces user-facing error replies and/or logs when inputs, permissions, or dependencies are invalid. + + * System fit: This function is part of the Runewager command/callback/state orchestration pipeline. + + */ + function adminGiveawayToolsKeyboard() { return Markup.inlineKeyboard([ [Markup.button.callback('🎁 Start Giveaway', 'admin_cmd_start_giveaway')], [Markup.button.callback('🧪 Test Giveaway', 'admin_cmd_testgiveaway')], [Markup.button.callback('📊 Giveaway Status', 'admin_cmd_giveaway_status')], [Markup.button.callback('⬅️ Admin Dashboard', 'open_admin_dashboard')], + [Markup.button.callback('🏠 Main Menu', 'to_main_menu'), Markup.button.callback('❌ Cancel', 'to_main_menu')], ]); } -function adminPromoToolsKeyboard() { +/** + + * adminPromoToolsKeyboard executes its scoped Runewager logic and participates in menu/command or utility flow composition. + + * Parameters: See the function signature for exact argument names and accepted values. + + * Returns: Returns the computed value or a Promise resolving to the operation result; may return void for side-effect handlers. + + * Side effects: May mutate runtime stores, pendingAction state, menu state, persistence files, logs, and callback progression. + + * Validation/safety: Uses existing guard utilities (admin checks, input checks, path checks, cooldown checks) where applicable. + + * Timeouts/fallbacks: Timeout and fallback behavior are controlled by the calling flow and global handler/state machine conventions. + + * Errors: Surfaces user-facing error replies and/or logs when inputs, permissions, or dependencies are invalid. + + * System fit: This function is part of the Runewager command/callback/state orchestration pipeline. + + */ + +function adminPromoToolsKeyboard() { return Markup.inlineKeyboard([ - [Markup.button.callback('📄 View Promo', 'admin_view')], - [Markup.button.callback('🧩 Manage Promo Codes', 'admin_manage_promo_codes')], - [Markup.button.callback('➕ Add Promo Code', 'admin_promo_code_add')], - [Markup.button.callback('✏️ Edit Promo Code', 'admin_edit_code')], - [Markup.button.callback('💰 Edit SC Amount', 'admin_edit_amount')], - [Markup.button.callback('🔢 Edit Claim Limit', 'admin_edit_limit')], - [Markup.button.callback('⏸ Pause Promo', 'admin_pause'), Markup.button.callback('▶️ Unpause', 'admin_unpause')], - [Markup.button.callback('📢 Broadcast Update', 'admin_broadcast')], + [Markup.button.callback('🎛 Promo Manager', 'admin_promo_manager')], + [Markup.button.callback('ℹ️ Promo Step Guide', 'admin_pm_help')], + [Markup.button.callback('➕ Create Promo', 'admin_pm_create')], + [Markup.button.callback('✏️ Edit Promo', 'admin_pm_edit')], + [Markup.button.callback('⏸ Pause/Unpause', 'admin_pm_pause_toggle')], + [Markup.button.callback('🗑 Delete Promo', 'admin_pm_delete')], + [Markup.button.callback('📊 Promo Stats', 'admin_pm_stats')], + [Markup.button.callback('👀 Preview Promo', 'admin_pm_preview')], + [Markup.button.callback('🧾 Approval Queue', 'admin_pm_queue')], [Markup.button.callback('📣 Announcements', 'admin_cmd_announce_start')], - [Markup.button.callback('💡 Tips Manager', 'admin_cmd_tips_dashboard')], + [Markup.button.callback('💡 Content Drops', 'admin_cmd_tips_dashboard')], [Markup.button.callback('⬅️ Admin Dashboard', 'open_admin_dashboard')], + [Markup.button.callback('🏠 Main Menu', 'to_main_menu'), Markup.button.callback('❌ Cancel', 'to_main_menu')], ]); } +/** + + * adminUserToolsKeyboard executes its scoped Runewager logic and participates in menu/command or utility flow composition. + + * Parameters: See the function signature for exact argument names and accepted values. + + * Returns: Returns the computed value or a Promise resolving to the operation result; may return void for side-effect handlers. + + * Side effects: May mutate runtime stores, pendingAction state, menu state, persistence files, logs, and callback progression. + + * Validation/safety: Uses existing guard utilities (admin checks, input checks, path checks, cooldown checks) where applicable. + + * Timeouts/fallbacks: Timeout and fallback behavior are controlled by the calling flow and global handler/state machine conventions. + + * Errors: Surfaces user-facing error replies and/or logs when inputs, permissions, or dependencies are invalid. + + * System fit: This function is part of the Runewager command/callback/state orchestration pipeline. + + */ + function adminUserToolsKeyboard() { return Markup.inlineKeyboard([ [Markup.button.callback('🔍 Whois User', 'admin_cmd_whois_prompt')], @@ -1807,9 +3269,30 @@ function adminUserToolsKeyboard() { [Markup.button.callback('♻️ Reset Bonus', 'w30_admin_reset')], [Markup.button.callback('📋 Pending Bonuses', 'w30_admin_pending')], [Markup.button.callback('⬅️ Admin Dashboard', 'open_admin_dashboard')], + [Markup.button.callback('🏠 Main Menu', 'to_main_menu'), Markup.button.callback('❌ Cancel', 'to_main_menu')], ]); } +/** + + * adminSystemToolsKeyboard executes its scoped Runewager logic and participates in menu/command or utility flow composition. + + * Parameters: See the function signature for exact argument names and accepted values. + + * Returns: Returns the computed value or a Promise resolving to the operation result; may return void for side-effect handlers. + + * Side effects: May mutate runtime stores, pendingAction state, menu state, persistence files, logs, and callback progression. + + * Validation/safety: Uses existing guard utilities (admin checks, input checks, path checks, cooldown checks) where applicable. + + * Timeouts/fallbacks: Timeout and fallback behavior are controlled by the calling flow and global handler/state machine conventions. + + * Errors: Surfaces user-facing error replies and/or logs when inputs, permissions, or dependencies are invalid. + + * System fit: This function is part of the Runewager command/callback/state orchestration pipeline. + + */ + function adminSystemToolsKeyboard(user = null) { const toggleLabel = user && user.adminModeOn ? '🟢 Admin Mode: ON (tap to toggle)' : '⚪ Admin Mode: OFF (tap to toggle)'; return Markup.inlineKeyboard([ @@ -1817,19 +3300,42 @@ function adminSystemToolsKeyboard(user = null) { [Markup.button.callback('📟 VPS Console (/sshv)', 'sshv_open')], [Markup.button.callback('🩺 Health Check', 'admin_cmd_health')], [Markup.button.callback('📦 Bot Version', 'admin_cmd_version')], - [Markup.button.callback('🧪 Tip Test (4h broadcast)', 'admin_cmd_tiptest')], + [Markup.button.callback('🤖 Verify Bot Setup', 'admin_cmd_verify_setup')], + [Markup.button.callback('🧪 Drop Test (4h broadcast)', 'admin_cmd_tiptest')], [Markup.button.callback('💾 Backup State', 'admin_backup_action')], [Markup.button.callback(toggleLabel, 'admin_cmd_mode_toggle')], [Markup.button.callback('⬅️ Admin Dashboard', 'open_admin_dashboard')], + [Markup.button.callback('🏠 Main Menu', 'to_main_menu'), Markup.button.callback('❌ Cancel', 'to_main_menu')], ]); } +/** + + * adminSupportToolsKeyboard executes its scoped Runewager logic and participates in menu/command or utility flow composition. + + * Parameters: See the function signature for exact argument names and accepted values. + + * Returns: Returns the computed value or a Promise resolving to the operation result; may return void for side-effect handlers. + + * Side effects: May mutate runtime stores, pendingAction state, menu state, persistence files, logs, and callback progression. + + * Validation/safety: Uses existing guard utilities (admin checks, input checks, path checks, cooldown checks) where applicable. + + * Timeouts/fallbacks: Timeout and fallback behavior are controlled by the calling flow and global handler/state machine conventions. + + * Errors: Surfaces user-facing error replies and/or logs when inputs, permissions, or dependencies are invalid. + + * System fit: This function is part of the Runewager command/callback/state orchestration pipeline. + + */ + function adminSupportToolsKeyboard() { return Markup.inlineKeyboard([ [Markup.button.callback('🐛 View Bug Reports', 'admin_cmd_viewbugs')], [Markup.button.callback('✅ Resolve Bug', 'admin_cmd_resolvebug_prompt')], [Markup.button.callback('📤 Export Bugs', 'admin_cmd_exportbugs')], [Markup.button.callback('⬅️ Admin Dashboard', 'open_admin_dashboard')], + [Markup.button.callback('🏠 Main Menu', 'to_main_menu'), Markup.button.callback('❌ Cancel', 'to_main_menu')], ]); } @@ -1882,6 +3388,26 @@ function buildAdminStatusText() { return lines; } +/** + + * sendAdminDashboard executes its scoped Runewager logic and participates in menu/command or utility flow composition. + + * Parameters: See the function signature for exact argument names and accepted values. + + * Returns: Returns the computed value or a Promise resolving to the operation result; may return void for side-effect handlers. + + * Side effects: May mutate runtime stores, pendingAction state, menu state, persistence files, logs, and callback progression. + + * Validation/safety: Uses existing guard utilities (admin checks, input checks, path checks, cooldown checks) where applicable. + + * Timeouts/fallbacks: Timeout and fallback behavior are controlled by the calling flow and global handler/state machine conventions. + + * Errors: Surfaces user-facing error replies and/or logs when inputs, permissions, or dependencies are invalid. + + * System fit: This function is part of the Runewager command/callback/state orchestration pipeline. + + */ + async function sendAdminDashboard(ctx, page = 1) { const pageLabel = page === 2 ? 'Page 2/2: Quick Actions' : 'Page 1/2: Categories'; const statusText = buildAdminStatusText(); @@ -1891,6 +3417,26 @@ async function sendAdminDashboard(ctx, page = 1) { ); } +/** + + * replaceCallbackPanel executes its scoped Runewager logic and participates in menu/command or utility flow composition. + + * Parameters: See the function signature for exact argument names and accepted values. + + * Returns: Returns the computed value or a Promise resolving to the operation result; may return void for side-effect handlers. + + * Side effects: May mutate runtime stores, pendingAction state, menu state, persistence files, logs, and callback progression. + + * Validation/safety: Uses existing guard utilities (admin checks, input checks, path checks, cooldown checks) where applicable. + + * Timeouts/fallbacks: Timeout and fallback behavior are controlled by the calling flow and global handler/state machine conventions. + + * Errors: Surfaces user-facing error replies and/or logs when inputs, permissions, or dependencies are invalid. + + * System fit: This function is part of the Runewager command/callback/state orchestration pipeline. + + */ + async function replaceCallbackPanel(ctx, text, extra = {}) { try { await ctx.editMessageText(text, extra); @@ -1907,6 +3453,26 @@ async function replaceCallbackPanel(ctx, text, extra = {}) { await ctx.reply(text, extra); } +/** + + * extractCallbackDataFromKeyboard executes its scoped Runewager logic and participates in menu/command or utility flow composition. + + * Parameters: See the function signature for exact argument names and accepted values. + + * Returns: Returns the computed value or a Promise resolving to the operation result; may return void for side-effect handlers. + + * Side effects: May mutate runtime stores, pendingAction state, menu state, persistence files, logs, and callback progression. + + * Validation/safety: Uses existing guard utilities (admin checks, input checks, path checks, cooldown checks) where applicable. + + * Timeouts/fallbacks: Timeout and fallback behavior are controlled by the calling flow and global handler/state machine conventions. + + * Errors: Surfaces user-facing error replies and/or logs when inputs, permissions, or dependencies are invalid. + + * System fit: This function is part of the Runewager command/callback/state orchestration pipeline. + + */ + function extractCallbackDataFromKeyboard(markup) { const rows = (markup && markup.reply_markup && Array.isArray(markup.reply_markup.inline_keyboard)) ? markup.reply_markup.inline_keyboard @@ -1920,6 +3486,26 @@ function extractCallbackDataFromKeyboard(markup) { return out; } +/** + + * escapeHtml executes its scoped Runewager logic and participates in menu/command or utility flow composition. + + * Parameters: See the function signature for exact argument names and accepted values. + + * Returns: Returns the computed value or a Promise resolving to the operation result; may return void for side-effect handlers. + + * Side effects: May mutate runtime stores, pendingAction state, menu state, persistence files, logs, and callback progression. + + * Validation/safety: Uses existing guard utilities (admin checks, input checks, path checks, cooldown checks) where applicable. + + * Timeouts/fallbacks: Timeout and fallback behavior are controlled by the calling flow and global handler/state machine conventions. + + * Errors: Surfaces user-facing error replies and/or logs when inputs, permissions, or dependencies are invalid. + + * System fit: This function is part of the Runewager command/callback/state orchestration pipeline. + + */ + function escapeHtml(text) { return String(text) .replace(/&/g, '&') @@ -1929,6 +3515,26 @@ function escapeHtml(text) { .replace(/'/g, '''); } +/** + + * formatTipForHtml executes its scoped Runewager logic and participates in menu/command or utility flow composition. + + * Parameters: See the function signature for exact argument names and accepted values. + + * Returns: Returns the computed value or a Promise resolving to the operation result; may return void for side-effect handlers. + + * Side effects: May mutate runtime stores, pendingAction state, menu state, persistence files, logs, and callback progression. + + * Validation/safety: Uses existing guard utilities (admin checks, input checks, path checks, cooldown checks) where applicable. + + * Timeouts/fallbacks: Timeout and fallback behavior are controlled by the calling flow and global handler/state machine conventions. + + * Errors: Surfaces user-facing error replies and/or logs when inputs, permissions, or dependencies are invalid. + + * System fit: This function is part of the Runewager command/callback/state orchestration pipeline. + + */ + function formatTipForHtml(text) { const t = String(text || '').trim(); if (!t) return ''; @@ -1937,6 +3543,26 @@ function formatTipForHtml(text) { return escapeHtml(t); } +/** + + * sendSettingsMenu executes its scoped Runewager logic and participates in menu/command or utility flow composition. + + * Parameters: See the function signature for exact argument names and accepted values. + + * Returns: Returns the computed value or a Promise resolving to the operation result; may return void for side-effect handlers. + + * Side effects: May mutate runtime stores, pendingAction state, menu state, persistence files, logs, and callback progression. + + * Validation/safety: Uses existing guard utilities (admin checks, input checks, path checks, cooldown checks) where applicable. + + * Timeouts/fallbacks: Timeout and fallback behavior are controlled by the calling flow and global handler/state machine conventions. + + * Errors: Surfaces user-facing error replies and/or logs when inputs, permissions, or dependencies are invalid. + + * System fit: This function is part of the Runewager command/callback/state orchestration pipeline. + + */ + async function sendSettingsMenu(ctx, user) { const s = user.settings; const lines = [ @@ -1954,6 +3580,26 @@ async function sendSettingsMenu(ctx, user) { await ctx.reply(lines, { parse_mode: 'Markdown', ...settingsKeyboard(user) }); } +/** + + * sendMainMenu executes its scoped Runewager logic and participates in menu/command or utility flow composition. + + * Parameters: See the function signature for exact argument names and accepted values. + + * Returns: Returns the computed value or a Promise resolving to the operation result; may return void for side-effect handlers. + + * Side effects: May mutate runtime stores, pendingAction state, menu state, persistence files, logs, and callback progression. + + * Validation/safety: Uses existing guard utilities (admin checks, input checks, path checks, cooldown checks) where applicable. + + * Timeouts/fallbacks: Timeout and fallback behavior are controlled by the calling flow and global handler/state machine conventions. + + * Errors: Surfaces user-facing error replies and/or logs when inputs, permissions, or dependencies are invalid. + + * System fit: This function is part of the Runewager command/callback/state orchestration pipeline. + + */ + async function sendMainMenu(ctx, user, page = 1) { const name = user.firstName ? `, ${user.firstName}` : ''; const adminUser = ADMIN_IDS.includes(Number(user.id)) && user.adminModeOn; @@ -1971,18 +3617,19 @@ async function sendMainMenu(ctx, user, page = 1) { /** Keyboard for the new persistent USER MAIN MENU */ function userMainMenuKeyboard(isAdminUser) { const rows = [ + [Markup.button.url('🎮 Play Runewager', LINKS.miniAppPlay)], [ - Markup.button.url('🎮 Play Now', LINKS.miniAppPlay), - Markup.button.callback('🎁 Claim Bonus', 'pmenu_claim_bonus'), + Markup.button.callback('🎁 Bonuses & Rewards', 'pmenu_claim_bonus'), + Markup.button.callback('👤 Profile & Link', 'pmenu_my_profile'), ], [ - Markup.button.callback('📊 My Profile', 'pmenu_my_profile'), - Markup.button.callback('🏆 Giveaways', 'pmenu_giveaways'), + Markup.button.callback('🏆 Giveaways & Community', 'pmenu_giveaways'), + Markup.button.callback('⚙️ Settings', 'menu_settings_tab'), ], - [Markup.button.callback('❓ Help / Commands', 'pmenu_help')], + [Markup.button.callback('❓ Help Center / Commands', 'pmenu_help')], ]; if (isAdminUser) { - rows.push([Markup.button.callback('🛠 Admin Dashboard', 'admin_dashboard')]); + rows.push([Markup.button.callback('🛠 Admin Control Center', 'admin_dashboard')]); } return Markup.inlineKeyboard(rows); } @@ -1995,11 +3642,12 @@ function userMainMenuText(user) { `👋 Hey ${name}! Here's your Runewager menu.\n\n` + (statusLine ? `${statusLine}\n\n` : '') + `🌍 *Runewager is 100% FREE to play — worldwide access, no deposits.*\n\n` - + `🎮 *Play Now* — Open the Runewager Mini App\n` - + `🎁 *Claim Bonus* — 3.5 SC new-user bonus & 30 SC wager bonus\n` - + `📊 *My Profile* — Linked account, XP, badges & referrals\n` - + `🏆 *Giveaways* — Active SC giveaways & eligibility\n` - + `❓ *Help / Commands* — Command booklet & bug reports\n\n` + + `🎮 *Play Runewager* — Open the Runewager Mini App\n` + + `🎁 *Bonuses & Rewards* — Promo menu + 30 SC wager bonus\n` + + `👤 *Profile & Link* — Linked account, XP, badges & referrals\n` + + `🏆 *Giveaways & Community* — Active SC giveaways + join links\n` + + `⚙️ *Settings* — Play mode, quick commands, tooltips\n` + + `❓ *Help Center / Commands* — Guide, commands & bug reports\n\n` + `/help · /menu · /profile · /link · /bonus` ); } @@ -2044,10 +3692,22 @@ async function sendPersistentUserMenu(ctx, user) { function adminMainMenuKeyboard(user) { const toggleLabel = user && user.adminModeOn ? '🟢 Admin Mode: ON (tap to toggle)' : '⚪ Admin Mode: OFF (tap to toggle)'; return Markup.inlineKeyboard([ - [Markup.button.callback('👤 Users & Stats', 'pamenu_stats')], - [Markup.button.callback('🎉 Giveaways', 'pamenu_active_giveaways')], - [Markup.button.callback('📣 Promo Tools', 'admin_cat_promo')], - [Markup.button.callback('⚙️ System Tools', 'admin_cat_system')], + [ + Markup.button.callback('👥 User Ops & Stats', 'pamenu_stats'), + Markup.button.callback('🎉 Giveaway Ops', 'pamenu_active_giveaways'), + ], + [ + Markup.button.callback('🎛 Promo Manager', 'admin_cat_promo'), + Markup.button.callback('📣 Broadcasts & Drops', 'admin_cmd_announce_start'), + ], + [ + Markup.button.callback('⚙️ System & Health', 'admin_cat_system'), + Markup.button.callback('🧪 Tests & Bugs', 'admin_cat_tests'), + ], + [ + Markup.button.callback('📟 VPS Console (/sshv)', 'sshv_open'), + Markup.button.callback('💡 Content Drops', 'admin_cmd_tips_dashboard'), + ], [Markup.button.callback(toggleLabel, 'admin_cmd_mode_toggle')], [Markup.button.callback('↩ Back to User Menu', 'pamenu_back_user')], ]); @@ -2201,6 +3861,26 @@ function toolsMenuKeyboard() { ]); } +/** + + * buildUserStatusLine executes its scoped Runewager logic and participates in menu/command or utility flow composition. + + * Parameters: See the function signature for exact argument names and accepted values. + + * Returns: Returns the computed value or a Promise resolving to the operation result; may return void for side-effect handlers. + + * Side effects: May mutate runtime stores, pendingAction state, menu state, persistence files, logs, and callback progression. + + * Validation/safety: Uses existing guard utilities (admin checks, input checks, path checks, cooldown checks) where applicable. + + * Timeouts/fallbacks: Timeout and fallback behavior are controlled by the calling flow and global handler/state machine conventions. + + * Errors: Surfaces user-facing error replies and/or logs when inputs, permissions, or dependencies are invalid. + + * System fit: This function is part of the Runewager command/callback/state orchestration pipeline. + + */ + function buildUserStatusLine(user) { const steps = []; if (user.ageConfirmed) steps.push('18+ ✅'); @@ -2210,10 +3890,50 @@ function buildUserStatusLine(user) { return steps.length ? `📊 Status: ${steps.join(' | ')}` : ''; } +/** + + * needsLinkedUsername executes its scoped Runewager logic and participates in menu/command or utility flow composition. + + * Parameters: See the function signature for exact argument names and accepted values. + + * Returns: Returns the computed value or a Promise resolving to the operation result; may return void for side-effect handlers. + + * Side effects: May mutate runtime stores, pendingAction state, menu state, persistence files, logs, and callback progression. + + * Validation/safety: Uses existing guard utilities (admin checks, input checks, path checks, cooldown checks) where applicable. + + * Timeouts/fallbacks: Timeout and fallback behavior are controlled by the calling flow and global handler/state machine conventions. + + * Errors: Surfaces user-facing error replies and/or logs when inputs, permissions, or dependencies are invalid. + + * System fit: This function is part of the Runewager command/callback/state orchestration pipeline. + + */ + function needsLinkedUsername(user) { return !user.runewagerUsername || !user.runewagerUsername.trim(); } +/** + + * linkedUsernameError executes its scoped Runewager logic and participates in menu/command or utility flow composition. + + * Parameters: See the function signature for exact argument names and accepted values. + + * Returns: Returns the computed value or a Promise resolving to the operation result; may return void for side-effect handlers. + + * Side effects: May mutate runtime stores, pendingAction state, menu state, persistence files, logs, and callback progression. + + * Validation/safety: Uses existing guard utilities (admin checks, input checks, path checks, cooldown checks) where applicable. + + * Timeouts/fallbacks: Timeout and fallback behavior are controlled by the calling flow and global handler/state machine conventions. + + * Errors: Surfaces user-facing error replies and/or logs when inputs, permissions, or dependencies are invalid. + + * System fit: This function is part of the Runewager command/callback/state orchestration pipeline. + + */ + async function linkedUsernameError(ctx) { await ctx.reply( '🔗 You need to link your Runewager username first.\n\n' @@ -2227,6 +3947,36 @@ async function linkedUsernameError(ctx) { } +/** + + + * deleteEphemeralBonusPrompt executes its scoped Runewager logic and participates in menu/command or utility flow composition. + + + * Parameters: See the function signature for exact argument names and accepted values. + + + * Returns: Returns the computed value or a Promise resolving to the operation result; may return void for side-effect handlers. + + + * Side effects: May mutate runtime stores, pendingAction state, menu state, persistence files, logs, and callback progression. + + + * Validation/safety: Uses existing guard utilities (admin checks, input checks, path checks, cooldown checks) where applicable. + + + * Timeouts/fallbacks: Timeout and fallback behavior are controlled by the calling flow and global handler/state machine conventions. + + + * Errors: Surfaces user-facing error replies and/or logs when inputs, permissions, or dependencies are invalid. + + + * System fit: This function is part of the Runewager command/callback/state orchestration pipeline. + + + */ + + async function deleteEphemeralBonusPrompt(ctx, user) { if (!user.ephemeralBonusMsgId || !user.ephemeralBonusChatId) return; try { @@ -2236,6 +3986,26 @@ async function deleteEphemeralBonusPrompt(ctx, user) { user.ephemeralBonusChatId = null; } +/** + + * sendEphemeralBonusPrompt executes its scoped Runewager logic and participates in menu/command or utility flow composition. + + * Parameters: See the function signature for exact argument names and accepted values. + + * Returns: Returns the computed value or a Promise resolving to the operation result; may return void for side-effect handlers. + + * Side effects: May mutate runtime stores, pendingAction state, menu state, persistence files, logs, and callback progression. + + * Validation/safety: Uses existing guard utilities (admin checks, input checks, path checks, cooldown checks) where applicable. + + * Timeouts/fallbacks: Timeout and fallback behavior are controlled by the calling flow and global handler/state machine conventions. + + * Errors: Surfaces user-facing error replies and/or logs when inputs, permissions, or dependencies are invalid. + + * System fit: This function is part of the Runewager command/callback/state orchestration pipeline. + + */ + async function sendEphemeralBonusPrompt(ctx, user) { const sent = await ctx.reply( `🎁 *Claim Your New User Bonus* @@ -2265,6 +4035,26 @@ Enter promo code *${promoStore.code}* on the Runewager affiliate page to claim y const adminEventsLogFile = path.join(dataDir, 'admin-events.log'); const ADMIN_LOG_CAP = 200; +/** + + * adminLog executes its scoped Runewager logic and participates in menu/command or utility flow composition. + + * Parameters: See the function signature for exact argument names and accepted values. + + * Returns: Returns the computed value or a Promise resolving to the operation result; may return void for side-effect handlers. + + * Side effects: May mutate runtime stores, pendingAction state, menu state, persistence files, logs, and callback progression. + + * Validation/safety: Uses existing guard utilities (admin checks, input checks, path checks, cooldown checks) where applicable. + + * Timeouts/fallbacks: Timeout and fallback behavior are controlled by the calling flow and global handler/state machine conventions. + + * Errors: Surfaces user-facing error replies and/or logs when inputs, permissions, or dependencies are invalid. + + * System fit: This function is part of the Runewager command/callback/state orchestration pipeline. + + */ + function adminLog(event, payload) { const entry = { ts: new Date().toISOString(), event, payload }; promoStore.logs.push(entry); @@ -2277,6 +4067,26 @@ function adminLog(event, payload) { } catch (_) { /* non-fatal — in-memory log is the source of truth */ } } +/** + + * clearPendingAction executes its scoped Runewager logic and participates in menu/command or utility flow composition. + + * Parameters: See the function signature for exact argument names and accepted values. + + * Returns: Returns the computed value or a Promise resolving to the operation result; may return void for side-effect handlers. + + * Side effects: May mutate runtime stores, pendingAction state, menu state, persistence files, logs, and callback progression. + + * Validation/safety: Uses existing guard utilities (admin checks, input checks, path checks, cooldown checks) where applicable. + + * Timeouts/fallbacks: Timeout and fallback behavior are controlled by the calling flow and global handler/state machine conventions. + + * Errors: Surfaces user-facing error replies and/or logs when inputs, permissions, or dependencies are invalid. + + * System fit: This function is part of the Runewager command/callback/state orchestration pipeline. + + */ + function clearPendingAction(user) { user.pendingAction = null; } @@ -2324,6 +4134,26 @@ async function showOnboardingPrompt(ctx, user, step) { } } +/** + + * isValidHttpUrl executes its scoped Runewager logic and participates in menu/command or utility flow composition. + + * Parameters: See the function signature for exact argument names and accepted values. + + * Returns: Returns the computed value or a Promise resolving to the operation result; may return void for side-effect handlers. + + * Side effects: May mutate runtime stores, pendingAction state, menu state, persistence files, logs, and callback progression. + + * Validation/safety: Uses existing guard utilities (admin checks, input checks, path checks, cooldown checks) where applicable. + + * Timeouts/fallbacks: Timeout and fallback behavior are controlled by the calling flow and global handler/state machine conventions. + + * Errors: Surfaces user-facing error replies and/or logs when inputs, permissions, or dependencies are invalid. + + * System fit: This function is part of the Runewager command/callback/state orchestration pipeline. + + */ + function isValidHttpUrl(value) { try { const parsed = new URL(value); @@ -2350,11 +4180,51 @@ function escapeMarkdownFull(text) { return String(text).replace(/[_*[\]()~`>#+\-=|{}.!\\]/g, '\\$&'); } -function ensureDataDir() { - if (!fs.existsSync(dataDir)) fs.mkdirSync(dataDir, { recursive: true }); -} +/** -function validateSafePath(inputPath, baseDir) { + * ensureDataDir executes its scoped Runewager logic and participates in menu/command or utility flow composition. + + * Parameters: See the function signature for exact argument names and accepted values. + + * Returns: Returns the computed value or a Promise resolving to the operation result; may return void for side-effect handlers. + + * Side effects: May mutate runtime stores, pendingAction state, menu state, persistence files, logs, and callback progression. + + * Validation/safety: Uses existing guard utilities (admin checks, input checks, path checks, cooldown checks) where applicable. + + * Timeouts/fallbacks: Timeout and fallback behavior are controlled by the calling flow and global handler/state machine conventions. + + * Errors: Surfaces user-facing error replies and/or logs when inputs, permissions, or dependencies are invalid. + + * System fit: This function is part of the Runewager command/callback/state orchestration pipeline. + + */ + +function ensureDataDir() { + if (!fs.existsSync(dataDir)) fs.mkdirSync(dataDir, { recursive: true }); +} + +/** + + * validateSafePath executes its scoped Runewager logic and participates in menu/command or utility flow composition. + + * Parameters: See the function signature for exact argument names and accepted values. + + * Returns: Returns the computed value or a Promise resolving to the operation result; may return void for side-effect handlers. + + * Side effects: May mutate runtime stores, pendingAction state, menu state, persistence files, logs, and callback progression. + + * Validation/safety: Uses existing guard utilities (admin checks, input checks, path checks, cooldown checks) where applicable. + + * Timeouts/fallbacks: Timeout and fallback behavior are controlled by the calling flow and global handler/state machine conventions. + + * Errors: Surfaces user-facing error replies and/or logs when inputs, permissions, or dependencies are invalid. + + * System fit: This function is part of the Runewager command/callback/state orchestration pipeline. + + */ + +function validateSafePath(inputPath, baseDir) { if (!inputPath || typeof inputPath !== 'string') { throw new Error('Invalid path input'); } @@ -2373,6 +4243,26 @@ function validateSafePath(inputPath, baseDir) { return realTarget; } +/** + + * loadJson executes its scoped Runewager logic and participates in menu/command or utility flow composition. + + * Parameters: See the function signature for exact argument names and accepted values. + + * Returns: Returns the computed value or a Promise resolving to the operation result; may return void for side-effect handlers. + + * Side effects: May mutate runtime stores, pendingAction state, menu state, persistence files, logs, and callback progression. + + * Validation/safety: Uses existing guard utilities (admin checks, input checks, path checks, cooldown checks) where applicable. + + * Timeouts/fallbacks: Timeout and fallback behavior are controlled by the calling flow and global handler/state machine conventions. + + * Errors: Surfaces user-facing error replies and/or logs when inputs, permissions, or dependencies are invalid. + + * System fit: This function is part of the Runewager command/callback/state orchestration pipeline. + + */ + function loadJson(filePath, fallback) { try { const safePath = validateSafePath(filePath, dataDir); @@ -2383,6 +4273,26 @@ function loadJson(filePath, fallback) { } } +/** + + * writeFileAtomic executes its scoped Runewager logic and participates in menu/command or utility flow composition. + + * Parameters: See the function signature for exact argument names and accepted values. + + * Returns: Returns the computed value or a Promise resolving to the operation result; may return void for side-effect handlers. + + * Side effects: May mutate runtime stores, pendingAction state, menu state, persistence files, logs, and callback progression. + + * Validation/safety: Uses existing guard utilities (admin checks, input checks, path checks, cooldown checks) where applicable. + + * Timeouts/fallbacks: Timeout and fallback behavior are controlled by the calling flow and global handler/state machine conventions. + + * Errors: Surfaces user-facing error replies and/or logs when inputs, permissions, or dependencies are invalid. + + * System fit: This function is part of the Runewager command/callback/state orchestration pipeline. + + */ + function writeFileAtomic(filePath, content) { const safeFilePath = validateSafePath(filePath, dataDir); const dirPath = path.dirname(safeFilePath); @@ -2392,15 +4302,75 @@ function writeFileAtomic(filePath, content) { fs.renameSync(tempFile, safeFilePath); } +/** + + * saveJson executes its scoped Runewager logic and participates in menu/command or utility flow composition. + + * Parameters: See the function signature for exact argument names and accepted values. + + * Returns: Returns the computed value or a Promise resolving to the operation result; may return void for side-effect handlers. + + * Side effects: May mutate runtime stores, pendingAction state, menu state, persistence files, logs, and callback progression. + + * Validation/safety: Uses existing guard utilities (admin checks, input checks, path checks, cooldown checks) where applicable. + + * Timeouts/fallbacks: Timeout and fallback behavior are controlled by the calling flow and global handler/state machine conventions. + + * Errors: Surfaces user-facing error replies and/or logs when inputs, permissions, or dependencies are invalid. + + * System fit: This function is part of the Runewager command/callback/state orchestration pipeline. + + */ + function saveJson(filePath, data) { ensureDataDir(); writeFileAtomic(filePath, JSON.stringify(data, null, 2)); } +/** + + * addBadge executes its scoped Runewager logic and participates in menu/command or utility flow composition. + + * Parameters: See the function signature for exact argument names and accepted values. + + * Returns: Returns the computed value or a Promise resolving to the operation result; may return void for side-effect handlers. + + * Side effects: May mutate runtime stores, pendingAction state, menu state, persistence files, logs, and callback progression. + + * Validation/safety: Uses existing guard utilities (admin checks, input checks, path checks, cooldown checks) where applicable. + + * Timeouts/fallbacks: Timeout and fallback behavior are controlled by the calling flow and global handler/state machine conventions. + + * Errors: Surfaces user-facing error replies and/or logs when inputs, permissions, or dependencies are invalid. + + * System fit: This function is part of the Runewager command/callback/state orchestration pipeline. + + */ + function addBadge(user, badge) { if (!user.badges.has(badge)) user.badges.add(badge); } +/** + + * onboardingStepLabel executes its scoped Runewager logic and participates in menu/command or utility flow composition. + + * Parameters: See the function signature for exact argument names and accepted values. + + * Returns: Returns the computed value or a Promise resolving to the operation result; may return void for side-effect handlers. + + * Side effects: May mutate runtime stores, pendingAction state, menu state, persistence files, logs, and callback progression. + + * Validation/safety: Uses existing guard utilities (admin checks, input checks, path checks, cooldown checks) where applicable. + + * Timeouts/fallbacks: Timeout and fallback behavior are controlled by the calling flow and global handler/state machine conventions. + + * Errors: Surfaces user-facing error replies and/or logs when inputs, permissions, or dependencies are invalid. + + * System fit: This function is part of the Runewager command/callback/state orchestration pipeline. + + */ + function onboardingStepLabel(step) { // Step 1 (Verify Account) previously meant Discord verification — now means account confirmed. // Discord is NOT involved in onboarding; steps are purely Runewager-based. @@ -2408,6 +4378,26 @@ function onboardingStepLabel(step) { return labels[step] || 'Done'; } +/** + + * getNextOnboardingStep executes its scoped Runewager logic and participates in menu/command or utility flow composition. + + * Parameters: See the function signature for exact argument names and accepted values. + + * Returns: Returns the computed value or a Promise resolving to the operation result; may return void for side-effect handlers. + + * Side effects: May mutate runtime stores, pendingAction state, menu state, persistence files, logs, and callback progression. + + * Validation/safety: Uses existing guard utilities (admin checks, input checks, path checks, cooldown checks) where applicable. + + * Timeouts/fallbacks: Timeout and fallback behavior are controlled by the calling flow and global handler/state machine conventions. + + * Errors: Surfaces user-facing error replies and/or logs when inputs, permissions, or dependencies are invalid. + + * System fit: This function is part of the Runewager command/callback/state orchestration pipeline. + + */ + function getNextOnboardingStep(user) { if (!user.ageConfirmed) return 0; // Step 1: account confirmed — set automatically once user confirms signup or is an existing user @@ -2418,6 +4408,26 @@ function getNextOnboardingStep(user) { return 5; } +/** + + * trackOnboardingProgress executes its scoped Runewager logic and participates in menu/command or utility flow composition. + + * Parameters: See the function signature for exact argument names and accepted values. + + * Returns: Returns the computed value or a Promise resolving to the operation result; may return void for side-effect handlers. + + * Side effects: May mutate runtime stores, pendingAction state, menu state, persistence files, logs, and callback progression. + + * Validation/safety: Uses existing guard utilities (admin checks, input checks, path checks, cooldown checks) where applicable. + + * Timeouts/fallbacks: Timeout and fallback behavior are controlled by the calling flow and global handler/state machine conventions. + + * Errors: Surfaces user-facing error replies and/or logs when inputs, permissions, or dependencies are invalid. + + * System fit: This function is part of the Runewager command/callback/state orchestration pipeline. + + */ + function trackOnboardingProgress(user) { const nextStep = getNextOnboardingStep(user); user.onboarding.currentStep = nextStep; @@ -2431,6 +4441,26 @@ function trackOnboardingProgress(user) { } } +/** + + * referralCodeForUser executes its scoped Runewager logic and participates in menu/command or utility flow composition. + + * Parameters: See the function signature for exact argument names and accepted values. + + * Returns: Returns the computed value or a Promise resolving to the operation result; may return void for side-effect handlers. + + * Side effects: May mutate runtime stores, pendingAction state, menu state, persistence files, logs, and callback progression. + + * Validation/safety: Uses existing guard utilities (admin checks, input checks, path checks, cooldown checks) where applicable. + + * Timeouts/fallbacks: Timeout and fallback behavior are controlled by the calling flow and global handler/state machine conventions. + + * Errors: Surfaces user-facing error replies and/or logs when inputs, permissions, or dependencies are invalid. + + * System fit: This function is part of the Runewager command/callback/state orchestration pipeline. + + */ + function referralCodeForUser(user) { if (!referralStore.links.has(user.id)) { referralStore.links.set(user.id, `rw${user.id.toString(36)}`); @@ -2438,11 +4468,51 @@ function referralCodeForUser(user) { return referralStore.links.get(user.id); } +/** + + * getActiveReferralBoost executes its scoped Runewager logic and participates in menu/command or utility flow composition. + + * Parameters: See the function signature for exact argument names and accepted values. + + * Returns: Returns the computed value or a Promise resolving to the operation result; may return void for side-effect handlers. + + * Side effects: May mutate runtime stores, pendingAction state, menu state, persistence files, logs, and callback progression. + + * Validation/safety: Uses existing guard utilities (admin checks, input checks, path checks, cooldown checks) where applicable. + + * Timeouts/fallbacks: Timeout and fallback behavior are controlled by the calling flow and global handler/state machine conventions. + + * Errors: Surfaces user-facing error replies and/or logs when inputs, permissions, or dependencies are invalid. + + * System fit: This function is part of the Runewager command/callback/state orchestration pipeline. + + */ + function getActiveReferralBoost() { const now = Date.now(); return referralStore.boosts.find((b) => b.startsAt <= now && b.endsAt >= now) || null; } +/** + + * awardReferralProgress executes its scoped Runewager logic and participates in menu/command or utility flow composition. + + * Parameters: See the function signature for exact argument names and accepted values. + + * Returns: Returns the computed value or a Promise resolving to the operation result; may return void for side-effect handlers. + + * Side effects: May mutate runtime stores, pendingAction state, menu state, persistence files, logs, and callback progression. + + * Validation/safety: Uses existing guard utilities (admin checks, input checks, path checks, cooldown checks) where applicable. + + * Timeouts/fallbacks: Timeout and fallback behavior are controlled by the calling flow and global handler/state machine conventions. + + * Errors: Surfaces user-facing error replies and/or logs when inputs, permissions, or dependencies are invalid. + + * System fit: This function is part of the Runewager command/callback/state orchestration pipeline. + + */ + function awardReferralProgress(userId, base = 1) { const boost = getActiveReferralBoost(); const mult = boost ? boost.multiplier : 1; @@ -2460,11 +4530,51 @@ function awardReferralProgress(userId, base = 1) { } } +/** + + * trackAnalytics executes its scoped Runewager logic and participates in menu/command or utility flow composition. + + * Parameters: See the function signature for exact argument names and accepted values. + + * Returns: Returns the computed value or a Promise resolving to the operation result; may return void for side-effect handlers. + + * Side effects: May mutate runtime stores, pendingAction state, menu state, persistence files, logs, and callback progression. + + * Validation/safety: Uses existing guard utilities (admin checks, input checks, path checks, cooldown checks) where applicable. + + * Timeouts/fallbacks: Timeout and fallback behavior are controlled by the calling flow and global handler/state machine conventions. + + * Errors: Surfaces user-facing error replies and/or logs when inputs, permissions, or dependencies are invalid. + + * System fit: This function is part of the Runewager command/callback/state orchestration pipeline. + + */ + function trackAnalytics(event, payload = {}) { analyticsStore.events.push({ event, payload, ts: Date.now() }); if (analyticsStore.events.length > MAX_ANALYTICS_EVENTS) analyticsStore.events.shift(); } +/** + + * safeStepHandler executes its scoped Runewager logic and participates in menu/command or utility flow composition. + + * Parameters: See the function signature for exact argument names and accepted values. + + * Returns: Returns the computed value or a Promise resolving to the operation result; may return void for side-effect handlers. + + * Side effects: May mutate runtime stores, pendingAction state, menu state, persistence files, logs, and callback progression. + + * Validation/safety: Uses existing guard utilities (admin checks, input checks, path checks, cooldown checks) where applicable. + + * Timeouts/fallbacks: Timeout and fallback behavior are controlled by the calling flow and global handler/state machine conventions. + + * Errors: Surfaces user-facing error replies and/or logs when inputs, permissions, or dependencies are invalid. + + * System fit: This function is part of the Runewager command/callback/state orchestration pipeline. + + */ + function safeStepHandler(name, handler) { return async (ctx) => { try { @@ -2501,6 +4611,26 @@ function safeAdminHandler(name, helpOpts, handler) { }; } +/** + + * sendEphemeral executes its scoped Runewager logic and participates in menu/command or utility flow composition. + + * Parameters: See the function signature for exact argument names and accepted values. + + * Returns: Returns the computed value or a Promise resolving to the operation result; may return void for side-effect handlers. + + * Side effects: May mutate runtime stores, pendingAction state, menu state, persistence files, logs, and callback progression. + + * Validation/safety: Uses existing guard utilities (admin checks, input checks, path checks, cooldown checks) where applicable. + + * Timeouts/fallbacks: Timeout and fallback behavior are controlled by the calling flow and global handler/state machine conventions. + + * Errors: Surfaces user-facing error replies and/or logs when inputs, permissions, or dependencies are invalid. + + * System fit: This function is part of the Runewager command/callback/state orchestration pipeline. + + */ + async function sendEphemeral(ctx, text, ttlMs = 20000) { const sent = await ctx.reply(text); setTimeout(() => { @@ -2508,6 +4638,26 @@ async function sendEphemeral(ctx, text, ttlMs = 20000) { }, ttlMs); } +/** + + * reactToMessage executes its scoped Runewager logic and participates in menu/command or utility flow composition. + + * Parameters: See the function signature for exact argument names and accepted values. + + * Returns: Returns the computed value or a Promise resolving to the operation result; may return void for side-effect handlers. + + * Side effects: May mutate runtime stores, pendingAction state, menu state, persistence files, logs, and callback progression. + + * Validation/safety: Uses existing guard utilities (admin checks, input checks, path checks, cooldown checks) where applicable. + + * Timeouts/fallbacks: Timeout and fallback behavior are controlled by the calling flow and global handler/state machine conventions. + + * Errors: Surfaces user-facing error replies and/or logs when inputs, permissions, or dependencies are invalid. + + * System fit: This function is part of the Runewager command/callback/state orchestration pipeline. + + */ + async function reactToMessage(ctx, emoji) { if (!ctx.chat || !ctx.message) return; await ctx.telegram.callApi('setMessageReaction', { @@ -2522,6 +4672,16 @@ async function reactToMessage(ctx, emoji) { // https://core.telegram.org/bots/webapps#validating-data-received-via-the-mini-app // secret_key = HMAC-SHA256(msg=BOT_TOKEN, key=WEBAPP_HMAC_KEY) // verify_hash = HMAC-SHA256(msg=data_check_string, key=secret_key) +/** + * verifyWebAppInitData executes its scoped Runewager logic and participates in menu/command or utility flow composition. + * Parameters: See the function signature for exact argument names and accepted values. + * Returns: Returns the computed value or a Promise resolving to the operation result; may return void for side-effect handlers. + * Side effects: May mutate runtime stores, pendingAction state, menu state, persistence files, logs, and callback progression. + * Validation/safety: Uses existing guard utilities (admin checks, input checks, path checks, cooldown checks) where applicable. + * Timeouts/fallbacks: Timeout and fallback behavior are controlled by the calling flow and global handler/state machine conventions. + * Errors: Surfaces user-facing error replies and/or logs when inputs, permissions, or dependencies are invalid. + * System fit: This function is part of the Runewager command/callback/state orchestration pipeline. + */ function verifyWebAppInitData(initDataRaw) { if (!initDataRaw || !BOT_TOKEN) return false; const params = new URLSearchParams(initDataRaw); @@ -2548,6 +4708,26 @@ function verifyWebAppInitData(initDataRaw) { // bounding memory usage on high-traffic bots. const SMART_BUTTON_TTL_MS = 2 * 60 * 1000; +/** + + * consumeSmartButton executes its scoped Runewager logic and participates in menu/command or utility flow composition. + + * Parameters: See the function signature for exact argument names and accepted values. + + * Returns: Returns the computed value or a Promise resolving to the operation result; may return void for side-effect handlers. + + * Side effects: May mutate runtime stores, pendingAction state, menu state, persistence files, logs, and callback progression. + + * Validation/safety: Uses existing guard utilities (admin checks, input checks, path checks, cooldown checks) where applicable. + + * Timeouts/fallbacks: Timeout and fallback behavior are controlled by the calling flow and global handler/state machine conventions. + + * Errors: Surfaces user-facing error replies and/or logs when inputs, permissions, or dependencies are invalid. + + * System fit: This function is part of the Runewager command/callback/state orchestration pipeline. + + */ + function consumeSmartButton(userId, key) { const token = `${userId}:${key}`; const now = Date.now(); @@ -2558,12 +4738,13 @@ function consumeSmartButton(userId, key) { } // Prune expired smart button entries every 10 minutes -setInterval(() => { +const smartButtonGcTimer = setInterval(() => { const now = Date.now(); for (const [token, expiry] of smartButtonTracker) { if (expiry <= now) smartButtonTracker.delete(token); } }, 10 * 60 * 1000); +smartButtonGcTimer.unref(); /** * Compute analytics for a given time window (milliseconds from now). @@ -2603,6 +4784,26 @@ function buildStatsWindow(windowMs) { }; } +/** + + * buildDashboard executes its scoped Runewager logic and participates in menu/command or utility flow composition. + + * Parameters: See the function signature for exact argument names and accepted values. + + * Returns: Returns the computed value or a Promise resolving to the operation result; may return void for side-effect handlers. + + * Side effects: May mutate runtime stores, pendingAction state, menu state, persistence files, logs, and callback progression. + + * Validation/safety: Uses existing guard utilities (admin checks, input checks, path checks, cooldown checks) where applicable. + + * Timeouts/fallbacks: Timeout and fallback behavior are controlled by the calling flow and global handler/state machine conventions. + + * Errors: Surfaces user-facing error replies and/or logs when inputs, permissions, or dependencies are invalid. + + * System fit: This function is part of the Runewager command/callback/state orchestration pipeline. + + */ + function buildDashboard() { const now = Date.now(); const allUsers = Array.from(userStore.values()); @@ -2611,7 +4812,7 @@ function buildDashboard() { const activeUsers = allUsers.filter((u) => (u.lastSeenAt || 0) > now - 7 * 24 * 60 * 60 * 1000).length; const activeUsers10m = allUsers.filter((u) => (u.lastSeenAt || 0) > now - 10 * 60 * 1000).length; const activeUsers5m = allUsers.filter((u) => (u.lastSeenAt || 0) > now - 5 * 60 * 1000).length; - const promoClaims = promoStore.claimsByUser.size; + const promoClaims = new Set(promoManagerStore.claims.filter((c) => ['claimed', 'approved'].includes(c.status)).map((c) => c.user_id)).size; const onboardingDone = allUsers.filter((u) => u.onboarding && u.onboarding.completedAt).length; const data = { generatedAt: new Date().toISOString(), @@ -2630,32 +4831,113 @@ function buildDashboard() { return data; } +/** + + * registerPromoClaim executes its scoped Runewager logic and participates in menu/command or utility flow composition. + + * Parameters: See the function signature for exact argument names and accepted values. + + * Returns: Returns the computed value or a Promise resolving to the operation result; may return void for side-effect handlers. + + * Side effects: May mutate runtime stores, pendingAction state, menu state, persistence files, logs, and callback progression. + + * Validation/safety: Uses existing guard utilities (admin checks, input checks, path checks, cooldown checks) where applicable. + + * Timeouts/fallbacks: Timeout and fallback behavior are controlled by the calling flow and global handler/state machine conventions. + + * Errors: Surfaces user-facing error replies and/or logs when inputs, permissions, or dependencies are invalid. + + * System fit: This function is part of the Runewager command/callback/state orchestration pipeline. + + */ + function registerPromoClaim(user, code) { if (!promoHistoryStore.has(user.id)) promoHistoryStore.set(user.id, []); promoHistoryStore.get(user.id).push({ code, ts: Date.now() }); saveJson(promoHistoryFile, Object.fromEntries(promoHistoryStore)); } +/** + + * loadPersistentData executes its scoped Runewager logic and participates in menu/command or utility flow composition. + + * Parameters: See the function signature for exact argument names and accepted values. + + * Returns: Returns the computed value or a Promise resolving to the operation result; may return void for side-effect handlers. + + * Side effects: May mutate runtime stores, pendingAction state, menu state, persistence files, logs, and callback progression. + + * Validation/safety: Uses existing guard utilities (admin checks, input checks, path checks, cooldown checks) where applicable. + + * Timeouts/fallbacks: Timeout and fallback behavior are controlled by the calling flow and global handler/state machine conventions. + + * Errors: Surfaces user-facing error replies and/or logs when inputs, permissions, or dependencies are invalid. + + * System fit: This function is part of the Runewager command/callback/state orchestration pipeline. + + */ + function loadPersistentData() { const promoHistory = loadJson(promoHistoryFile, {}); Object.entries(promoHistory).forEach(([id, entries]) => promoHistoryStore.set(Number(id), entries)); Object.assign(analyticsStore, loadJson(analyticsFile, analyticsStore)); loadRuntimeState(); + loadPromoManagerDb(); loadHelpfulMessages(); loadSshvSessions(); } -function persistAnalytics() { - saveJson(analyticsFile, analyticsStore); -} +/** -async function configureBotSurface() { - // ── User commands visible in ALL chats (Telegram's global default) ────────── - const globalCommands = [ - { command: 'start', description: 'Start or resume onboarding' }, - { command: 'menu', description: 'Open your main menu' }, - { command: 'help', description: 'Help guide, commands & bug report' }, - { command: 'commands', description: 'Full command reference (alias for /help)' }, + * persistAnalytics executes its scoped Runewager logic and participates in menu/command or utility flow composition. + + * Parameters: See the function signature for exact argument names and accepted values. + + * Returns: Returns the computed value or a Promise resolving to the operation result; may return void for side-effect handlers. + + * Side effects: May mutate runtime stores, pendingAction state, menu state, persistence files, logs, and callback progression. + + * Validation/safety: Uses existing guard utilities (admin checks, input checks, path checks, cooldown checks) where applicable. + + * Timeouts/fallbacks: Timeout and fallback behavior are controlled by the calling flow and global handler/state machine conventions. + + * Errors: Surfaces user-facing error replies and/or logs when inputs, permissions, or dependencies are invalid. + + * System fit: This function is part of the Runewager command/callback/state orchestration pipeline. + + */ + +function persistAnalytics() { + saveJson(analyticsFile, analyticsStore); +} + +/** + + * configureBotSurface executes its scoped Runewager logic and participates in menu/command or utility flow composition. + + * Parameters: See the function signature for exact argument names and accepted values. + + * Returns: Returns the computed value or a Promise resolving to the operation result; may return void for side-effect handlers. + + * Side effects: May mutate runtime stores, pendingAction state, menu state, persistence files, logs, and callback progression. + + * Validation/safety: Uses existing guard utilities (admin checks, input checks, path checks, cooldown checks) where applicable. + + * Timeouts/fallbacks: Timeout and fallback behavior are controlled by the calling flow and global handler/state machine conventions. + + * Errors: Surfaces user-facing error replies and/or logs when inputs, permissions, or dependencies are invalid. + + * System fit: This function is part of the Runewager command/callback/state orchestration pipeline. + + */ + +async function configureBotSurface() { + // ── User commands visible in ALL chats (Telegram's global default) ────────── + const globalCommands = [ + { command: 'start', description: 'Start or resume onboarding' }, + { command: 'menu', description: 'Open your main menu' }, + { command: 'help', description: 'Help guide, commands & bug report' }, + { command: 'commands', description: 'Full command reference (alias for /help)' }, { command: 'play', description: 'Launch Runewager Mini App inside Telegram' }, { command: 'status', description: 'Quick account status — 18+, username, promo, onboarding' }, { command: 'profile', description: 'View your profile, XP, badges, referral code' }, @@ -2926,6 +5208,26 @@ function buildHelpPages(user) { const adminUser = ADMIN_IDS.includes(Number(user.id)) && user.adminModeOn; const tips = user.settings && user.settings.tooltipsEnabled; + /** + + * navRow executes its scoped Runewager logic and participates in menu/command or utility flow composition. + + * Parameters: See the function signature for exact argument names and accepted values. + + * Returns: Returns the computed value or a Promise resolving to the operation result; may return void for side-effect handlers. + + * Side effects: May mutate runtime stores, pendingAction state, menu state, persistence files, logs, and callback progression. + + * Validation/safety: Uses existing guard utilities (admin checks, input checks, path checks, cooldown checks) where applicable. + + * Timeouts/fallbacks: Timeout and fallback behavior are controlled by the calling flow and global handler/state machine conventions. + + * Errors: Surfaces user-facing error replies and/or logs when inputs, permissions, or dependencies are invalid. + + * System fit: This function is part of the Runewager command/callback/state orchestration pipeline. + + */ + function navRow(page, total) { const row = []; if (page > 1) row.push(Markup.button.callback('◀️ Prev', `help_page_${page - 1}`)); @@ -3128,7 +5430,6 @@ function buildHelpPages(user) { '/help or /commands — Open this help booklet', '/bugreport — Report a bug to the admin team', '/support — Open the support portal (DM only)', - '/language — View detected language setting', '', tips ? 'ℹ️ Tip: Enable tooltips in /settings for extra guidance.' : '', '', @@ -3196,41 +5497,27 @@ function buildHelpPages(user) { '/unapprove_group — Remove group from whitelist', '/list_groups — List all approved groups', '', - '💡 TIPS MANAGEMENT', - '/tips (or /t, /tp) — Post a tip to the group', + '💡 CONTENT DROPS MANAGEMENT', + '/tips (or /t, /tp) — Post a content drop to the group', '/tiplist — List all tips', '/tipadd — Add a new tip', '/tipremove — Remove a tip', '/tipedit — Edit an existing tip', '/tiptoggle — Enable/disable tips feature', - '/tipsettings — Configure tips settings', - '', - '🐞 BUG REPORTS', - '/bugreports — View all open bug reports', - '/exportbugs — Export all reports as plain text', - '/resolvebug — Mark a report resolved', + '/tipsettings — Configure content drop settings', '', '📊 ANALYTICS', '/funnel — Conversion funnel stats', '/discord_stats — Discord verification stats', '', - '🔧 SYSTEM', - '/testall — Full static self-test (every feature PASS/FAIL)', - '/testgiveaway — Fake giveaway end-to-end test', - '/health — Live health endpoint data', - '/version — Bot version, Node.js version, uptime', - '/deploy — Trigger live deploy from latest git commit', - '/deploy_status — Last deploy result and timing', - '/logs [lines] — Last N lines of bot log', - '/admin_log [lines] — Admin event log (NDJSON)', - '/admin_backup — Snapshot runtime state', + '🧪 BUGS + TESTS + SYSTEM COMMANDS', + 'Moved to Admin Dashboard → Tests & Bugs / System Tools menus for easier use.', '', 'ℹ️ All commands include smart errors and usage hints.', 'ℹ️ Use /on to enable admin view, /off to hide admin UI.', ].join('\n'), buttons: [ - [Markup.button.callback('🛠 Open Admin Dashboard', 'open_admin_dashboard'), Markup.button.callback('🧪 Run TestAll', 'admin_cmd_testall')], - [Markup.button.callback('🐞 Bug Reports', 'pamenu_bug_reports')], + [Markup.button.callback('🛠 Open Admin Dashboard', 'open_admin_dashboard'), Markup.button.callback('🧪 Tests & Bugs', 'admin_cat_tests')], navRow(6, total), [Markup.button.callback('⬅️ Menu', 'to_main_menu')], ], @@ -3337,6 +5624,26 @@ function buildUsernameConfirmPrompt(normalized) { }; } +/** + + * sendLinkAccountPrompt executes its scoped Runewager logic and participates in menu/command or utility flow composition. + + * Parameters: See the function signature for exact argument names and accepted values. + + * Returns: Returns the computed value or a Promise resolving to the operation result; may return void for side-effect handlers. + + * Side effects: May mutate runtime stores, pendingAction state, menu state, persistence files, logs, and callback progression. + + * Validation/safety: Uses existing guard utilities (admin checks, input checks, path checks, cooldown checks) where applicable. + + * Timeouts/fallbacks: Timeout and fallback behavior are controlled by the calling flow and global handler/state machine conventions. + + * Errors: Surfaces user-facing error replies and/or logs when inputs, permissions, or dependencies are invalid. + + * System fit: This function is part of the Runewager command/callback/state orchestration pipeline. + + */ + async function sendLinkAccountPrompt(ctx, user) { const alreadyLinked = user.runewagerUsername && user.runewagerUsername.trim(); const header = alreadyLinked @@ -3430,12 +5737,150 @@ bot.command('sshv', async (ctx) => { // → Send Channel Only → channel → state.none // ========================= +/** + + * parseModeLabel executes its scoped Runewager logic and participates in menu/command or utility flow composition. + + * Parameters: See the function signature for exact argument names and accepted values. + + * Returns: Returns the computed value or a Promise resolving to the operation result; may return void for side-effect handlers. + + * Side effects: May mutate runtime stores, pendingAction state, menu state, persistence files, logs, and callback progression. + + * Validation/safety: Uses existing guard utilities (admin checks, input checks, path checks, cooldown checks) where applicable. + + * Timeouts/fallbacks: Timeout and fallback behavior are controlled by the calling flow and global handler/state machine conventions. + + * Errors: Surfaces user-facing error replies and/or logs when inputs, permissions, or dependencies are invalid. + + * System fit: This function is part of the Runewager command/callback/state orchestration pipeline. + + */ + +function parseModeLabel(mode) { + return mode === 'HTML' ? 'HTML' : 'Text'; +} + +/** + + * announceBuilderKeyboard executes its scoped Runewager logic and participates in menu/command or utility flow composition. + + * Parameters: See the function signature for exact argument names and accepted values. + + * Returns: Returns the computed value or a Promise resolving to the operation result; may return void for side-effect handlers. + + * Side effects: May mutate runtime stores, pendingAction state, menu state, persistence files, logs, and callback progression. + + * Validation/safety: Uses existing guard utilities (admin checks, input checks, path checks, cooldown checks) where applicable. + + * Timeouts/fallbacks: Timeout and fallback behavior are controlled by the calling flow and global handler/state machine conventions. + + * Errors: Surfaces user-facing error replies and/or logs when inputs, permissions, or dependencies are invalid. + + * System fit: This function is part of the Runewager command/callback/state orchestration pipeline. + + */ + +function announceBuilderKeyboard(config) { + const dm = config.sendDm ? '✅ DM Users' : '☐ DM Users'; + const ch = config.sendChannel ? '✅ Channel' : '☐ Channel'; + const gr = config.sendGroup ? '✅ Group' : '☐ Group'; + return Markup.inlineKeyboard([ + [Markup.button.callback(dm, 'announce_toggle_dm'), Markup.button.callback(ch, 'announce_toggle_channel')], + [Markup.button.callback(gr, 'announce_toggle_group'), Markup.button.callback(`Mode: ${parseModeLabel(config.parseMode)}`, 'announce_toggle_mode')], + [Markup.button.callback('✏️ Edit Text', 'announce_edit')], + [Markup.button.callback('🚀 Send Broadcast Now', 'announce_send_now')], + [Markup.button.callback('❌ Cancel', 'admin_cancel')], + ]); +} + +/** + + * sendAnnouncementTargets executes its scoped Runewager logic and participates in menu/command or utility flow composition. + + * Parameters: See the function signature for exact argument names and accepted values. + + * Returns: Returns the computed value or a Promise resolving to the operation result; may return void for side-effect handlers. + + * Side effects: May mutate runtime stores, pendingAction state, menu state, persistence files, logs, and callback progression. + + * Validation/safety: Uses existing guard utilities (admin checks, input checks, path checks, cooldown checks) where applicable. + + * Timeouts/fallbacks: Timeout and fallback behavior are controlled by the calling flow and global handler/state machine conventions. + + * Errors: Surfaces user-facing error replies and/or logs when inputs, permissions, or dependencies are invalid. + + * System fit: This function is part of the Runewager command/callback/state orchestration pipeline. + + */ + +async function sendAnnouncementTargets(ctx, announcementText, config) { + let sentDm = 0; + let failedDm = 0; + const sendOpts = config.parseMode === 'HTML' ? { parse_mode: 'HTML' } : {}; + + if (config.sendDm) { + for (const [, u] of userStore) { + if (u.optOutBroadcasts || u.muted) continue; + try { + // eslint-disable-next-line no-await-in-loop + await ctx.telegram.sendMessage(u.id, announcementText, sendOpts); + sentDm += 1; + } catch (_) { + failedDm += 1; + } + } + } + + let channelStatus = 'skipped'; + if (config.sendChannel && config.targetChannel) { + try { + await ctx.telegram.sendMessage(config.targetChannel, announcementText, sendOpts); + channelStatus = `sent (${config.targetChannel})`; + } catch (e) { + channelStatus = `failed (${e.message})`; + } + } + + let groupStatus = 'skipped'; + if (config.sendGroup && config.targetGroup) { + try { + await ctx.telegram.sendMessage(config.targetGroup, announcementText, sendOpts); + groupStatus = `sent (${config.targetGroup})`; + } catch (e) { + groupStatus = `failed (${e.message})`; + } + } + + return { sentDm, failedDm, channelStatus, groupStatus }; +} + +/** + + * handleAnnounceCommand executes its scoped Runewager logic and participates in menu/command or utility flow composition. + + * Parameters: See the function signature for exact argument names and accepted values. + + * Returns: Returns the computed value or a Promise resolving to the operation result; may return void for side-effect handlers. + + * Side effects: May mutate runtime stores, pendingAction state, menu state, persistence files, logs, and callback progression. + + * Validation/safety: Uses existing guard utilities (admin checks, input checks, path checks, cooldown checks) where applicable. + + * Timeouts/fallbacks: Timeout and fallback behavior are controlled by the calling flow and global handler/state machine conventions. + + * Errors: Surfaces user-facing error replies and/or logs when inputs, permissions, or dependencies are invalid. + + * System fit: This function is part of the Runewager command/callback/state orchestration pipeline. + + */ + async function handleAnnounceCommand(ctx) { if (!requireAdmin(ctx)) return; const user = getUser(ctx); clearPendingAction(user); user.pendingAction = { type: 'await_announcement_text' }; - await ctx.reply('Okay! What would you like to announce?\nYou can send normal text or HTML.'); + await ctx.reply('Okay! What would you like to announce?\nYou can send normal text or HTML. After sending text, choose targets (DM/channel/group).', Markup.inlineKeyboard([[Markup.button.callback('⬅️ Return to Admin Menu', 'open_admin_dashboard')], [Markup.button.callback('❌ Cancel', 'admin_cancel')]])); } bot.command('A', handleAnnounceCommand); @@ -3782,8 +6227,12 @@ bot.command('health', async (ctx) => { if (!requireAdmin(ctx)) return; try { const { data } = await new Promise((resolve, reject) => { - const opts = { hostname: '127.0.0.1', port, path: '/health', method: 'GET', timeout: 5000, rejectUnauthorized: false }; - const req = require('https').request(opts, (res) => { + const port = Number(process.env.PORT || 3000); + const useTls = isHealthTlsEnabled(); + const client = useTls ? require('https') : require('http'); + const opts = { hostname: '127.0.0.1', port, path: '/health', method: 'GET', timeout: 5000 }; + if (useTls) opts.rejectUnauthorized = false; + const req = client.request(opts, (res) => { let body = ''; res.on('data', (c) => { body += c; }); res.on('end', () => resolve({ status: res.statusCode, data: body })); @@ -4131,30 +6580,13 @@ bot.command('discord', async (ctx) => { }); bot.command('promo', async (ctx) => { - const user = getUser(ctx); - const activeCode = getActivePromoCodeForUser(user); - if (!activeCode) { - await ctx.reply('No promo code is active for your account tier right now.'); - return; - } - const audienceLabel = activeCode.audience === PROMO_AUDIENCE.NEW_USER ? 'New User' : 'Existing User'; - const requirementLabel = activeCode.requirementType === PROMO_REQUIREMENT_TYPE.NO_REQUIREMENT ? 'No extra wager requirement' : 'Wager requirement applies'; - await ctx.reply( - `🎁 ${audienceLabel} Promo -Code: ${activeCode.code} -Amount: ${activeCode.amountSC} SC -Requirement: ${requirementLabel} - -${AFFILIATE_REMINDER_TEXT}`, - Markup.inlineKeyboard([ - [Markup.button.url('🔗 Sign Up', LINKS.runewagerSignup)], - [Markup.button.url('📋 Enter Promo', LINKS.promoInput)], - [Markup.button.callback('❓ How to Claim', 'menu_claim_bonus')], - ]), - ); + await ctx.reply('Open the Promo Menu to see DB-backed promos you are eligible for.', Markup.inlineKeyboard([[Markup.button.callback('🎁 Open Promo Menu', 'menu_claim_bonus')]])); }); bot.command('setpromo', async (ctx) => { + await ctx.reply('Legacy /setpromo is disabled. Use 🎛 Promo Manager.'); + return; + if (!requireAdmin(ctx)) return; const user = getUser(ctx); clearPendingAction(user); @@ -4212,6 +6644,19 @@ bot.action('to_main_menu', async (ctx) => { await sendPersistentUserMenu(ctx, user); }); + +bot.action('menu_page_1', async (ctx) => { + const user = getUser(ctx); + await ctx.answerCbQuery(); + await sendMainMenu(ctx, user, 1); +}); + +bot.action('menu_page_2', async (ctx) => { + const user = getUser(ctx); + await ctx.answerCbQuery(); + await sendMainMenu(ctx, user, 2); +}); + // ── Persistent USER MAIN MENU actions (pmenu_*) ──────────────────────────── bot.action('pmenu_claim_bonus', async (ctx) => { @@ -4301,7 +6746,7 @@ bot.action('pmenu_help', async (ctx) => { await ctx.answerCbQuery(); // Show help sub-menu: Command Help Booklet + Bug Report await replaceCallbackPanel(ctx, - '❓ *Help & Support*\n\nChoose what you need:', + '❓ *Help Center*\n\nChoose a help category:', { parse_mode: 'Markdown', ...Markup.inlineKeyboard([ @@ -4320,6 +6765,18 @@ bot.action('help_open_booklet', async (ctx) => { await sendHelpMenu(ctx, user, 1); }); +bot.action(/^help_page_(\d+)$/, async (ctx) => { + const user = getUser(ctx); + await ctx.answerCbQuery(); + const page = Number(ctx.match[1]); + const pages = buildHelpPages(user); + const total = pages.length; + const safePage = Math.max(1, Math.min(total, page)); + const selected = pages[safePage - 1]; + await replaceCallbackPanel(ctx, selected.text, { parse_mode: 'Markdown', ...Markup.inlineKeyboard(selected.buttons) }); +}); + + bot.action('help_open_bugreport', async (ctx) => { const user = getUser(ctx); await ctx.answerCbQuery(); @@ -4334,114 +6791,246 @@ bot.action('help_open_bugreport', async (ctx) => { ); }); -bot.action('pmenu_admin', async (ctx) => { + +// ── Legacy/shortcut callback aliases to keep menu flows deterministic ─────── +bot.action('menu_help', async (ctx) => { const user = getUser(ctx); await ctx.answerCbQuery(); - if (!requireAdmin(ctx)) return; - await sendPersistentAdminMenu(ctx, user); + await sendHelpMenu(ctx, user, 1); }); -// ── Persistent ADMIN MAIN MENU actions (pamenu_*) ───────────────────────── - -bot.action('pamenu_status', async (ctx) => { +bot.action('open_help', async (ctx) => { const user = getUser(ctx); await ctx.answerCbQuery(); - if (!requireAdmin(ctx)) return; - await ctx.reply(buildSystemStatusText(), { - parse_mode: 'Markdown', - ...Markup.inlineKeyboard([[Markup.button.callback('↩ Admin Menu', 'pamenu_back_admin')]]), - }); - await sendPersistentAdminMenu(ctx, user); + await sendHelpMenu(ctx, user, 1); }); -bot.action('pamenu_stats', async (ctx) => { +bot.action('help_tab_admin', async (ctx) => { const user = getUser(ctx); await ctx.answerCbQuery(); - if (!requireAdmin(ctx)) return; - await ctx.reply( - '📈 *Stats Dashboard*\n\nSelect a time window:', - { parse_mode: 'Markdown', ...Markup.inlineKeyboard([ - [ - Markup.button.callback('🕐 24h', 'pamenu_stats_24h'), - Markup.button.callback('📅 7 days', 'pamenu_stats_7d'), - ], - [ - Markup.button.callback('📆 30 days', 'pamenu_stats_30d'), - Markup.button.callback('♾️ Lifetime', 'pamenu_stats_lifetime'), - ], - [Markup.button.callback('↩ Admin Menu', 'pamenu_back_admin')], - ]) }, - ); + await sendHelpMenu(ctx, user, 6); }); -bot.action('pamenu_stats_24h', async (ctx) => { - await ctx.answerCbQuery(); - if (!requireAdmin(ctx)) return; +bot.action('menu_settings_tab', async (ctx) => { const user = getUser(ctx); - const s = buildStatsWindow(24 * 60 * 60 * 1000); - await replaceCallbackPanel(ctx, buildStatsText('Last 24 hours', 24 * 60 * 60 * 1000), { parse_mode: 'Markdown', ...Markup.inlineKeyboard([[Markup.button.callback('↩ Admin Menu', 'pamenu_back_admin')]]) }); - await sendPersistentAdminMenu(ctx, user); + await ctx.answerCbQuery(); + await sendSettingsMenu(ctx, user); }); -bot.action('pamenu_stats_7d', async (ctx) => { - await ctx.answerCbQuery(); - if (!requireAdmin(ctx)) return; +bot.action('settings_toggle_playmode', async (ctx) => { const user = getUser(ctx); - await replaceCallbackPanel(ctx, buildStatsText('Last 7 days', 7 * 24 * 60 * 60 * 1000), { parse_mode: 'Markdown', ...Markup.inlineKeyboard([[Markup.button.callback('↩ Admin Menu', 'pamenu_back_admin')]]) }); - await sendPersistentAdminMenu(ctx, user); + await ctx.answerCbQuery(); + user.settings.playMode = user.settings.playMode === 'browser' ? 'miniapp' : 'browser'; + await sendSettingsMenu(ctx, user); }); -bot.action('pamenu_stats_30d', async (ctx) => { - await ctx.answerCbQuery(); - if (!requireAdmin(ctx)) return; +bot.action('settings_toggle_quick_commands', async (ctx) => { const user = getUser(ctx); - await replaceCallbackPanel(ctx, buildStatsText('Last 30 days', 30 * 24 * 60 * 60 * 1000), { parse_mode: 'Markdown', ...Markup.inlineKeyboard([[Markup.button.callback('↩ Admin Menu', 'pamenu_back_admin')]]) }); - await sendPersistentAdminMenu(ctx, user); + await ctx.answerCbQuery(); + user.settings.showQuickCommands = !user.settings.showQuickCommands; + await sendSettingsMenu(ctx, user); }); -bot.action('pamenu_stats_lifetime', async (ctx) => { - await ctx.answerCbQuery(); - if (!requireAdmin(ctx)) return; +bot.action('settings_toggle_tooltips', async (ctx) => { const user = getUser(ctx); - await ctx.reply(buildStatsText('Lifetime (all time)', 0), { parse_mode: 'Markdown', ...Markup.inlineKeyboard([[Markup.button.callback('↩ Admin Menu', 'pamenu_back_admin')]]) }); - await sendPersistentAdminMenu(ctx, user); + await ctx.answerCbQuery(); + user.settings.tooltipsEnabled = !user.settings.tooltipsEnabled; + await sendSettingsMenu(ctx, user); }); -bot.action('pamenu_start_giveaway', async (ctx) => { +bot.action('menu_qc_play', async (ctx) => { const user = getUser(ctx); await ctx.answerCbQuery(); - if (!requireAdmin(ctx)) return; - await gwizStart(ctx, user, {}); + await ctx.reply('🎮 Open Runewager:', Markup.inlineKeyboard([[miniAppPlayButton()], [Markup.button.callback('⬅️ Main Menu', 'to_main_menu')]])); }); -bot.action('pamenu_active_giveaways', async (ctx) => { - const user = getUser(ctx); +bot.action('menu_qc_profile', async (ctx) => { await ctx.answerCbQuery(); - if (!requireAdmin(ctx)) return; - const running = Array.from(giveawayStore.running.values()); - const text = buildActiveGiveawaysText(); - const keyboard = activeGiveawaysKeyboard(running); - await ctx.reply(text, { parse_mode: 'Markdown', ...keyboard }); + await ctx.reply('👤 Open your profile in the Runewager mini app:', Markup.inlineKeyboard([[Markup.button.url('👤 Open Profile', LINKS.miniAppProfile)], [Markup.button.callback('⬅️ Main Menu', 'to_main_menu')]])); }); -bot.action('pamenu_tools', async (ctx) => { +bot.action('menu_qc_status', async (ctx) => { const user = getUser(ctx); await ctx.answerCbQuery(); - if (!requireAdmin(ctx)) return; - await ctx.reply( - '🛠 *Admin Tools*\n\nSelect a tool:', - { parse_mode: 'Markdown', ...toolsMenuKeyboard() }, - ); + await ctx.reply(buildUserStatusLine(user), Markup.inlineKeyboard([[Markup.button.callback('⬅️ Main Menu', 'to_main_menu')]])); }); -bot.action('pamenu_admin_help', async (ctx) => { +bot.action('w30_request_start', async (ctx) => { const user = getUser(ctx); await ctx.answerCbQuery(); - if (!requireAdmin(ctx)) return; - await sendHelpMenu(ctx, user, 6); + await showBonusStatus(ctx, user); }); -bot.action('pamenu_bug_reports', async (ctx) => { +bot.action('w30_bonus_info', async (ctx) => { + await ctx.answerCbQuery(); + await ctx.reply( + `🎯 *30 SC Wager Bonus*\n\n• One-time bonus\n• Requires 3,000 SC wager in a rolling 7-day period\n• Manual admin review/credit`, + { parse_mode: 'Markdown', ...Markup.inlineKeyboard([[Markup.button.callback('🎯 Request Bonus', 'w30_request_start')], [Markup.button.callback('⬅️ Main Menu', 'to_main_menu')]]) }, + ); +}); + +bot.action('w30_rules', async (ctx) => { + await ctx.answerCbQuery(); + await ctx.reply( + `📋 *30 SC Bonus Rules*\n\n1) Link your Runewager username\n2) Stay under GambleCodez affiliate\n3) Wager at least 3,000 SC in 7-day rolling window\n4) Submit request via the bonus button\n5) Admin manually verifies and credits`, + { parse_mode: 'Markdown', ...Markup.inlineKeyboard([[Markup.button.callback('🎯 Request Bonus', 'w30_request_start')], [Markup.button.callback('⬅️ Main Menu', 'to_main_menu')]]) }, + ); +}); + +bot.action('admin_cmd_tips_dashboard', async (ctx) => { + if (!requireAdmin(ctx)) return; + await ctx.answerCbQuery(); + await handleTipsCommand(ctx); +}); + +bot.action('admin_cmd_announce_start', async (ctx) => { + if (!requireAdmin(ctx)) return; + await ctx.answerCbQuery(); + await handleAnnounceCommand(ctx); +}); + +bot.action('admin_cmd_tiptest', async (ctx) => { + if (!requireAdmin(ctx)) return; + await ctx.answerCbQuery(); + const enabled = tipsStore.tips.filter((t) => t.enabled); + if (!enabled.length) { await ctx.reply('No enabled content drops to test.'); return; } + const pool = enabled.length > 1 && tipsStore.lastSentTipId != null + ? enabled.filter((t) => t.id !== tipsStore.lastSentTipId) + : enabled; + const tip = pool[Math.floor(Math.random() * pool.length)]; + const sendResult = await postTipToConfiguredTarget(tip, bot.telegram); + if (sendResult.ok) { + tipsStore.lastSentTipId = tip.id; + persistRuntimeState(); + saveHelpfulMessages(); + await ctx.reply(`✅ Test content drop posted to ${sendResult.target} (silent).`, Markup.inlineKeyboard([[Markup.button.callback('💡 Open Content Drops Manager', 'admin_cmd_tips_dashboard')], [Markup.button.callback('⬅️ Admin Dashboard', 'open_admin_dashboard')]])); + } else { + await ctx.reply(`❌ Failed to post test content drop. ${sendResult.error ? sendResult.error.message : 'Unknown error'}\nTip: use /register_chat or forward any message from the target group/channel here to auto-register.`); + } +}); + +bot.action('admin_broadcast', async (ctx) => { + if (!requireAdmin(ctx)) return; + await ctx.answerCbQuery(); + await replaceCallbackPanel(ctx, 'Legacy promo broadcast is disabled. Use Announcements instead.', { + ...Markup.inlineKeyboard([[Markup.button.callback('📣 Announcements', 'admin_cmd_announce_start')], [Markup.button.callback('⬅️ Promo Tools', 'admin_cat_promo')]]), + }); +}); + +bot.action('admin_cancel', async (ctx) => { + if (!requireAdmin(ctx)) return; + await ctx.answerCbQuery('Cancelled'); + await sendPersistentAdminMenu(ctx, getUser(ctx)); +}); + +bot.action('pmenu_admin', async (ctx) => { + const user = getUser(ctx); + await ctx.answerCbQuery(); + if (!requireAdmin(ctx)) return; + await sendPersistentAdminMenu(ctx, user); +}); + +// ── Persistent ADMIN MAIN MENU actions (pamenu_*) ───────────────────────── + +bot.action('pamenu_status', async (ctx) => { + const user = getUser(ctx); + await ctx.answerCbQuery(); + if (!requireAdmin(ctx)) return; + await ctx.reply(buildSystemStatusText(), { + parse_mode: 'Markdown', + ...Markup.inlineKeyboard([[Markup.button.callback('↩ Admin Menu', 'pamenu_back_admin')]]), + }); + await sendPersistentAdminMenu(ctx, user); +}); + +bot.action('pamenu_stats', async (ctx) => { + const user = getUser(ctx); + await ctx.answerCbQuery(); + if (!requireAdmin(ctx)) return; + await ctx.reply( + '📈 *Stats Dashboard*\n\nSelect a time window:', + { parse_mode: 'Markdown', ...Markup.inlineKeyboard([ + [ + Markup.button.callback('🕐 24h', 'pamenu_stats_24h'), + Markup.button.callback('📅 7 days', 'pamenu_stats_7d'), + ], + [ + Markup.button.callback('📆 30 days', 'pamenu_stats_30d'), + Markup.button.callback('♾️ Lifetime', 'pamenu_stats_lifetime'), + ], + [Markup.button.callback('↩ Admin Menu', 'pamenu_back_admin')], + ]) }, + ); +}); + +bot.action('pamenu_stats_24h', async (ctx) => { + await ctx.answerCbQuery(); + if (!requireAdmin(ctx)) return; + const user = getUser(ctx); + const s = buildStatsWindow(24 * 60 * 60 * 1000); + await replaceCallbackPanel(ctx, buildStatsText('Last 24 hours', 24 * 60 * 60 * 1000), { parse_mode: 'Markdown', ...Markup.inlineKeyboard([[Markup.button.callback('↩ Admin Menu', 'pamenu_back_admin')]]) }); + await sendPersistentAdminMenu(ctx, user); +}); + +bot.action('pamenu_stats_7d', async (ctx) => { + await ctx.answerCbQuery(); + if (!requireAdmin(ctx)) return; + const user = getUser(ctx); + await replaceCallbackPanel(ctx, buildStatsText('Last 7 days', 7 * 24 * 60 * 60 * 1000), { parse_mode: 'Markdown', ...Markup.inlineKeyboard([[Markup.button.callback('↩ Admin Menu', 'pamenu_back_admin')]]) }); + await sendPersistentAdminMenu(ctx, user); +}); + +bot.action('pamenu_stats_30d', async (ctx) => { + await ctx.answerCbQuery(); + if (!requireAdmin(ctx)) return; + const user = getUser(ctx); + await replaceCallbackPanel(ctx, buildStatsText('Last 30 days', 30 * 24 * 60 * 60 * 1000), { parse_mode: 'Markdown', ...Markup.inlineKeyboard([[Markup.button.callback('↩ Admin Menu', 'pamenu_back_admin')]]) }); + await sendPersistentAdminMenu(ctx, user); +}); + +bot.action('pamenu_stats_lifetime', async (ctx) => { + await ctx.answerCbQuery(); + if (!requireAdmin(ctx)) return; + const user = getUser(ctx); + await ctx.reply(buildStatsText('Lifetime (all time)', 0), { parse_mode: 'Markdown', ...Markup.inlineKeyboard([[Markup.button.callback('↩ Admin Menu', 'pamenu_back_admin')]]) }); + await sendPersistentAdminMenu(ctx, user); +}); + +bot.action('pamenu_start_giveaway', async (ctx) => { + const user = getUser(ctx); + await ctx.answerCbQuery(); + if (!requireAdmin(ctx)) return; + await gwizStart(ctx, user, {}); +}); + +bot.action('pamenu_active_giveaways', async (ctx) => { + const user = getUser(ctx); + await ctx.answerCbQuery(); + if (!requireAdmin(ctx)) return; + const running = Array.from(giveawayStore.running.values()); + const text = buildActiveGiveawaysText(); + const keyboard = activeGiveawaysKeyboard(running); + await replaceCallbackPanel(ctx, text, { parse_mode: 'Markdown', ...keyboard }); +}); + +bot.action('pamenu_tools', async (ctx) => { + const user = getUser(ctx); + await ctx.answerCbQuery(); + if (!requireAdmin(ctx)) return; + await ctx.reply( + '🛠 *Admin Tools*\n\nSelect a tool:', + { parse_mode: 'Markdown', ...toolsMenuKeyboard() }, + ); +}); + +bot.action('pamenu_admin_help', async (ctx) => { + const user = getUser(ctx); + await ctx.answerCbQuery(); + if (!requireAdmin(ctx)) return; + await sendHelpMenu(ctx, user, 6); +}); + +bot.action('pamenu_bug_reports', async (ctx) => { await ctx.answerCbQuery(); if (!requireAdmin(ctx)) return; const reports = (promoStore.bugreports || []).filter((r) => r.status === 'open'); @@ -4482,6 +7071,11 @@ bot.action('pamenu_back_admin', async (ctx) => { const user = getUser(ctx); await ctx.answerCbQuery(); if (!requireAdmin(ctx)) return; + try { + await ctx.deleteMessage(); + } catch (_) { + try { await ctx.editMessageReplyMarkup({ inline_keyboard: [] }); } catch (_) { /* ignore */ } + } await sendPersistentAdminMenu(ctx, user); }); @@ -4515,10 +7109,13 @@ bot.action('pamenu_tools_health', async (ctx) => { await ctx.answerCbQuery('Running health check...'); if (!requireAdmin(ctx)) return; try { - const httpsClient = require('https'); + const useTls = isHealthTlsEnabled(); + const httpClient = useTls ? require('https') : require('http'); const port = Number(process.env.PORT || 3000); const result = await new Promise((resolve, reject) => { - const req = httpsClient.get({ hostname: '127.0.0.1', port, path: '/health', rejectUnauthorized: false }, (res) => { + const opts = { hostname: '127.0.0.1', port, path: '/health' }; + if (useTls) opts.rejectUnauthorized = false; + const req = httpClient.get(opts, (res) => { let data = ''; res.on('data', (chunk) => { data += chunk; }); res.on('end', () => resolve(data)); @@ -4610,6 +7207,16 @@ bot.action('age_yes', async (ctx) => { }); // Shows the mandatory GambleCodez affiliate code step +/** + * sendGambleCodezVIPStep executes its scoped Runewager logic and participates in menu/command or utility flow composition. + * Parameters: See the function signature for exact argument names and accepted values. + * Returns: Returns the computed value or a Promise resolving to the operation result; may return void for side-effect handlers. + * Side effects: May mutate runtime stores, pendingAction state, menu state, persistence files, logs, and callback progression. + * Validation/safety: Uses existing guard utilities (admin checks, input checks, path checks, cooldown checks) where applicable. + * Timeouts/fallbacks: Timeout and fallback behavior are controlled by the calling flow and global handler/state machine conventions. + * Errors: Surfaces user-facing error replies and/or logs when inputs, permissions, or dependencies are invalid. + * System fit: This function is part of the Runewager command/callback/state orchestration pipeline. + */ async function sendGambleCodezVIPStep(ctx, user) { if (user.sawGambleCodezStep) { await sendPersistentUserMenu(ctx, user); @@ -4718,549 +7325,300 @@ bot.action('menu_verify_account', async (ctx) => { bot.action('verified_yes', async (ctx) => { // User self-reported they pasted their code in Discord #web-link. // Runewager's Discord bot handles confirmation — this bot just records the step. - const user = getUser(ctx); - user.verified = true; - addBadge(user, 'account_confirmed'); - user.profileXP += 20; - trackOnboardingProgress(user); - await ctx.answerCbQuery('Step confirmed.'); - await reactToMessage(ctx, '👍'); - await ctx.reply(COPY.accountReady, Markup.inlineKeyboard([ - [Markup.button.url('🎮 Open Mini App', LINKS.miniAppPlay)], - [Markup.button.url('📋 Enter Promo Code (Mini App)', LINKS.miniAppClaim)], - [Markup.button.callback('🔗 Link Runewager Username', 'menu_link_runewager')], - [Markup.button.callback('⬅️ Main Menu', 'to_main_menu')], - ])); -}); - -// Link routing: Runewager pages via Mini App or browser button; Discord links always browser - -bot.action('menu_link_runewager', async (ctx) => { - const user = getUser(ctx); - clearPendingAction(user); - await ctx.answerCbQuery(); - await sendLinkAccountPrompt(ctx, user); -}); - -bot.action('cancel_link', async (ctx) => { - const user = getUser(ctx); - clearPendingAction(user); - await ctx.answerCbQuery('Cancelled.'); - await ctx.reply('Link cancelled.', Markup.inlineKeyboard([[Markup.button.callback('⬅️ Main Menu', 'to_main_menu')]])); -}); - -// Onboarding "Continue to Next Step" — triggered after username is linked -bot.action('onboarding_next_step', async (ctx) => { - const user = getUser(ctx); - await ctx.answerCbQuery(); - const nextStep = getNextOnboardingStep(user); - if (nextStep >= 5) { - await ctx.reply('🎉 Onboarding complete! Use /bonus to claim your 30 SC bonus.', Markup.inlineKeyboard([[Markup.button.callback('🎯 Claim 30 SC Bonus', 'w30_request_start')]])); - return; - } - await showOnboardingPrompt(ctx, user, nextStep); -}); - -// Shared handler: finalise username link after user confirmation -async function finaliseUsernameLink(ctx, user, normalized, returnTo) { - const wasLinked = Boolean(user.runewagerUsername && user.runewagerUsername.trim()); - user.runewagerUsername = normalized; - user.pendingAction = null; - addBadge(user, 'linked_profile'); - if (!wasLinked) user.profileXP += 10; - trackOnboardingProgress(user); - await reactToMessage(ctx, '🔗'); - - // If triggered mid-flow (e.g. bonus request), resume that flow - if (returnTo === 'w30_bonus') { - await ctx.reply(`✅ Username linked: \`${escapeMarkdownV2(normalized)}\`\n\n${AFFILIATE_REMINDER_TEXT}\n\nContinuing your bonus request…`, { parse_mode: 'Markdown' }); - const check = checkBonusEligibility(user); - if (check.ok) { - await submitBonusRequest(ctx, user); - } else if (check.needsWager) { - user.pendingAction = { type: 'w30_await_wager_total' }; - await ctx.reply( - 'What is your total SC wagered in the past 7 days?\n\n' - + 'Enter the number only (e.g. 3500). No screenshots needed — admin verifies this on Runewager.', - ); - } else { - await ctx.reply(check.reason); - } - return; - } - - const nextStep = getNextOnboardingStep(user); - const nextStepText = nextStep < 5 - ? `\n\n➡️ *Next step:* ${onboardingStepLabel(nextStep)}` - : '\n\n🎉 *Onboarding complete!* You can now request the 30 SC bonus.'; - const nextStepButton = nextStep < 5 - ? [[Markup.button.callback(`➡️ Continue to Next Step — ${onboardingStepLabel(nextStep)}`, 'onboarding_next_step')]] - : [[Markup.button.callback('🎯 Claim 30 SC Bonus Now', 'w30_request_start')]]; - await ctx.reply( - `✅ *Runewager account linked!*\n\n👤 Username: \`${escapeMarkdownV2(normalized)}\`\n\n${AFFILIATE_REMINDER_TEXT}${nextStepText}`, - { - parse_mode: 'Markdown', - ...Markup.inlineKeyboard([ - ...nextStepButton, - [Markup.button.callback('🎯 Request 30 SC Bonus', 'w30_request_start')], - [Markup.button.callback('👤 View Profile', 'menu_profile_action')], - [Markup.button.callback('⬅️ Main Menu', 'to_main_menu')], - ]), - }, - ); -} - -// "Yes, Continue" — user confirms the displayed username is correct -bot.action('confirm_yes_username', async (ctx) => { - const user = getUser(ctx); - const action = user.pendingAction; - - if (!action || action.type !== 'await_username_confirm') { - await ctx.answerCbQuery('Nothing to confirm.'); - return; - } - await ctx.answerCbQuery('Username confirmed!'); - await finaliseUsernameLink(ctx, user, action.data.username, action.data.returnTo || null); -}); - -// "No, Edit" — return user to WAITING_FOR_USERNAME -bot.action('confirm_no_username', async (ctx) => { - const user = getUser(ctx); - const action = user.pendingAction; - const returnTo = (action && action.data && action.data.returnTo) || null; - user.pendingAction = { type: 'await_runewager_username', returnTo }; - await ctx.answerCbQuery("No problem — let's try again."); - await ctx.reply( - '✏️ *Re-enter your Runewager username*\n\n' - + '*Use the command:*\n`/link YourUsername`\n' - + '*Example:* `/link GambleCodez`\n\n' - + 'Or just type your exact username here and send it.', - { - parse_mode: 'Markdown', - ...Markup.inlineKeyboard([ - [Markup.button.url('👤 Find My Username on Runewager', LINKS.runewagerProfile)], - [Markup.button.callback('❌ Cancel', 'cancel_link')], - ]), - }, - ); -}); - -// Legacy alias kept for backward compat with any existing keyboard buttons -bot.action('confirm_smart_username', async (ctx) => { - const user = getUser(ctx); - const action = user.pendingAction; - if (!action || action.type !== 'await_username_confirm') { - await ctx.answerCbQuery('Nothing to confirm.'); - return; - } - await ctx.answerCbQuery('Username linked!'); - await finaliseUsernameLink(ctx, user, action.data.username, action.data.returnTo || null); -}); - -bot.action('menu_join_channel', async (ctx) => { - const user = getUser(ctx); - user.hasJoinedChannel = true; - trackOnboardingProgress(user); - await ctx.answerCbQuery('Channel join intent saved.'); - await ctx.reply( - '✅ Great! You are marked as joined for GambleCodezDrops eligibility checks.', - Markup.inlineKeyboard([[Markup.button.url('📢 Open GambleCodezDrops', LINKS.gczChannel)]]), - ); -}); - -bot.action('menu_join_group', async (ctx) => { - const user = getUser(ctx); - user.hasJoinedGroup = true; - trackOnboardingProgress(user); - await ctx.answerCbQuery('Group join intent saved.'); - await ctx.reply( - '✅ Great! You are marked as joined for GambleCodezPrizeHub eligibility checks.', - Markup.inlineKeyboard([[Markup.button.url('💬 Open GambleCodezPrizeHub', LINKS.gczGroup)]]), - ); -}); - -bot.action('menu_claim_bonus', async (ctx) => { - const user = getUser(ctx); - clearPendingAction(user); - await ctx.answerCbQuery(); - if (!user.ageConfirmed) { - await ctx.reply(COPY.ageGate, { parse_mode: 'Markdown', ...ageGateKeyboard() }); - return; - } - if (needsLinkedUsername(user)) { - await linkedUsernameError(ctx); - return; - } - if (!promoStore.active) { - await ctx.reply('Promo claims are currently paused. Please check back soon.'); - return; - } - if (promoStore.remainingClaims <= 0) { - await ctx.reply('Promo claims are currently exhausted.'); - return; - } - - const activeCode = getActivePromoCodeForUser(user); - if (!activeCode) { - await ctx.reply(user.claimedPromo - ? 'No existing-user promo code is active yet. Please check back soon.' - : 'No new-user promo code is active yet. Please check back soon.'); - return; - } - - const cooldownRemaining = getPromoClaimCooldownRemainingMs(user); - if (cooldownRemaining > 0) { - await ctx.reply(`⏳ Promo cooldown active. Please wait *${formatCooldown(cooldownRemaining)}* before claiming another code.`, { parse_mode: 'Markdown' }); - return; - } - - const audienceLabel = activeCode.audience === PROMO_AUDIENCE.NEW_USER ? 'NEW USER BONUS — ONCE PER ACCOUNT/IP' : 'EXISTING USER BONUS CODE'; - const requirementLabel = activeCode.requirementType === PROMO_REQUIREMENT_TYPE.NO_REQUIREMENT - ? 'No additional wagering requirement for this code.' - : 'This code includes a wager requirement as listed in your account terms.'; - - await ctx.reply( - `🎁 ${audienceLabel} -Promo Code: ${activeCode.code} -Amount: ${activeCode.amountSC} SC each - -${AFFILIATE_REMINDER_TEXT} - -Requirement: ${requirementLabel} - -Steps: -1) Stay registered under GambleCodez affiliate -2) Open the affiliate/promo page -3) Enter the promo code shown above`, - { - parse_mode: 'Markdown', - ...Markup.inlineKeyboard([ - [signupButton()], - [promoButton()], - [Markup.button.callback('ℹ️ Tell me more about the 30 SC bonus', 'w30_bonus_info')], - [Markup.button.callback('✅ I Entered Promo Code', 'promo_confirm_claimed')], - ]), - }, - ); - await sendPhoto(ctx, IMG_PROMO_ENTRY, '📸 Step 3: Enter the promo code here (Menu → Refer a Friend → Promo entry box)'); -}); - - -function applyPromoClaimIntent(user) { - const activeCode = getActivePromoCodeForUser(user); - if (!activeCode) { - return { ok: false, message: user.claimedPromo ? 'No existing-user promo codes are active right now.' : 'No new-user promo codes are active right now.' }; - } - if (activeCode.audience === PROMO_AUDIENCE.NEW_USER && user.claimedPromo) { - return { ok: false, message: 'You already claimed a new-user code. Only existing-user codes are now available.' }; - } - - const cooldownRemaining = getPromoClaimCooldownRemainingMs(user); - if (cooldownRemaining > 0) { - return { ok: false, message: `Promo cooldown active. Please wait ${formatCooldown(cooldownRemaining)} before claiming another code.` }; - } - - if (promoStore.remainingClaims <= 0 || !promoStore.active) { - return { ok: false, message: 'This promo is not available right now.' }; - } - - if (!promoStore.claimsByUser.has(user.id)) { - promoStore.claimsByUser.add(user.id); - promoStore.remainingClaims = Math.max(0, promoStore.remainingClaims - 1); - } - - user.claimedPromo = true; - if (!user.claimedPromoAt) user.claimedPromoAt = Date.now(); - user.lastAnyPromoClaimAt = Date.now(); - if (!Array.isArray(user.claimedPromoCodes)) user.claimedPromoCodes = []; - user.claimedPromoCodes.push({ - codeId: activeCode.id, - code: activeCode.code, - audience: activeCode.audience, - requirementType: activeCode.requirementType, - ts: Date.now(), - }); - if (user.claimedPromoCodes.length > 50) user.claimedPromoCodes = user.claimedPromoCodes.slice(-50); - - registerPromoClaim(user, activeCode.code || 'unknown'); - addBadge(user, 'promo_claimed'); - user.profileXP += 20; - trackAnalytics('promo_claimed', { - userId: user.id, - code: activeCode.code, - audience: activeCode.audience, - requirementType: activeCode.requirementType, - }); - adminLog('promo_claim_click', { - userId: user.id, - code: activeCode.code, - codeId: activeCode.id, - audience: activeCode.audience, - requirementType: activeCode.requirementType, - }); - trackOnboardingProgress(user); - return { ok: true, code: activeCode }; -} - - -bot.action('promo_confirm_claimed', async (ctx) => { - const user = getUser(ctx); - if (!consumeSmartButton(user.id, 'promo_confirm_claimed')) { - await ctx.answerCbQuery('Already processed'); - return; - } - - const result = applyPromoClaimIntent(user); - if (!result.ok) { - await ctx.answerCbQuery(); - await ctx.reply(result.message); - return; - } - - await ctx.answerCbQuery('Saved'); - await ctx.reply( - '✅ Promo claim intent saved! If you need help, contact @GambleCodez on Telegram or use the support link below.', - Markup.inlineKeyboard([ - [Markup.button.url('🆘 Runewager Support', LINKS.rwDiscordSupport)], - [Markup.button.callback('↩ Back to Menu', 'to_main_menu')], - ]), - ); - await reactToMessage(ctx, '🎁'); - await sendPersistentUserMenu(ctx, user); -}); - -bot.action('promo_confirm_claimed_next', async (ctx) => { - const user = getUser(ctx); - if (!consumeSmartButton(user.id, 'promo_confirm_claimed_next')) { - await ctx.answerCbQuery('Already processed'); - return; - } - - const result = applyPromoClaimIntent(user); - if (!result.ok) { - await ctx.answerCbQuery(); - await ctx.reply(result.message); - return; - } - - await ctx.answerCbQuery('Saved'); - await reactToMessage(ctx, '🎁'); - const nextStep = getNextOnboardingStep(user); - if (nextStep >= 5) { - await ctx.reply('🎉 Onboarding complete! Use /bonus to claim your 30 SC bonus.', Markup.inlineKeyboard([[Markup.button.callback('🎯 Claim 30 SC Bonus', 'w30_request_start')]])); - return; - } - await showOnboardingPrompt(ctx, user, nextStep); -}); - -bot.action('w30_rules', async (ctx) => { - await ctx.answerCbQuery(); - await ctx.reply(WAGER_BONUS_RULES_TEXT, { - parse_mode: 'Markdown', - ...Markup.inlineKeyboard([ - [Markup.button.callback('🎯 Request My 30 SC Bonus', 'w30_request_start')], - [Markup.button.callback('⬅️ Back', 'to_main_menu')], - ]), - }); -}); - -// "Tell me more about the 30 SC bonus" — full explanation with affiliate reminder -bot.action('w30_bonus_info', async (ctx) => { - await ctx.answerCbQuery(); - const user = getUser(ctx); - const wb = user.wagerBonus || {}; - const attemptsLeft = 3 - (wb.attempts || 0); - const infoText = [ - '🎯 *GambleCodez 30 SC Bonus — Full Information*', - '', - AFFILIATE_REMINDER_TEXT, - '', - '*Requirements:*', - '• Must be under the *GambleCodez* affiliate', - '• Must wager *3,000 SC* in any rolling 7-day period', - '• Bonus is *once per account* (limit one per player)', - `• You may submit *up to 3 verification attempts* (you have *${attemptsLeft}* remaining)`, - '', - '*Process:*', - '• Bonus: *30 SC*', - '• Admin manually reviews and approves', - '• Admin is pinged the moment you request verification', - '• Admin sends the bonus directly on Runewager — the bot does NOT send SC', - '', - '⚠️ Do not message admins repeatedly. Requests are reviewed in order.', - ].join('\n'); - await ctx.reply(infoText, { - parse_mode: 'Markdown', - ...Markup.inlineKeyboard([ - [Markup.button.callback('🎯 Request My 30 SC Bonus', 'w30_request_start')], - [Markup.button.callback('🔗 Link Runewager Username', 'menu_link_runewager')], - [Markup.button.callback('⬅️ Back to Menu', 'to_main_menu')], - ]), - }); -}); - -bot.action('w30_request_start', async (ctx) => { - const user = getUser(ctx); - - if (!consumeSmartButton(user.id, 'w30_request_start')) { - await ctx.answerCbQuery('Already processed'); - return; - } - - clearPendingAction(user); - - await ctx.answerCbQuery(); - - // Run eligibility checks 1–3 (not wager yet — wager may need input) - const check = checkBonusEligibility(user); - - // Username not linked — show /link instruction then resume the bonus flow - if (!check.ok && check.needsUsername) { - user.pendingAction = { type: 'await_runewager_username', returnTo: 'w30_bonus' }; - await ctx.reply( - '🔗 *One quick step first — link your Runewager username*\n\n' - + '*Use the command:*\n`/link YourUsername`\n' - + '*Example:* `/link GambleCodez`\n\n' - + `${AFFILIATE_REMINDER_TEXT}\n\n` - + '⚠️ You will be asked to confirm your username before it is saved.', - { - parse_mode: 'Markdown', - ...Markup.inlineKeyboard([ - [Markup.button.url('👤 Find My Username on Runewager', LINKS.runewagerProfile)], - [Markup.button.callback('ℹ️ Tell me more about the 30 SC bonus', 'w30_bonus_info')], - [Markup.button.callback('❌ Cancel', 'cancel_link')], - ]), - }, - ); - return; - } - - if (!check.ok && !check.needsWager) { - await ctx.reply(check.reason); - return; - } - - if (check.needsWager) { - // Ask user to declare their 7-day wager total (no screenshots; admin verifies on Runewager) - user.pendingAction = { type: 'w30_await_wager_total' }; - await ctx.reply( - 'What is your total SC wagered in the past 7 days?\n\n' - + 'Enter the number only (e.g. 3500). No screenshots needed — admin verifies this on Runewager.', - ); - return; - } - - await submitBonusRequest(ctx, user); -}); - -bot.action('menu_settings', async (ctx) => { - await ctx.answerCbQuery(); - await ctx.reply('Quick access links:', Markup.inlineKeyboard([ + const user = getUser(ctx); + user.verified = true; + addBadge(user, 'account_confirmed'); + user.profileXP += 20; + trackOnboardingProgress(user); + await ctx.answerCbQuery('Step confirmed.'); + await reactToMessage(ctx, '👍'); + await ctx.reply(COPY.accountReady, Markup.inlineKeyboard([ [Markup.button.url('🎮 Open Mini App', LINKS.miniAppPlay)], - [Markup.button.url('🔗 Sign Up (Browser)', LINKS.runewagerSignup)], - [Markup.button.url('📋 Promo Entry (Browser)', LINKS.promoInput)], - [Markup.button.url('📘 Runewager Discord', LINKS.rwDiscordJoin)], + [Markup.button.url('📋 Enter Promo Code (Mini App)', LINKS.miniAppClaim)], + [Markup.button.callback('🔗 Link Runewager Username', 'menu_link_runewager')], [Markup.button.callback('⬅️ Main Menu', 'to_main_menu')], ])); }); -bot.action('menu_help', async (ctx) => { - const user = getUser(ctx); - await ctx.answerCbQuery(); - await sendHelpMenu(ctx, user, 'user'); -}); +// Link routing: Runewager pages via Mini App or browser button; Discord links always browser -bot.action('open_help', async (ctx) => { +bot.action('menu_link_runewager', async (ctx) => { const user = getUser(ctx); + clearPendingAction(user); await ctx.answerCbQuery(); - await sendHelpMenu(ctx, user, 'user'); + await sendLinkAccountPrompt(ctx, user); }); -bot.action('help_tab_user', async (ctx) => { +bot.action('cancel_link', async (ctx) => { const user = getUser(ctx); - await ctx.answerCbQuery(); - await sendHelpMenu(ctx, user, 1); + clearPendingAction(user); + await ctx.answerCbQuery('Cancelled.'); + await ctx.reply('Link cancelled.', Markup.inlineKeyboard([[Markup.button.callback('⬅️ Main Menu', 'to_main_menu')]])); }); -bot.action('help_tab_admin', async (ctx) => { - if (!requireAdmin(ctx)) return; +// Onboarding "Continue to Next Step" — triggered after username is linked +bot.action('onboarding_next_step', async (ctx) => { const user = getUser(ctx); await ctx.answerCbQuery(); - await sendHelpMenu(ctx, user, 6); + const nextStep = getNextOnboardingStep(user); + if (nextStep >= 5) { + await ctx.reply('🎉 Onboarding complete! Use /bonus to claim your 30 SC bonus.', Markup.inlineKeyboard([[Markup.button.callback('🎯 Claim 30 SC Bonus', 'w30_request_start')]])); + return; + } + await showOnboardingPrompt(ctx, user, nextStep); }); -// ── Paginated help booklet ───────────────────────────────────────────────── -bot.action(/^help_page_(\d+)$/, async (ctx) => { - const user = getUser(ctx); - await ctx.answerCbQuery(); - await sendHelpMenu(ctx, user, Number(ctx.match[1])); -}); +// Shared handler: finalise username link after user confirmation +/** + * finaliseUsernameLink executes its scoped Runewager logic and participates in menu/command or utility flow composition. + * Parameters: See the function signature for exact argument names and accepted values. + * Returns: Returns the computed value or a Promise resolving to the operation result; may return void for side-effect handlers. + * Side effects: May mutate runtime stores, pendingAction state, menu state, persistence files, logs, and callback progression. + * Validation/safety: Uses existing guard utilities (admin checks, input checks, path checks, cooldown checks) where applicable. + * Timeouts/fallbacks: Timeout and fallback behavior are controlled by the calling flow and global handler/state machine conventions. + * Errors: Surfaces user-facing error replies and/or logs when inputs, permissions, or dependencies are invalid. + * System fit: This function is part of the Runewager command/callback/state orchestration pipeline. + */ +async function finaliseUsernameLink(ctx, user, normalized, returnTo) { + const wasLinked = Boolean(user.runewagerUsername && user.runewagerUsername.trim()); + user.runewagerUsername = normalized; + user.pendingAction = null; + addBadge(user, 'linked_profile'); + if (!wasLinked) user.profileXP += 10; + trackOnboardingProgress(user); + await reactToMessage(ctx, '🔗'); + // If triggered mid-flow (e.g. bonus request), resume that flow + if (returnTo === 'w30_bonus') { + await ctx.reply(`✅ Username linked: \`${escapeMarkdownV2(normalized)}\`\n\n${AFFILIATE_REMINDER_TEXT}\n\nContinuing your bonus request…`, { parse_mode: 'Markdown' }); + const check = checkBonusEligibility(user); + if (check.ok) { + await submitBonusRequest(ctx, user); + } else if (check.needsWager) { + user.pendingAction = { type: 'w30_await_wager_total' }; + await ctx.reply( + 'What is your total SC wagered in the past 7 days?\n\n' + + 'Enter the number only (e.g. 3500). No screenshots needed — admin verifies this on Runewager.', + ); + } else { + await ctx.reply(check.reason); + } + return; + } + const nextStep = getNextOnboardingStep(user); + const nextStepText = nextStep < 5 + ? `\n\n➡️ *Next step:* ${onboardingStepLabel(nextStep)}` + : '\n\n🎉 *Onboarding complete!* You can now request the 30 SC bonus.'; + const nextStepButton = nextStep < 5 + ? [[Markup.button.callback(`➡️ Continue to Next Step — ${onboardingStepLabel(nextStep)}`, 'onboarding_next_step')]] + : [[Markup.button.callback('🎯 Claim 30 SC Bonus Now', 'w30_request_start')]]; + await ctx.reply( + `✅ *Runewager account linked!*\n\n👤 Username: \`${escapeMarkdownV2(normalized)}\`\n\n${AFFILIATE_REMINDER_TEXT}${nextStepText}`, + { + parse_mode: 'Markdown', + ...Markup.inlineKeyboard([ + ...nextStepButton, + [Markup.button.callback('🎯 Request 30 SC Bonus', 'w30_request_start')], + [Markup.button.callback('👤 View Profile', 'menu_profile_action')], + [Markup.button.callback('⬅️ Main Menu', 'to_main_menu')], + ]), + }, + ); +} -// ── Menu pagination ──────────────────────────────────────────────────────── -bot.action('menu_page_1', async (ctx) => { +// "Yes, Continue" — user confirms the displayed username is correct +bot.action('confirm_yes_username', async (ctx) => { const user = getUser(ctx); - await ctx.answerCbQuery(); - await sendPersistentUserMenu(ctx, user); + const action = user.pendingAction; + + if (!action || action.type !== 'await_username_confirm') { + await ctx.answerCbQuery('Nothing to confirm.'); + return; + } + await ctx.answerCbQuery('Username confirmed!'); + await finaliseUsernameLink(ctx, user, action.data.username, action.data.returnTo || null); }); -bot.action('menu_page_2', async (ctx) => { +// "No, Edit" — return user to WAITING_FOR_USERNAME +bot.action('confirm_no_username', async (ctx) => { const user = getUser(ctx); - await ctx.answerCbQuery(); - await sendMainMenu(ctx, user, 2); + const action = user.pendingAction; + const returnTo = (action && action.data && action.data.returnTo) || null; + user.pendingAction = { type: 'await_runewager_username', returnTo }; + await ctx.answerCbQuery("No problem — let's try again."); + await ctx.reply( + '✏️ *Re-enter your Runewager username*\n\n' + + '*Use the command:*\n`/link YourUsername`\n' + + '*Example:* `/link GambleCodez`\n\n' + + 'Or just type your exact username here and send it.', + { + parse_mode: 'Markdown', + ...Markup.inlineKeyboard([ + [Markup.button.url('👤 Find My Username on Runewager', LINKS.runewagerProfile)], + [Markup.button.callback('❌ Cancel', 'cancel_link')], + ]), + }, + ); }); -// ── Settings toggles ─────────────────────────────────────────────────────── -bot.action('menu_settings_tab', async (ctx) => { +// Legacy alias kept for backward compat with any existing keyboard buttons +bot.action('confirm_smart_username', async (ctx) => { const user = getUser(ctx); - await ctx.answerCbQuery(); - await sendSettingsMenu(ctx, user); + const action = user.pendingAction; + if (!action || action.type !== 'await_username_confirm') { + await ctx.answerCbQuery('Nothing to confirm.'); + return; + } + await ctx.answerCbQuery('Username linked!'); + await finaliseUsernameLink(ctx, user, action.data.username, action.data.returnTo || null); }); -bot.action('settings_toggle_playmode', async (ctx) => { +bot.action('menu_join_channel', async (ctx) => { const user = getUser(ctx); - await ctx.answerCbQuery('Play mode updated!'); - user.settings.playMode = user.settings.playMode === 'browser' ? 'miniapp' : 'browser'; - await sendSettingsMenu(ctx, user); + user.hasJoinedChannel = true; + trackOnboardingProgress(user); + await ctx.answerCbQuery('Channel join intent saved.'); + await ctx.reply( + '✅ Great! You are marked as joined for GambleCodezDrops eligibility checks.', + Markup.inlineKeyboard([[Markup.button.url('📢 Open GambleCodezDrops', LINKS.gczChannel)]]), + ); }); -bot.action('settings_toggle_quick_commands', async (ctx) => { +bot.action('menu_join_group', async (ctx) => { const user = getUser(ctx); - await ctx.answerCbQuery('Quick commands updated!'); - user.settings.showQuickCommands = !user.settings.showQuickCommands; - await sendSettingsMenu(ctx, user); + user.hasJoinedGroup = true; + trackOnboardingProgress(user); + await ctx.answerCbQuery('Group join intent saved.'); + await ctx.reply( + '✅ Great! You are marked as joined for GambleCodezPrizeHub eligibility checks.', + Markup.inlineKeyboard([[Markup.button.url('💬 Open GambleCodezPrizeHub', LINKS.gczGroup)]]), + ); }); -bot.action('settings_toggle_tooltips', async (ctx) => { +bot.action('menu_claim_bonus', async (ctx) => { const user = getUser(ctx); - await ctx.answerCbQuery('Tooltips updated!'); - user.settings.tooltipsEnabled = !user.settings.tooltipsEnabled; - await sendSettingsMenu(ctx, user); + clearPendingAction(user); + await ctx.answerCbQuery(); + if (!user.ageConfirmed) { + await ctx.reply(COPY.ageGate, { parse_mode: 'Markdown', ...ageGateKeyboard() }); + return; + } + if (needsLinkedUsername(user)) { + await linkedUsernameError(ctx); + return; + } + + const eligiblePromos = []; + for (const promo of listActivePromos()) { + // eslint-disable-next-line no-await-in-loop + const result = await evaluatePromoEligibility(user, promo, { logFailure: true }); + if (result.eligible) eligiblePromos.push(promo); + } + + if (!eligiblePromos.length) { + await ctx.reply('No eligible promos are available for your account right now.'); + return; + } + + const rows = eligiblePromos.map((promo) => [Markup.button.callback(`🎁 ${promo.name}`, `promo_open_${promo.promo_id}`)]); + rows.push([Markup.button.callback('✅ I claimed successfully', 'promo_user_claimed_successfully')]); + rows.push([Markup.button.callback('⬅️ Main Menu', 'to_main_menu')]); + await ctx.reply(`🎁 *Promo Menu*\n\nOnly promos you are eligible for are shown below.`, { parse_mode: 'Markdown', ...Markup.inlineKeyboard(rows) }); }); -// ── Quick-command bar shortcuts ──────────────────────────────────────────── -bot.action('menu_qc_play', async (ctx) => { +bot.action('promo_user_claimed_successfully', async (ctx) => { const user = getUser(ctx); - await ctx.answerCbQuery(); - const url = user.settings.playMode === 'browser' ? LINKS.runewagerSignup : LINKS.miniAppPlay; - await ctx.reply('🎮 Open Runewager:', Markup.inlineKeyboard([[Markup.button.url('Play Runewager', url)]])); + user.hasClaimedNewUserPromo = true; + user.lastAnyPromoClaimAt = Date.now(); + await ctx.answerCbQuery('Saved'); + await ctx.reply('✅ Got it — you are marked as already claimed. New-user-only promos are now hidden for your account.'); }); -bot.action('menu_qc_profile', async (ctx) => { +bot.action(/promo_open_(\d+)/, async (ctx) => { const user = getUser(ctx); + const promoId = Number(ctx.match[1]); + const promo = getPromoById(promoId); await ctx.answerCbQuery(); - await ctx.reply('👤 Open your Runewager profile:', Markup.inlineKeyboard([[Markup.button.url('My Profile', LINKS.miniAppProfile)]])); + if (!promo || promo.status !== PROMO_STATUS.ACTIVE) { + await ctx.reply('Promo not available.'); + return; + } + const eligibility = await evaluatePromoEligibility(user, promo, { logFailure: true }); + const buttons = [[Markup.button.callback('⬅️ Promo Menu', 'menu_claim_bonus')]]; + if (eligibility.eligible) buttons.unshift([Markup.button.callback('✅ Claim Promo', `promo_claim_${promo.promo_id}`)]); + const reasonText = eligibility.eligible ? 'You are eligible to claim this promo.' : `Not eligible: ${eligibility.reasons.join('; ')}`; + await ctx.reply(`${renderPromoForUser(promo)} + +${reasonText}`, { parse_mode: 'Markdown', ...Markup.inlineKeyboard(buttons) }); + if (promo.image_url && isValidHttpUrl(promo.image_url)) { + await sendPhoto(ctx, promo.image_url, `Preview image for ${promo.name}`); + } }); -bot.action('menu_qc_status', async (ctx) => { +bot.action(/promo_claim_(\d+)/, async (ctx) => { const user = getUser(ctx); + const promoId = Number(ctx.match[1]); + const promo = getPromoById(promoId); await ctx.answerCbQuery(); - const statusLine = buildUserStatusLine(user); - await ctx.reply(`📊 Your Status\n\n${statusLine}`, Markup.inlineKeyboard([[Markup.button.callback('⬅️ Menu', 'to_main_menu')]])); + if (!promo || promo.status !== PROMO_STATUS.ACTIVE) { + await ctx.reply('Promo not available.'); + return; + } + const eligibility = await evaluatePromoEligibility(user, promo, { logFailure: true }); + if (!eligibility.eligible) { + await ctx.reply(`Cannot claim: ${eligibility.reasons.join('; ')}`); + return; + } + const claim = { + claim_id: promoManagerStore.nextClaimId++, + promo_id: promo.promo_id, + user_id: user.id, + username: user.runewagerUsername || '', + status: promo.auto_approve ? 'claimed' : 'pending_approval', + claim_reason: promo.auto_approve ? 'auto_approved' : 'pending_admin_review', + claimed_at: Date.now(), + created_at: Date.now(), + updated_at: Date.now(), + }; + promoManagerStore.claims.push(claim); + user.claimedPromo = true; + user.lastAnyPromoClaimAt = Date.now(); + if (!Array.isArray(user.claimedPromoCodes)) user.claimedPromoCodes = []; + user.claimedPromoCodes.push({ code: promo.code_or_link, promoId: promo.promo_id, ts: Date.now() }); + if (promo.auto_approve) { + await ctx.reply('✅ Promo claimed successfully.'); + } else { + await ctx.reply('🧾 Claim submitted for admin approval. You will be notified once reviewed.'); + for (const adminId of ADMIN_IDS) { + bot.telegram.sendMessage(adminId, `New promo claim pending approval. Claim #${claim.claim_id} | Promo #${promo.promo_id} | User ${user.id}`).catch(() => {}); + } + } }); -// ── Extra user menu shortcuts ────────────────────────────────────────────── +bot.command('pmapprove', safeAdminHandler('pmapprove', { usage: '/pmapprove ' }, async (ctx) => { + const claimId = Number((ctx.message.text || '').split(/\s+/)[1]); + const claim = promoManagerStore.claims.find((entry) => entry.claim_id === claimId); + if (!claim || claim.status !== 'pending_approval') return ctx.reply('Pending claim not found.'); + claim.status = 'approved'; + claim.updated_at = Date.now(); + await ctx.reply(`✅ Approved claim #${claimId}.`); + await bot.telegram.sendMessage(claim.user_id, `✅ Your promo claim #${claimId} has been approved.`).catch(() => {}); +})); + +bot.command('pmdeny', safeAdminHandler('pmdeny', { usage: '/pmdeny ' }, async (ctx) => { + const parts = (ctx.message.text || '').split(/\s+/); + const claimId = Number(parts[1]); + const reason = parts.slice(2).join(' ').trim() || 'No reason provided'; + const claim = promoManagerStore.claims.find((entry) => entry.claim_id === claimId); + if (!claim || claim.status !== 'pending_approval') return ctx.reply('Pending claim not found.'); + claim.status = 'denied'; + claim.claim_reason = reason; + claim.updated_at = Date.now(); + await ctx.reply(`❌ Denied claim #${claimId}.`); + await bot.telegram.sendMessage(claim.user_id, `❌ Your promo claim #${claimId} was denied. Reason: ${reason}`).catch(() => {}); +})); + bot.action('menu_referral', async (ctx) => { const user = getUser(ctx); await ctx.answerCbQuery(); @@ -5279,6 +7637,26 @@ bot.action('menu_bugreport', async (ctx) => { await ctx.reply('🐛 *Bug Report*\n\nDescribe the bug you encountered. Include steps to reproduce if possible.\n\nOr type /cancel to cancel.', { parse_mode: 'Markdown' }); }); +/** + + * replyBonusStatus executes its scoped Runewager logic and participates in menu/command or utility flow composition. + + * Parameters: See the function signature for exact argument names and accepted values. + + * Returns: Returns the computed value or a Promise resolving to the operation result; may return void for side-effect handlers. + + * Side effects: May mutate runtime stores, pendingAction state, menu state, persistence files, logs, and callback progression. + + * Validation/safety: Uses existing guard utilities (admin checks, input checks, path checks, cooldown checks) where applicable. + + * Timeouts/fallbacks: Timeout and fallback behavior are controlled by the calling flow and global handler/state machine conventions. + + * Errors: Surfaces user-facing error replies and/or logs when inputs, permissions, or dependencies are invalid. + + * System fit: This function is part of the Runewager command/callback/state orchestration pipeline. + + */ + async function replyBonusStatus(ctx, user, answerCallback = false) { if (answerCallback) await ctx.answerCbQuery(); const wb = user.wagerBonus; @@ -5356,17 +7734,75 @@ bot.action('admin_cat_user', async (ctx) => { await replaceCallbackPanel(ctx, '👤 *User Tools*', { parse_mode: 'Markdown', ...adminUserToolsKeyboard() }); }); -bot.action('admin_cat_system', async (ctx) => { +bot.action('admin_cat_system', async (ctx) => { + if (!requireAdmin(ctx)) return; + await ctx.answerCbQuery(); + const user = getUser(ctx); + await replaceCallbackPanel(ctx, '⚙️ *System Tools*', { parse_mode: 'Markdown', ...adminSystemToolsKeyboard(user) }); +}); + +bot.action('admin_cat_support', async (ctx) => { + if (!requireAdmin(ctx)) return; + await ctx.answerCbQuery(); + await replaceCallbackPanel(ctx, '🛟 *Support Tools*', { parse_mode: 'Markdown', ...adminSupportToolsKeyboard() }); +}); + + +/** + + + * adminTestsToolsKeyboard executes its scoped Runewager logic and participates in menu/command or utility flow composition. + + + * Parameters: See the function signature for exact argument names and accepted values. + + + * Returns: Returns the computed value or a Promise resolving to the operation result; may return void for side-effect handlers. + + + * Side effects: May mutate runtime stores, pendingAction state, menu state, persistence files, logs, and callback progression. + + + * Validation/safety: Uses existing guard utilities (admin checks, input checks, path checks, cooldown checks) where applicable. + + + * Timeouts/fallbacks: Timeout and fallback behavior are controlled by the calling flow and global handler/state machine conventions. + + + * Errors: Surfaces user-facing error replies and/or logs when inputs, permissions, or dependencies are invalid. + + + * System fit: This function is part of the Runewager command/callback/state orchestration pipeline. + + + */ + + +function adminTestsToolsKeyboard() { + return Markup.inlineKeyboard([ + [Markup.button.callback('🧪 Run TestAll', 'admin_cmd_testall'), Markup.button.callback('🎁 Test Giveaway', 'admin_cmd_testgiveaway')], + [Markup.button.callback('🐛 View Bug Reports', 'admin_cmd_viewbugs'), Markup.button.callback('📤 Export Bugs', 'admin_cmd_exportbugs')], + [Markup.button.callback('✅ Resolve Bug', 'admin_cmd_resolvebug_prompt'), Markup.button.callback('🧪 Test Content Drop', 'admin_cmd_tiptest')], + [Markup.button.callback('📟 Open SSHV Console', 'sshv_open')], + [Markup.button.callback('⬅️ Admin Dashboard', 'open_admin_dashboard')], + [Markup.button.callback('🏠 Main Menu', 'to_main_menu'), Markup.button.callback('❌ Cancel', 'to_main_menu')], + ]); +} + +bot.action('admin_cat_tests', async (ctx) => { if (!requireAdmin(ctx)) return; await ctx.answerCbQuery(); - const user = getUser(ctx); - await replaceCallbackPanel(ctx, '⚙️ *System Tools*', { parse_mode: 'Markdown', ...adminSystemToolsKeyboard(user) }); -}); + await replaceCallbackPanel(ctx, + `🧪 *Tests & Bug Tools* -bot.action('admin_cat_support', async (ctx) => { - if (!requireAdmin(ctx)) return; - await ctx.answerCbQuery(); - await replaceCallbackPanel(ctx, '🛟 *Support Tools*', { parse_mode: 'Markdown', ...adminSupportToolsKeyboard() }); +Use this menu for diagnostics, bug workflows, and test sends. + +• TestAll = full sanity check +• Test Giveaway = simulated giveaway flow +• Test Content Drop = sends one random drop to current target +• SSHV = VPS console tools`, + { parse_mode: 'Markdown', ...adminTestsToolsKeyboard() }, + ); }); // Inline triggers for admin dashboard quick-action buttons @@ -5381,9 +7817,29 @@ bot.action('admin_cmd_giveaway_status', async (ctx) => { if (!requireAdmin(ctx)) return; await ctx.answerCbQuery(); const running = Array.from(giveawayStore.running.values()); - if (!running.length) { await ctx.reply('No giveaways currently running.'); return; } - const lines = running.map((g) => `#${g.id} — ${g.participants.size} entries — ends ${new Date(g.endTime).toUTCString()}`); - await ctx.reply(`📊 Running Giveaways (${running.length}):\n\n${lines.join('\n')}`); + if (!running.length) { + await replaceCallbackPanel(ctx, `🎁 *Active Giveaways*\n\nNo giveaways are currently running.`, { + parse_mode: 'Markdown', + ...Markup.inlineKeyboard([[Markup.button.callback('⬅️ Return to Admin Menu', 'pamenu_back_admin')]]), + }); + return; + } + const lines = running.map((g) => { + const remainSec = Math.max(0, Math.floor((g.endTime - Date.now()) / 1000)); + const remain = remainSec > 3600 + ? `${Math.floor(remainSec / 3600)}h ${Math.floor((remainSec % 3600) / 60)}m` + : `${Math.floor(remainSec / 60)}m ${remainSec % 60}s`; + return `#${g.id} — ${g.participants.size} entries — ⏱ ${remain} remaining`; + }); + const actionRows = running.slice(0, 5).map((g) => ([ + Markup.button.callback(`🎉 Join #${g.id}`, `gw_join_${g.id}`), + Markup.button.callback(`📋 Details #${g.id}`, `gw_details_${g.id}`), + ])); + actionRows.push([Markup.button.callback('⬅️ Return to Admin Menu', 'pamenu_back_admin')]); + await replaceCallbackPanel(ctx, `📊 *Running Giveaways* (${running.length})\n\n${lines.join('\\n')}`, { + parse_mode: 'Markdown', + ...Markup.inlineKeyboard(actionRows), + }); }); bot.action('admin_cmd_testgiveaway', async (ctx) => { @@ -5407,7 +7863,29 @@ bot.action('admin_cmd_health', async (ctx) => { bot.action('admin_cmd_version', async (ctx) => { if (!requireAdmin(ctx)) return; await ctx.answerCbQuery(); - await ctx.reply('Use /version to view bot version info.'); + const commitHash = process.env.COMMIT_HASH || 'local'; + const deployTime = process.env.DEPLOY_TIME || 'unknown'; + const uptime = Math.floor(process.uptime()); + const uptimeStr = `${Math.floor(uptime / 3600)}h ${Math.floor((uptime % 3600) / 60)}m ${uptime % 60}s`; + await replaceCallbackPanel(ctx, + `📦 *Bot Version Info* + +Version: \`${pkgVersion}\` +Commit: \`${commitHash}\` +Deployed: ${deployTime} +Node: \`${process.version}\` +Uptime: ${uptimeStr}`, + { parse_mode: 'Markdown', ...Markup.inlineKeyboard([ + [Markup.button.callback('⚙️ System Tools', 'admin_cat_system')], + [Markup.button.callback('⬅️ Admin Dashboard', 'open_admin_dashboard')], + ]) }, + ); +}); + +bot.action('admin_cmd_verify_setup', async (ctx) => { + if (!requireAdmin(ctx)) return; + await ctx.answerCbQuery(); + await ctx.reply('Use /verify_bot_setup to validate BotFather setup, permissions, and targets.'); }); bot.action('admin_backup_action', async (ctx) => { @@ -5525,8 +8003,10 @@ bot.action('sshv_ctrl_c', async (ctx) => { if (session.runningProcess && !session.runningProcess.killed) { session.runningProcess.kill('SIGINT'); session.buffer = 'SIGINT sent to running process.'; + adminLog('sshv_ctrl_c', { adminId: user.id, outcome: 'sent', cwd: session.cwd, command: session.lastCommand || '' }); await ctx.answerCbQuery('SIGINT sent'); } else { + adminLog('sshv_ctrl_c', { adminId: user.id, outcome: 'no_running_process', cwd: session.cwd, command: session.lastCommand || '' }); await ctx.answerCbQuery('No running process.'); } persistSshvSessions(); @@ -5541,8 +8021,10 @@ bot.action('sshv_ctrl_z', async (ctx) => { if (session.runningProcess && !session.runningProcess.killed) { try { session.runningProcess.kill('SIGTSTP'); } catch (_) { session.runningProcess.kill('SIGTERM'); } session.buffer = 'Stop signal sent to running process.'; + adminLog('sshv_ctrl_z', { adminId: user.id, outcome: 'sent', cwd: session.cwd, command: session.lastCommand || '' }); await ctx.answerCbQuery('Stop sent'); } else { + adminLog('sshv_ctrl_z', { adminId: user.id, outcome: 'no_running_process', cwd: session.cwd, command: session.lastCommand || '' }); await ctx.answerCbQuery('No running process.'); } persistSshvSessions(); @@ -5618,14 +8100,23 @@ bot.action('sshv_editor_save', async (ctx) => { await ctx.answerCbQuery('Send edited file content first.'); return; } - fs.writeFileSync(session.editorMode.filePath, draft, 'utf8'); - adminLog('sshv_editor_save', { adminId: user.id, filePath: session.editorMode.filePath, size: draft.length }); - session.buffer = `Saved ${session.editorMode.filePath}`; - session.editorMode = null; - user.pendingAction = { type: 'await_sshv_command' }; - persistSshvSessions(); - await ctx.answerCbQuery('Saved'); - await renderSshvConsole(ctx, session); + try { + fs.writeFileSync(session.editorMode.filePath, draft, 'utf8'); + adminLog('sshv_editor_save', { adminId: user.id, filePath: session.editorMode.filePath, size: draft.length }); + session.buffer = `Saved ${session.editorMode.filePath}`; + session.editorMode = null; + user.pendingAction = { type: 'await_sshv_command' }; + persistSshvSessions(); + await ctx.answerCbQuery('Saved'); + await renderSshvConsole(ctx, session); + } catch (e) { + adminLog('sshv_editor_save_error', { adminId: user.id, filePath: session.editorMode.filePath, error: e.message }); + session.buffer = `Failed to save ${session.editorMode.filePath}: ${e.message}`; + user.pendingAction = { type: 'await_sshv_editor_content' }; + persistSshvSessions(); + await ctx.answerCbQuery('Save failed'); + await renderSshvConsole(ctx, session, 'Save failed. You can edit and retry.'); + } }); bot.action('sshv_editor_cancel', async (ctx) => { @@ -5633,6 +8124,7 @@ bot.action('sshv_editor_cancel', async (ctx) => { const user = getUser(ctx); const session = getSshvSession(user.id, { createIfMissing: true }); session.editorMode = null; + adminLog('sshv_editor_cancel', { adminId: user.id }); user.pendingAction = { type: 'await_sshv_command' }; persistSshvSessions(); session.buffer = 'Editor cancelled.'; @@ -5800,6 +8292,26 @@ bot.action('menu_giveaways', async (ctx) => { await sendGiveawayListPage(ctx, 1); }); +/** + + * sendGiveawayListPage executes its scoped Runewager logic and participates in menu/command or utility flow composition. + + * Parameters: See the function signature for exact argument names and accepted values. + + * Returns: Returns the computed value or a Promise resolving to the operation result; may return void for side-effect handlers. + + * Side effects: May mutate runtime stores, pendingAction state, menu state, persistence files, logs, and callback progression. + + * Validation/safety: Uses existing guard utilities (admin checks, input checks, path checks, cooldown checks) where applicable. + + * Timeouts/fallbacks: Timeout and fallback behavior are controlled by the calling flow and global handler/state machine conventions. + + * Errors: Surfaces user-facing error replies and/or logs when inputs, permissions, or dependencies are invalid. + + * System fit: This function is part of the Runewager command/callback/state orchestration pipeline. + + */ + async function sendGiveawayListPage(ctx, page) { const features = giveawayFeatureList; const { slice, navRow, totalPages } = paginate(features, page, 6, 'page_giveaways'); @@ -5839,141 +8351,140 @@ bot.action(/^walk_(next|back|done)$/, async (ctx) => { // ========================= // Admin promo callbacks // ========================= -bot.action('admin_view', async (ctx) => { - if (!requireAdmin(ctx)) return; - await ctx.answerCbQuery(); - await ctx.reply(promoText()); -}); +/** + * promoManagerSummaryText executes its scoped Runewager logic and participates in menu/command or utility flow composition. + * Parameters: See the function signature for exact argument names and accepted values. + * Returns: Returns the computed value or a Promise resolving to the operation result; may return void for side-effect handlers. + * Side effects: May mutate runtime stores, pendingAction state, menu state, persistence files, logs, and callback progression. + * Validation/safety: Uses existing guard utilities (admin checks, input checks, path checks, cooldown checks) where applicable. + * Timeouts/fallbacks: Timeout and fallback behavior are controlled by the calling flow and global handler/state machine conventions. + * Errors: Surfaces user-facing error replies and/or logs when inputs, permissions, or dependencies are invalid. + * System fit: This function is part of the Runewager command/callback/state orchestration pipeline. + */ +function promoManagerSummaryText() { + const promos = promoManagerStore.promos.slice().sort((a, b) => a.promo_id - b.promo_id); + const lines = promos.map((promo) => { + const claims = countPromoClaims(promo.promo_id); + return `• #${promo.promo_id} ${promo.name} — ${promo.status} — claims: ${claims}`; + }); + return ['🎛 *Promo Manager*', '', 'All promo configuration now lives in the DB.', '', lines.length ? lines.join('\n') : '_No promos yet._'].join('\n'); +} -bot.action('admin_manage_promo_codes', async (ctx) => { +bot.action('admin_promo_manager', async (ctx) => { if (!requireAdmin(ctx)) return; await ctx.answerCbQuery(); - await replaceCallbackPanel(ctx, promoCodeManagerText(), { - parse_mode: 'Markdown', - ...Markup.inlineKeyboard([ - [Markup.button.callback('➕ Add Promo Code', 'admin_promo_code_add')], - [Markup.button.callback('🔁 Toggle Promo Code', 'admin_promo_code_toggle_prompt')], - [Markup.button.callback('⬅️ Promo Tools', 'admin_cat_promo')], - ]), - }); + await replaceCallbackPanel(ctx, promoManagerSummaryText(), { parse_mode: 'Markdown', ...adminPromoToolsKeyboard() }); }); -bot.action('admin_promo_code_add', async (ctx) => { + +bot.action('admin_pm_help', async (ctx) => { if (!requireAdmin(ctx)) return; - const user = getUser(ctx); - user.pendingAction = { type: 'admin_promo_code_add_input' }; await ctx.answerCbQuery(); - await ctx.reply(`Send new code as:\n||||\n\nExample:\nSPORTS5|existing_user|wager|5|Weekend reload`); + const guide = [ + 'ℹ️ *Promo Manager Step Guide*', + '', + 'Create Promo is a 9-step wizard. Example values:', + '1) Name: `New User Bonus`', + '2) Code/Link: `WELCOME35` or `https://casino.example/promo`', + '3) Casino base URL: `casino.example`', + '4) Description: `Claim 3.5 SC for first-time users`', + '5) Image URL (optional): `https://.../promo.png` or `skip`', + '6) Requirement type: `A` New user, `B` Existing user, `C` Existing + wager', + ' If C: enter wager SC amount, then rolling days (e.g. `3000`, `7`)', + '7) Claim limit: number or `none`', + '8) Cooldown hours: number or `none`', + '9) Approval mode: `auto` or `admin`', + '', + 'Tips:', + '• Use base domain only in step 3 (no path/query).', + '• Use clear names so users know who the promo is for.', + '• You can preview, pause, and edit anytime from this menu.', + ].join('\n'); + await replaceCallbackPanel(ctx, guide, { parse_mode: 'Markdown', ...adminPromoToolsKeyboard() }); }); -bot.action('admin_promo_code_toggle_prompt', async (ctx) => { +bot.action('admin_view', async (ctx) => { if (!requireAdmin(ctx)) return; - const user = getUser(ctx); - user.pendingAction = { type: 'admin_promo_code_toggle_input' }; await ctx.answerCbQuery(); - await ctx.reply('Send the promo code ID to toggle active/paused state (example: 2).'); + await ctx.reply(promoManagerSummaryText(), { parse_mode: 'Markdown', ...adminPromoToolsKeyboard() }); }); -bot.action('admin_edit_code', async (ctx) => { +bot.action('admin_pm_create', async (ctx) => { if (!requireAdmin(ctx)) return; const user = getUser(ctx); - user.pendingAction = { type: 'admin_edit_code_input' }; + user.pendingAction = { type: 'admin_pm_create_name', data: {} }; await ctx.answerCbQuery(); - await ctx.reply('Enter the new promo code:'); + await ctx.reply('Promo creation step 1/9: Enter promo name.'); }); -bot.action('admin_edit_amount', async (ctx) => { +bot.action('admin_pm_edit', async (ctx) => { if (!requireAdmin(ctx)) return; const user = getUser(ctx); - user.pendingAction = { type: 'admin_edit_amount_input' }; + user.pendingAction = { type: 'admin_pm_edit_select_id' }; await ctx.answerCbQuery(); - await ctx.reply('Enter the new SC amount:'); + await ctx.reply('Send promo ID to edit.'); }); -bot.action('admin_edit_limit', async (ctx) => { +bot.action('admin_pm_pause_toggle', async (ctx) => { if (!requireAdmin(ctx)) return; const user = getUser(ctx); - user.pendingAction = { type: 'admin_edit_limit_input' }; + user.pendingAction = { type: 'admin_pm_pause_toggle_id' }; await ctx.answerCbQuery(); - await ctx.reply('Enter the new total claim limit:'); + await ctx.reply('Send promo ID to pause/unpause.'); }); -bot.action('admin_pause', async (ctx) => { +bot.action('admin_pm_delete', async (ctx) => { if (!requireAdmin(ctx)) return; + const user = getUser(ctx); + user.pendingAction = { type: 'admin_pm_delete_id' }; await ctx.answerCbQuery(); - await replaceCallbackPanel(ctx, 'Pause the current promo?', Markup.inlineKeyboard([[Markup.button.callback('✅ Confirm Pause', 'admin_pause_yes')], [Markup.button.callback('❌ Cancel', 'admin_cancel')]])); + await ctx.reply('Send promo ID to mark deleted.'); }); -bot.action('admin_unpause', async (ctx) => { +bot.action('admin_pm_stats', async (ctx) => { if (!requireAdmin(ctx)) return; await ctx.answerCbQuery(); - await replaceCallbackPanel(ctx, 'Unpause the promo?', Markup.inlineKeyboard([[Markup.button.callback('✅ Confirm Unpause', 'admin_unpause_yes')], [Markup.button.callback('❌ Cancel', 'admin_cancel')]])); + const lines = promoManagerStore.promos.map((promo) => { + const claims = countPromoClaims(promo.promo_id); + const uniqueUsers = new Set(promoManagerStore.claims.filter((c) => c.promo_id === promo.promo_id && ['claimed', 'approved'].includes(c.status)).map((c) => c.user_id)).size; + const failures = promoManagerStore.eligibilityFailures.filter((f) => f.promo_id === promo.promo_id).length; + return `#${promo.promo_id} ${promo.name} +Claims: ${claims} | Unique users: ${uniqueUsers} | Eligibility failures: ${failures}`; + }); + await ctx.reply(['📊 Promo Stats', '', lines.length ? lines.join('\n\n') : 'No promos found.'].join('\n')); }); -bot.action('admin_remove', async (ctx) => { +bot.action('admin_pm_preview', async (ctx) => { if (!requireAdmin(ctx)) return; + const user = getUser(ctx); + user.pendingAction = { type: 'admin_pm_preview_id' }; await ctx.answerCbQuery(); - await replaceCallbackPanel(ctx, 'Are you sure you want to remove the promo?', Markup.inlineKeyboard([[Markup.button.callback('✅ Confirm Remove', 'admin_remove_yes')], [Markup.button.callback('❌ Cancel', 'admin_cancel')]])); + await ctx.reply('Send promo ID to preview user-facing card.'); }); -bot.action('admin_broadcast', async (ctx) => { +bot.action('admin_pm_queue', async (ctx) => { if (!requireAdmin(ctx)) return; await ctx.answerCbQuery(); - await replaceCallbackPanel(ctx, 'Send updated promo to all users?', Markup.inlineKeyboard([[Markup.button.callback('✅ Confirm Broadcast', 'admin_broadcast_yes')], [Markup.button.callback('❌ Cancel', 'admin_cancel')]])); -}); - -bot.action('admin_cancel', async (ctx) => { - if (!requireAdmin(ctx)) return; - await ctx.answerCbQuery('Cancelled'); - await replaceCallbackPanel(ctx, '✅ Cancelled. Select another promo action:', { parse_mode: 'Markdown', ...adminPromoToolsKeyboard() }); + const pending = promoManagerStore.claims.filter((claim) => claim.status === 'pending_approval'); + const lines = pending.map((claim) => `Claim #${claim.claim_id} | Promo #${claim.promo_id} | User ${claim.user_id}`); + await ctx.reply(['🧾 Pending Promo Queue', '', lines.length ? lines.join('\n') : 'No pending claims.', '', 'Use /pmapprove or /pmdeny .'].join('\n')); }); -bot.action('admin_pause_yes', async (ctx) => { - if (!requireAdmin(ctx)) return; - promoStore.active = false; - adminLog('pause_promo', { by: ctx.from.id }); - await ctx.answerCbQuery('Promo paused'); - await ctx.reply('Promo paused.'); -}); - -bot.action('admin_unpause_yes', async (ctx) => { - if (!requireAdmin(ctx)) return; - promoStore.active = true; - adminLog('unpause_promo', { by: ctx.from.id }); - await ctx.answerCbQuery('Promo active'); - await ctx.reply('Promo is now active.'); -}); - -bot.action('admin_remove_yes', async (ctx) => { - if (!requireAdmin(ctx)) return; - promoStore.active = false; - promoStore.code = ''; - promoStore.amountSC = 0; - promoStore.totalClaimLimit = 0; - promoStore.remainingClaims = 0; - adminLog('remove_promo', { by: ctx.from.id }); - await ctx.answerCbQuery('Removed'); - await ctx.reply('Promo removed.'); -}); +bot.action('admin_pause', async (ctx) => { if (!requireAdmin(ctx)) return; await ctx.answerCbQuery(); await ctx.reply('Use 🎛 Promo Manager → Pause/Unpause.'); }); +bot.action('admin_unpause', async (ctx) => { if (!requireAdmin(ctx)) return; await ctx.answerCbQuery(); await ctx.reply('Use 🎛 Promo Manager → Pause/Unpause.'); }); +bot.action('admin_remove', async (ctx) => { if (!requireAdmin(ctx)) return; await ctx.answerCbQuery(); await ctx.reply('Use 🎛 Promo Manager → Delete Promo.'); }); +bot.action('admin_manage_promo_codes', async (ctx) => { if (!requireAdmin(ctx)) return; await ctx.answerCbQuery(); await ctx.reply('Promo code manager is replaced by 🎛 Promo Manager.'); }); +bot.action('admin_promo_code_add', async (ctx) => { if (!requireAdmin(ctx)) return; await ctx.answerCbQuery(); await ctx.reply('Promo code manager is replaced by 🎛 Promo Manager.'); }); +bot.action('admin_promo_code_toggle_prompt', async (ctx) => { if (!requireAdmin(ctx)) return; await ctx.answerCbQuery(); await ctx.reply('Promo code manager is replaced by 🎛 Promo Manager.'); }); +bot.action('admin_edit_code', async (ctx) => { if (!requireAdmin(ctx)) return; await ctx.answerCbQuery(); await ctx.reply('Use 🎛 Promo Manager → Edit Promo.'); }); +bot.action('admin_edit_amount', async (ctx) => { if (!requireAdmin(ctx)) return; await ctx.answerCbQuery(); await ctx.reply('Use 🎛 Promo Manager → Edit Promo.'); }); +bot.action('admin_edit_limit', async (ctx) => { if (!requireAdmin(ctx)) return; await ctx.answerCbQuery(); await ctx.reply('Use 🎛 Promo Manager → Edit Promo.'); }); bot.action('admin_broadcast_yes', async (ctx) => { if (!requireAdmin(ctx)) return; - let sent = 0; - for (const [, u] of userStore) { - if (u.optOutBroadcasts) continue; - try { - // eslint-disable-next-line no-await-in-loop - await bot.telegram.sendMessage(u.id, `📢 Promo Update\n\n${promoText()}`); - sent += 1; - } catch (e) { - // ignore blocked users - } - } - adminLog('broadcast', { by: ctx.from.id, sent }); - await ctx.answerCbQuery('Broadcasted'); - await ctx.reply(`Promo update broadcasted to ${sent} users.`); + await ctx.answerCbQuery(); + await handleAnnounceCommand(ctx); }); - // ========================= // 30 SC Wager Bonus Admin callbacks // ========================= @@ -6373,6 +8884,80 @@ bot.on('inline_query', safeStepHandler('inline_query', async (ctx) => { await ctx.answerInlineQuery(filtered, { cache_time: 5, is_personal: true }); })); + +/** + + + * extractForwardedChat executes its scoped Runewager logic and participates in menu/command or utility flow composition. + + + * Parameters: See the function signature for exact argument names and accepted values. + + + * Returns: Returns the computed value or a Promise resolving to the operation result; may return void for side-effect handlers. + + + * Side effects: May mutate runtime stores, pendingAction state, menu state, persistence files, logs, and callback progression. + + + * Validation/safety: Uses existing guard utilities (admin checks, input checks, path checks, cooldown checks) where applicable. + + + * Timeouts/fallbacks: Timeout and fallback behavior are controlled by the calling flow and global handler/state machine conventions. + + + * Errors: Surfaces user-facing error replies and/or logs when inputs, permissions, or dependencies are invalid. + + + * System fit: This function is part of the Runewager command/callback/state orchestration pipeline. + + + */ + + +function extractForwardedChat(msg = {}) { + return msg.forward_from_chat + || (msg.forward_origin && msg.forward_origin.chat) + || (msg.sender_chat ? msg.sender_chat : null) + || null; +} + +/** + + * autoRegisterForwardedChatIfPresent executes its scoped Runewager logic and participates in menu/command or utility flow composition. + + * Parameters: See the function signature for exact argument names and accepted values. + + * Returns: Returns the computed value or a Promise resolving to the operation result; may return void for side-effect handlers. + + * Side effects: May mutate runtime stores, pendingAction state, menu state, persistence files, logs, and callback progression. + + * Validation/safety: Uses existing guard utilities (admin checks, input checks, path checks, cooldown checks) where applicable. + + * Timeouts/fallbacks: Timeout and fallback behavior are controlled by the calling flow and global handler/state machine conventions. + + * Errors: Surfaces user-facing error replies and/or logs when inputs, permissions, or dependencies are invalid. + + * System fit: This function is part of the Runewager command/callback/state orchestration pipeline. + + */ + +async function autoRegisterForwardedChatIfPresent(ctx, user) { + if (!isAdmin(ctx)) return false; + const fwdChat = extractForwardedChat(ctx.message || {}); + if (!fwdChat || !fwdChat.id) return false; + const chatId = Number(fwdChat.id); + approvedGroupsStore.add(chatId); + tipsStore.targetGroup = String(chatId); + broadcastConfigStore.targetGroup = String(chatId); + persistRuntimeState(); + await ctx.reply(`✅ Auto-registered target chat from forwarded message: +• ${fwdChat.title || chatId} (${chatId}) + +Content Drops + group broadcasts now use this target.`); + return true; +} + // ========================= // Text message handler for pending actions // ========================= @@ -6381,6 +8966,32 @@ bot.on('text', async (ctx) => { const text = (ctx.message.text || '').trim(); + if (user.pendingAction && !user.pendingAction.createdAt) user.pendingAction.createdAt = Date.now(); + + if (user.pendingAction && (Date.now() - Number(user.pendingAction.createdAt || 0)) > PENDING_ACTION_TIMEOUT_MS) { + const expiredType = user.pendingAction.type || 'current action'; + user.pendingAction = null; + await ctx.reply( + `⏱️ Your pending step (${expiredType}) timed out after 15 minutes. Please start again from the menu.`, + Markup.inlineKeyboard([[Markup.button.callback('🏠 Main Menu', 'to_main_menu')], [Markup.button.callback('❌ Cancel', 'to_main_menu')]]), + ); + return; + } + + if (user.pendingAction && text.startsWith('/')) { + const cmd = text.slice(1).split(/\s+/)[0].split('@')[0].toLowerCase(); + if (cmd === 'cancel') { + user.pendingAction = null; + await ctx.reply('✅ Pending action cancelled.', Markup.inlineKeyboard([[Markup.button.callback('🏠 Main Menu', 'to_main_menu')]])); + return; + } + if (cmd === 'menu' || cmd === 'start' || cmd === 'help') { + user.pendingAction = null; + await sendMainMenu(ctx, user, 1); + return; + } + } + if (!user.pendingAction && text.startsWith('/')) { const cmd = text.slice(1).split(/\s+/)[0].split('@')[0]; if (!REGISTERED_COMMANDS.has(cmd)) { @@ -6413,6 +9024,8 @@ bot.on('text', async (ctx) => { return; } + if (!user.pendingAction && await autoRegisterForwardedChatIfPresent(ctx, user)) return; + // Smart username detection — fires when there is NO pending action, the message looks // like a Runewager username (3-30 chars, alphanumeric/underscore/hyphen/dot, no spaces), // and the user has not yet linked an account. Always requires confirmation — never auto-accepts. @@ -6456,9 +9069,9 @@ bot.on('text', async (ctx) => { session.lastActivity = Date.now(); persistSshvSessions(); await ctx.reply( - `📝 Draft captured for \`${escapeMarkdownFull(session.editorMode.filePath)}\`. Save changes?`, + `📝 Draft captured for \`${escapeMarkdownV2(session.editorMode.filePath)}\`. Save changes?`, { - parse_mode: 'MarkdownV2', + parse_mode: 'Markdown', ...Markup.inlineKeyboard([ [Markup.button.callback('💾 Save', 'sshv_editor_save'), Markup.button.callback('❌ Cancel', 'sshv_editor_cancel')], ]), @@ -6467,6 +9080,26 @@ bot.on('text', async (ctx) => { return; } + if (action.type === 'await_register_chat_forward') { + if (!requireAdmin(ctx)) return; + const fwdChat = extractForwardedChat(ctx.message || {}); + if (!fwdChat || !fwdChat.id) { + await ctx.reply('Please forward a message from the target channel/group.'); + return; + } + const chatId = Number(fwdChat.id); + approvedGroupsStore.add(chatId); + tipsStore.targetGroup = String(chatId); + broadcastConfigStore.targetGroup = String(chatId); + user.pendingAction = null; + persistRuntimeState(); + await ctx.reply(`✅ Registered chat for broadcasts/content drops: +• ${fwdChat.title || chatId} (${chatId}) + +Content Drops and group broadcasts will now use this target.`); + return; + } + // Admin prompt: whois lookup if (action.type === 'await_admin_whois') { if (!requireAdmin(ctx)) return; @@ -6602,6 +9235,156 @@ bot.on('text', async (ctx) => { return; } + if (action.type === 'admin_pm_create_name') { + if (!requireAdmin(ctx)) return; + action.data.name = text; + user.pendingAction = { type: 'admin_pm_create_code', data: action.data }; + await ctx.reply('Step 2/9: Enter promo code or promo link.'); + return; + } + if (action.type === 'admin_pm_create_code') { + if (!requireAdmin(ctx)) return; + action.data.code_or_link = text; + user.pendingAction = { type: 'admin_pm_create_domain', data: action.data }; + await ctx.reply('Step 3/9: Enter casino/affiliate base domain (example: runewager.com).'); + return; + } + if (action.type === 'admin_pm_create_domain') { + if (!requireAdmin(ctx)) return; + action.data.casino_base_url = text; + user.pendingAction = { type: 'admin_pm_create_description', data: action.data }; + await ctx.reply('Step 4/9: Enter promo description.'); + return; + } + if (action.type === 'admin_pm_create_description') { + if (!requireAdmin(ctx)) return; + action.data.description = text; + user.pendingAction = { type: 'admin_pm_create_image', data: action.data }; + await ctx.reply('Step 5/9: Enter promo image URL or type "skip".'); + return; + } + if (action.type === 'admin_pm_create_image') { + if (!requireAdmin(ctx)) return; + action.data.image_url = text.toLowerCase() === 'skip' ? '' : text; + user.pendingAction = { type: 'admin_pm_create_requirement', data: action.data }; + await ctx.reply(`Step 6/9: Requirement type? Reply with: +A = New User Only +B = Existing User +C = Existing User With Wager Requirement`); + return; + } + if (action.type === 'admin_pm_create_requirement') { + if (!requireAdmin(ctx)) return; + const choice = text.trim().toUpperCase(); + if (!['A', 'B', 'C'].includes(choice)) { await ctx.reply('Reply with A, B, or C.'); return; } + action.data.requirement_type = choice === 'A' ? PROMO_REQUIREMENT.NEW_USER_ONLY : (choice === 'B' ? PROMO_REQUIREMENT.EXISTING_USER : PROMO_REQUIREMENT.EXISTING_USER_WITH_WAGER); + if (choice === 'C') { + user.pendingAction = { type: 'admin_pm_create_required_wager_amount', data: action.data }; + await ctx.reply('Enter required_wager_amount (SC).'); + return; + } + action.data.required_wager_amount = 0; + action.data.required_wager_days = 0; + user.pendingAction = { type: 'admin_pm_create_claim_limit', data: action.data }; + await ctx.reply('Step 7/9: Claim limit (number) or "skip".'); + return; + } + if (action.type === 'admin_pm_create_required_wager_amount') { + if (!requireAdmin(ctx)) return; + action.data.required_wager_amount = Number(text) || 0; + user.pendingAction = { type: 'admin_pm_create_required_wager_days', data: action.data }; + await ctx.reply('Enter required_wager_days (rolling window).'); + return; + } + if (action.type === 'admin_pm_create_required_wager_days') { + if (!requireAdmin(ctx)) return; + action.data.required_wager_days = Number(text) || 0; + user.pendingAction = { type: 'admin_pm_create_claim_limit', data: action.data }; + await ctx.reply('Step 7/9: Claim limit (number) or "skip".'); + return; + } + if (action.type === 'admin_pm_create_claim_limit') { + if (!requireAdmin(ctx)) return; + action.data.claim_limit = text.toLowerCase() === 'skip' ? null : (Number(text) || null); + user.pendingAction = { type: 'admin_pm_create_cooldown', data: action.data }; + await ctx.reply('Step 8/9: Cooldown in hours (number) or "skip".'); + return; + } + if (action.type === 'admin_pm_create_cooldown') { + if (!requireAdmin(ctx)) return; + action.data.cooldown_hours = text.toLowerCase() === 'skip' ? 0 : (Number(text) || 0); + user.pendingAction = { type: 'admin_pm_create_auto_approve', data: action.data }; + await ctx.reply('Step 9/9: Auto-approve? Reply yes or no.'); + return; + } + if (action.type === 'admin_pm_create_auto_approve') { + if (!requireAdmin(ctx)) return; + const promo = normalizePromoManagerPromo({ + promo_id: promoManagerStore.nextPromoId++, + ...action.data, + auto_approve: /^y/i.test(text.trim()), + created_by: ctx.from.id, + created_at: Date.now(), + updated_at: Date.now(), + status: PROMO_STATUS.ACTIVE, + }); + promoManagerStore.promos.push(promo); + user.pendingAction = null; + await ctx.reply(`✅ Promo created (#${promo.promo_id}): ${promo.name}`); + return; + } + if (action.type === 'admin_pm_edit_select_id') { + if (!requireAdmin(ctx)) return; + const promo = getPromoById(Number(text)); + if (!promo) { await ctx.reply('Promo ID not found.'); return; } + user.pendingAction = { type: 'admin_pm_edit_field', data: { promoId: promo.promo_id } }; + await ctx.reply('Send field=value to edit (name,code_or_link,casino_base_url,description,image_url,claim_limit,cooldown_hours,auto_approve,status).'); + return; + } + if (action.type === 'admin_pm_edit_field') { + if (!requireAdmin(ctx)) return; + const [field, ...rest] = text.split('='); + const promo = getPromoById(action.data.promoId); + if (!promo) { user.pendingAction = null; await ctx.reply('Promo not found.'); return; } + const valueRaw = rest.join('=').trim(); + if (!field || !valueRaw) { await ctx.reply('Use field=value format.'); return; } + const key = field.trim(); + if (!(key in promo)) { await ctx.reply('Invalid field.'); return; } + promo[key] = ['claim_limit', 'cooldown_hours', 'required_wager_amount', 'required_wager_days'].includes(key) ? Number(valueRaw) : (/^(true|false)$/i.test(valueRaw) ? /^true$/i.test(valueRaw) : valueRaw); + promo.updated_at = Date.now(); + user.pendingAction = null; + await ctx.reply(`✅ Promo #${promo.promo_id} updated (${key}).`); + return; + } + if (action.type === 'admin_pm_pause_toggle_id') { + if (!requireAdmin(ctx)) return; + const promo = getPromoById(Number(text)); + if (!promo || promo.status === PROMO_STATUS.DELETED) { await ctx.reply('Promo ID not found.'); return; } + promo.status = promo.status === PROMO_STATUS.ACTIVE ? PROMO_STATUS.PAUSED : PROMO_STATUS.ACTIVE; + promo.updated_at = Date.now(); + user.pendingAction = null; + await ctx.reply(`✅ Promo #${promo.promo_id} is now ${promo.status}.`); + return; + } + if (action.type === 'admin_pm_delete_id') { + if (!requireAdmin(ctx)) return; + const promo = getPromoById(Number(text)); + if (!promo) { await ctx.reply('Promo ID not found.'); return; } + promo.status = PROMO_STATUS.DELETED; + promo.updated_at = Date.now(); + user.pendingAction = null; + await ctx.reply(`🗑 Promo #${promo.promo_id} marked deleted.`); + return; + } + if (action.type === 'admin_pm_preview_id') { + if (!requireAdmin(ctx)) return; + const promo = getPromoById(Number(text)); + if (!promo) { await ctx.reply('Promo ID not found.'); return; } + user.pendingAction = null; + await ctx.reply(renderPromoForUser(promo), { parse_mode: 'Markdown' }); + return; + } + // Admin promo edits if (action.type === 'admin_promo_code_add_input') { if (!requireAdmin(ctx)) return; @@ -6954,7 +9737,7 @@ bot.on('text', async (ctx) => { if (action.type === 'await_tip_add_text') { if (!requireAdmin(ctx)) return; if (!text) { - await ctx.reply('Tip text cannot be empty. Please send the tip you want to add.'); + await ctx.reply('Drop text cannot be empty. Please send the tip you want to add.'); return; } const newTip = { id: tipsStore.nextTipId, text, enabled: true }; @@ -6963,7 +9746,7 @@ bot.on('text', async (ctx) => { user.pendingAction = null; persistRuntimeState(); saveHelpfulMessages(); - await ctx.reply(`Added as Tip #${newTip.id}.`); + await ctx.reply(`Added as Drop #${newTip.id}.`); return; } @@ -6971,7 +9754,7 @@ bot.on('text', async (ctx) => { if (action.type === 'await_tip_edit_text') { if (!requireAdmin(ctx)) return; if (!text) { - await ctx.reply('Tip text cannot be empty. Please send the updated tip text.'); + await ctx.reply('Drop text cannot be empty. Please send the updated tip text.'); return; } const tipId = action.data && action.data.tipId; @@ -6985,7 +9768,7 @@ bot.on('text', async (ctx) => { user.pendingAction = null; persistRuntimeState(); saveHelpfulMessages(); - await ctx.reply(`Tip #${tipId} updated.`); + await ctx.reply(`Drop #${tipId} updated.`); return; } @@ -7113,84 +9896,181 @@ bot.action('admin_edit_limit_yes', async (ctx) => { await ctx.reply('Claim limit updated successfully.'); }); -bot.action('gw_create_no', async (ctx) => { +bot.action('gw_create_no', async (ctx) => { + const user = getUser(ctx); + user.pendingAction = null; + persistSshvSessions(); + await ctx.answerCbQuery('Cancelled'); + await ctx.reply('Giveaway setup cancelled.'); +}); + +// ========================= +// Announce command callback actions +// ========================= + +bot.action('announce_edit', async (ctx) => { + if (!requireAdmin(ctx)) return; + const user = getUser(ctx); + await ctx.answerCbQuery(); + user.pendingAction = { type: 'await_announcement_text' }; + await ctx.reply('Sure — send the updated announcement text.'); +}); + +bot.action('announce_toggle_dm', async (ctx) => { + if (!requireAdmin(ctx)) return; + const user = getUser(ctx); + const action = user.pendingAction; + await ctx.answerCbQuery(); + if (!action || action.type !== 'await_announcement_action' || !action.data) return; + action.data.sendDm = !action.data.sendDm; + await replaceCallbackPanel(ctx, '📣 Broadcast targets updated.', announceBuilderKeyboard(action.data)); +}); + +bot.action('announce_toggle_channel', async (ctx) => { + if (!requireAdmin(ctx)) return; const user = getUser(ctx); - user.pendingAction = null; - persistSshvSessions(); - await ctx.answerCbQuery('Cancelled'); - await ctx.reply('Giveaway setup cancelled.'); + const action = user.pendingAction; + await ctx.answerCbQuery(); + if (!action || action.type !== 'await_announcement_action' || !action.data) return; + action.data.sendChannel = !action.data.sendChannel; + await replaceCallbackPanel(ctx, '📣 Broadcast targets updated.', announceBuilderKeyboard(action.data)); }); -// ========================= -// Announce command callback actions -// ========================= +bot.action('announce_toggle_group', async (ctx) => { + if (!requireAdmin(ctx)) return; + const user = getUser(ctx); + const action = user.pendingAction; + await ctx.answerCbQuery(); + if (!action || action.type !== 'await_announcement_action' || !action.data) return; + action.data.sendGroup = !action.data.sendGroup; + await replaceCallbackPanel(ctx, '📣 Broadcast targets updated.', announceBuilderKeyboard(action.data)); +}); -bot.action('announce_edit', async (ctx) => { +bot.action('announce_toggle_mode', async (ctx) => { if (!requireAdmin(ctx)) return; const user = getUser(ctx); + const action = user.pendingAction; await ctx.answerCbQuery(); - user.pendingAction = { type: 'await_announcement_text' }; - await ctx.reply('Sure — send the updated announcement text.'); + if (!action || action.type !== 'await_announcement_action' || !action.data) return; + action.data.parseMode = action.data.parseMode === 'HTML' ? 'TEXT' : 'HTML'; + await replaceCallbackPanel(ctx, `📣 Mode switched to ${parseModeLabel(action.data.parseMode)}.`, announceBuilderKeyboard(action.data)); }); -bot.action('announce_send_all', async (ctx) => { +bot.action('announce_send_now', async (ctx) => { if (!requireAdmin(ctx)) return; const user = getUser(ctx); - await ctx.answerCbQuery('Sending...'); const action = user.pendingAction; + await ctx.answerCbQuery('Sending...'); if (!action || action.type !== 'await_announcement_action' || !action.data) { await ctx.reply('No announcement pending. Use /announce to start a new one.'); return; } - const announcementText = action.data.announcementText; + const result = await sendAnnouncementTargets(ctx, action.data.announcementText, action.data); + adminLog('announce_send_now', { by: ctx.from.id, ...result, mode: action.data.parseMode, targets: action.data }); user.pendingAction = null; + await ctx.reply( + `✅ Broadcast complete.\n• DM users sent: ${result.sentDm}\n• DM failed: ${result.failedDm}\n• Channel: ${result.channelStatus}\n• Group: ${result.groupStatus}`, + ); +}); - // Broadcast to all non-opted-out users - let sent = 0; - for (const [, u] of userStore) { - if (u.optOutBroadcasts || u.muted) continue; - try { - await ctx.telegram.sendMessage(u.id, announcementText); - sent += 1; - } catch (_) { /* ignore blocked/deactivated users */ } - } - - // Send to the GambleCodezDrops channel - try { - await ctx.telegram.sendMessage(ANNOUNCE_CHANNEL, announcementText); - } catch (e) { - await ctx.reply(`⚠️ Channel send failed: ${e.message}`); +// Legacy aliases +bot.action('announce_send_all', async (ctx) => { + if (!requireAdmin(ctx)) return; + const user = getUser(ctx); + const action = user.pendingAction; + if (action && action.type === 'await_announcement_action' && action.data) { + action.data.sendDm = true; + action.data.sendChannel = true; + action.data.sendGroup = false; } - - adminLog('announce_all', { by: ctx.from.id, sent }); - await ctx.reply(`📣 Announcement sent to ${sent} users and the GambleCodezDrops channel.`); + await ctx.answerCbQuery('Using new broadcast flow'); + await ctx.reply('Legacy "Send All" mapped to the new broadcast flow. Tap "Send Broadcast Now" after reviewing toggles.'); }); bot.action('announce_send_channel', async (ctx) => { if (!requireAdmin(ctx)) return; const user = getUser(ctx); - await ctx.answerCbQuery('Sending to channel...'); const action = user.pendingAction; - if (!action || action.type !== 'await_announcement_action' || !action.data) { - await ctx.reply('No announcement pending. Use /announce to start a new one.'); - return; - } - const announcementText = action.data.announcementText; - user.pendingAction = null; - - try { - await ctx.telegram.sendMessage(ANNOUNCE_CHANNEL, announcementText); - adminLog('announce_channel', { by: ctx.from.id }); - await ctx.reply('📣 Announcement sent to the GambleCodezDrops channel.'); - } catch (e) { - await ctx.reply(`❌ Failed to send to channel: ${e.message}`); + if (action && action.type === 'await_announcement_action' && action.data) { + action.data.sendDm = false; + action.data.sendChannel = true; + action.data.sendGroup = false; } + await ctx.answerCbQuery('Using new broadcast flow'); + await ctx.reply('Legacy "Send Channel Only" mapped to new broadcast flow. Tap "Send Broadcast Now" to continue.'); }); // ========================= -// Tips System — scheduler + admin commands + callbacks +// Content Drops System — scheduler + admin commands + callbacks // ========================= +/** + + * resolveTipTargetChatId executes its scoped Runewager logic and participates in menu/command or utility flow composition. + + * Parameters: See the function signature for exact argument names and accepted values. + + * Returns: Returns the computed value or a Promise resolving to the operation result; may return void for side-effect handlers. + + * Side effects: May mutate runtime stores, pendingAction state, menu state, persistence files, logs, and callback progression. + + * Validation/safety: Uses existing guard utilities (admin checks, input checks, path checks, cooldown checks) where applicable. + + * Timeouts/fallbacks: Timeout and fallback behavior are controlled by the calling flow and global handler/state machine conventions. + + * Errors: Surfaces user-facing error replies and/or logs when inputs, permissions, or dependencies are invalid. + + * System fit: This function is part of the Runewager command/callback/state orchestration pipeline. + + */ + +async function resolveTipTargetChatId(rawTarget) { + const t = String(rawTarget || '').trim(); + if (!t) return null; + if (/^-?\d+$/.test(t)) return Number(t); + if (t.startsWith('@')) return t; + return `@${t}`; +} + +/** + + * postTipToConfiguredTarget executes its scoped Runewager logic and participates in menu/command or utility flow composition. + + * Parameters: See the function signature for exact argument names and accepted values. + + * Returns: Returns the computed value or a Promise resolving to the operation result; may return void for side-effect handlers. + + * Side effects: May mutate runtime stores, pendingAction state, menu state, persistence files, logs, and callback progression. + + * Validation/safety: Uses existing guard utilities (admin checks, input checks, path checks, cooldown checks) where applicable. + + * Timeouts/fallbacks: Timeout and fallback behavior are controlled by the calling flow and global handler/state machine conventions. + + * Errors: Surfaces user-facing error replies and/or logs when inputs, permissions, or dependencies are invalid. + + * System fit: This function is part of the Runewager command/callback/state orchestration pipeline. + + */ + +async function postTipToConfiguredTarget(tip, telegram) { + const primaryTarget = await resolveTipTargetChatId(tipsStore.targetGroup || broadcastConfigStore.targetGroup); + const fallbackTarget = approvedGroupsStore.size ? Array.from(approvedGroupsStore)[0] : null; + const candidates = [primaryTarget, fallbackTarget].filter(Boolean); + let lastErr = null; + for (const target of candidates) { + try { + // eslint-disable-next-line no-await-in-loop + await telegram.sendMessage(target, formatTipForHtml(tip.text), { parse_mode: 'HTML', disable_notification: true }); + tipsStore.targetGroup = String(target); + broadcastConfigStore.targetGroup = String(target); + return { ok: true, target }; + } catch (e) { + lastErr = e; + } + } + return { ok: false, error: lastErr }; +} + /** * Start (or restart) the tips scheduler. * Clears any existing timer then arms a fresh one using the current interval. @@ -7207,27 +10087,20 @@ function startTipsScheduler() { : enabled; const tip = pool[Math.floor(Math.random() * pool.length)]; try { - const me = await bot.telegram.getMe(); - const member = await bot.telegram.getChatMember(tipsStore.targetGroup, me.id); - const canSend = member - && (member.status === 'administrator' || member.status === 'creator' || member.can_send_messages); - if (!canSend) { - logEvent('warn', 'Skipping helpful message: missing permission in target group', { target: tipsStore.targetGroup }); + const sendResult = await postTipToConfiguredTarget(tip, bot.telegram); + if (!sendResult.ok) { + logEvent('warn', 'Failed to post group tip', { tipId: tip.id, error: sendResult.error ? sendResult.error.message : 'unknown' }); return; } - await bot.telegram.sendMessage(tipsStore.targetGroup, tip.text, { - parse_mode: 'Markdown', - disable_notification: true, - }); tipsStore.lastSentTipId = tip.id; - logEvent('info', 'Group tip posted', { tipId: tip.id }); + logEvent('info', 'Group tip posted', { tipId: tip.id, target: sendResult.target }); } catch (e) { logEvent('warn', 'Failed to post group tip', { tipId: tip.id, error: e.message }); } }, ms); } -/** Build the Tips Manager dashboard keyboard */ +/** Build the Content Drops Manager dashboard keyboard */ function tipsDashboardKeyboard() { return Markup.inlineKeyboard([ [Markup.button.callback('➕ Add Tip', 'tips_cmd_add'), Markup.button.callback('✏️ Edit Tip', 'tips_cmd_edit')], @@ -7252,17 +10125,37 @@ function tipSelectKeyboard(action) { return Markup.inlineKeyboard(rows); } -/** Send (or re-send) the Tips Manager dashboard */ +/** Send (or re-send) the Content Drops Manager dashboard */ async function sendTipsDashboard(ctx) { const total = tipsStore.tips.length; const enabled = tipsStore.tips.filter((t) => t.enabled).length; const status = tipsStore.systemEnabled ? '🟢 Enabled' : '🔴 Disabled'; - const text = `📝 *Tips Manager*\n\nStatus: ${status}\nTotal Tips: ${total} (${enabled} active)\nInterval: every ${tipsStore.intervalHours}h\nTarget: ${tipsStore.targetGroup}`; + const text = `📝 *Content Drops Manager*\n\nStatus: ${status}\nTotal Drops: ${total} (${enabled} active)\nInterval: every ${tipsStore.intervalHours}h\nTarget: ${tipsStore.targetGroup}`; await replaceCallbackPanel(ctx, text, { parse_mode: 'Markdown', ...tipsDashboardKeyboard() }); } // ── /tips /tiplist /tipadd /tipremove /tipedit /tiptoggle /tiptest /tipsettings ── +/** + + * handleTipsCommand executes its scoped Runewager logic and participates in menu/command or utility flow composition. + + * Parameters: See the function signature for exact argument names and accepted values. + + * Returns: Returns the computed value or a Promise resolving to the operation result; may return void for side-effect handlers. + + * Side effects: May mutate runtime stores, pendingAction state, menu state, persistence files, logs, and callback progression. + + * Validation/safety: Uses existing guard utilities (admin checks, input checks, path checks, cooldown checks) where applicable. + + * Timeouts/fallbacks: Timeout and fallback behavior are controlled by the calling flow and global handler/state machine conventions. + + * Errors: Surfaces user-facing error replies and/or logs when inputs, permissions, or dependencies are invalid. + + * System fit: This function is part of the Runewager command/callback/state orchestration pipeline. + + */ + async function handleTipsCommand(ctx) { if (!requireAdmin(ctx)) return; await sendTipsDashboard(ctx); @@ -7274,7 +10167,7 @@ bot.command('tp', handleTipsCommand); bot.command('tiplist', async (ctx) => { if (!requireAdmin(ctx)) return; - const lines = ['📋 *Current Tips:*\n']; + const lines = ['📋 *Current Content Drops:*\n']; for (const [idx, tip] of tipsStore.tips.entries()) { const preview = tip.text.replace(/\*/g, '').slice(0, 80); lines.push(`${idx + 1}. #${tip.id} ${tip.enabled ? '✅' : '🔇'} ${preview}${tip.text.length > 80 ? '…' : ''}`); @@ -7351,7 +10244,7 @@ bot.command('tiptoggle', async (ctx) => { tipsStore.systemEnabled = !tipsStore.systemEnabled; persistRuntimeState(); saveHelpfulMessages(); - const status = tipsStore.systemEnabled ? '🟢 Tips System Enabled' : '🔴 Tips System Disabled'; + const status = tipsStore.systemEnabled ? '🟢 Content Drops System Enabled' : '🔴 Content Drops System Disabled'; await ctx.reply(status); }); @@ -7363,23 +10256,21 @@ bot.command('tiptest', async (ctx) => { ? enabled.filter((t) => t.id !== tipsStore.lastSentTipId) : enabled; const tip = pool[Math.floor(Math.random() * pool.length)]; - try { - await bot.telegram.sendMessage(tipsStore.targetGroup, formatTipForHtml(tip.text), { - parse_mode: 'HTML', - disable_notification: true, - }); + const sendResult = await postTipToConfiguredTarget(tip, bot.telegram); + if (sendResult.ok) { tipsStore.lastSentTipId = tip.id; persistRuntimeState(); saveHelpfulMessages(); - await ctx.reply(`✅ Test tip posted to ${tipsStore.targetGroup} (silent).`); - } catch (e) { - await ctx.reply(`❌ Failed to post test tip to ${tipsStore.targetGroup}: ${e.message}`); + await ctx.reply(`✅ Test tip posted to ${sendResult.target} (silent).`); + } else { + await ctx.reply(`❌ Failed to post test tip. ${sendResult.error ? sendResult.error.message : 'Unknown error'} +Tip: run /register_chat and forward a message from the target group/channel.`); } }); bot.command('tipsettings', async (ctx) => { if (!requireAdmin(ctx)) return; - const text = `⚙️ *Tips Settings*\n\nInterval: every ${tipsStore.intervalHours} hours\nSilent mode: Always ON\nTarget group: ${tipsStore.targetGroup}\n\nTo change the interval, reply with the number of hours (e.g. \`4\`).`; + const text = `⚙️ *Content Drops Settings*\n\nInterval: every ${tipsStore.intervalHours} hours\nSilent mode: Always ON\nTarget group: ${tipsStore.targetGroup}\n\nTo change the interval, reply with the number of hours (e.g. \`4\`).`; const user = getUser(ctx); clearPendingAction(user); user.pendingAction = { type: 'await_tip_settings_interval' }; @@ -7417,7 +10308,7 @@ bot.action('tips_cmd_toggle', async (ctx) => { tipsStore.systemEnabled = !tipsStore.systemEnabled; persistRuntimeState(); saveHelpfulMessages(); - const status = tipsStore.systemEnabled ? '🟢 Tips System Enabled' : '🔴 Tips System Disabled'; + const status = tipsStore.systemEnabled ? '🟢 Content Drops System Enabled' : '🔴 Content Drops System Disabled'; await ctx.reply(status); await sendTipsDashboard(ctx); }); @@ -7425,7 +10316,7 @@ bot.action('tips_cmd_toggle', async (ctx) => { bot.action('tips_cmd_list', async (ctx) => { if (!requireAdmin(ctx)) return; await ctx.answerCbQuery(); - const lines = ['📋 *Current Tips:*\n']; + const lines = ['📋 *Current Content Drops:*\n']; for (const [idx, tip] of tipsStore.tips.entries()) { const preview = tip.text.replace(/\*/g, '').slice(0, 80); lines.push(`${idx + 1}. #${tip.id} ${tip.enabled ? '✅' : '🔇'} ${preview}${tip.text.length > 80 ? '…' : ''}`); @@ -7442,17 +10333,15 @@ bot.action('tips_cmd_test', async (ctx) => { ? enabled.filter((t) => t.id !== tipsStore.lastSentTipId) : enabled; const tip = pool[Math.floor(Math.random() * pool.length)]; - try { - await bot.telegram.sendMessage(tipsStore.targetGroup, formatTipForHtml(tip.text), { - parse_mode: 'HTML', - disable_notification: true, - }); + const sendResult = await postTipToConfiguredTarget(tip, bot.telegram); + if (sendResult.ok) { tipsStore.lastSentTipId = tip.id; persistRuntimeState(); saveHelpfulMessages(); - await ctx.reply(`✅ Test tip posted to ${tipsStore.targetGroup} (silent).`); - } catch (e) { - await ctx.reply(`❌ Failed to post test tip to ${tipsStore.targetGroup}: ${e.message}`); + await ctx.reply(`✅ Test tip posted to ${sendResult.target} (silent).`); + } else { + await ctx.reply(`❌ Failed to post test tip. ${sendResult.error ? sendResult.error.message : 'Unknown error'} +Tip: run /register_chat and forward a message from the target group/channel.`); } await sendTipsDashboard(ctx); }); @@ -7472,7 +10361,7 @@ bot.action('tips_cmd_settings', async (ctx) => { const user = getUser(ctx); clearPendingAction(user); user.pendingAction = { type: 'await_tip_settings_interval' }; - const text = `⚙️ *Tips Settings*\n\nInterval: every ${tipsStore.intervalHours} hours\nSilent mode: Always ON\nTarget group: ${tipsStore.targetGroup}\n\nSend the new interval in hours (e.g. \`4\`).`; + const text = `⚙️ *Content Drops Settings*\n\nInterval: every ${tipsStore.intervalHours} hours\nSilent mode: Always ON\nTarget group: ${tipsStore.targetGroup}\n\nSend the new interval in hours (e.g. \`4\`).`; await ctx.reply(text, { parse_mode: 'Markdown' }); }); @@ -7494,7 +10383,7 @@ bot.action(/^tip_remove_(\d+)$/, async (ctx) => { tipsStore.tips.splice(idx, 1); persistRuntimeState(); saveHelpfulMessages(); - await ctx.reply(`Tip #${tipId} removed.`); + await ctx.reply(`Drop #${tipId} removed.`); }); // tip_edit_select_ @@ -7507,7 +10396,7 @@ bot.action(/^tip_edit_select_(\d+)$/, async (ctx) => { const user = getUser(ctx); clearPendingAction(user); user.pendingAction = { type: 'await_tip_edit_text', data: { tipId } }; - await ctx.reply(`Send the updated text for Tip #${tipId}.\n\nCurrent text:\n${tip.text}`); + await ctx.reply(`Send the updated text for Drop #${tipId}.\n\nCurrent text:\n${tip.text}`); }); // tip_toggle_ (per-tip enable/disable, accessible from tiplist) @@ -7520,7 +10409,7 @@ bot.action(/^tip_toggle_(\d+)$/, async (ctx) => { tip.enabled = !tip.enabled; persistRuntimeState(); saveHelpfulMessages(); - await ctx.reply(`Tip #${tipId} is now ${tip.enabled ? '✅ enabled' : '🔇 disabled'}.`); + await ctx.reply(`Drop #${tipId} is now ${tip.enabled ? '✅ enabled' : '🔇 disabled'}.`); }); // ========================= @@ -7558,6 +10447,16 @@ bot.action('gwiz_title_skip', async (ctx) => { }); // ── Winners step (Step 3) ──────────────────────────────────────────────────── +/** + * gwizSetWinners executes its scoped Runewager logic and participates in menu/command or utility flow composition. + * Parameters: See the function signature for exact argument names and accepted values. + * Returns: Returns the computed value or a Promise resolving to the operation result; may return void for side-effect handlers. + * Side effects: May mutate runtime stores, pendingAction state, menu state, persistence files, logs, and callback progression. + * Validation/safety: Uses existing guard utilities (admin checks, input checks, path checks, cooldown checks) where applicable. + * Timeouts/fallbacks: Timeout and fallback behavior are controlled by the calling flow and global handler/state machine conventions. + * Errors: Surfaces user-facing error replies and/or logs when inputs, permissions, or dependencies are invalid. + * System fit: This function is part of the Runewager command/callback/state orchestration pipeline. + */ async function gwizSetWinners(ctx, count) { if (!requireAdmin(ctx)) return; const user = getUser(ctx); @@ -7582,6 +10481,16 @@ bot.action('gwiz_winners_custom', async (ctx) => { }); // ── SC per winner step (Step 2) ───────────────────────────────────────────── +/** + * gwizSetSc executes its scoped Runewager logic and participates in menu/command or utility flow composition. + * Parameters: See the function signature for exact argument names and accepted values. + * Returns: Returns the computed value or a Promise resolving to the operation result; may return void for side-effect handlers. + * Side effects: May mutate runtime stores, pendingAction state, menu state, persistence files, logs, and callback progression. + * Validation/safety: Uses existing guard utilities (admin checks, input checks, path checks, cooldown checks) where applicable. + * Timeouts/fallbacks: Timeout and fallback behavior are controlled by the calling flow and global handler/state machine conventions. + * Errors: Surfaces user-facing error replies and/or logs when inputs, permissions, or dependencies are invalid. + * System fit: This function is part of the Runewager command/callback/state orchestration pipeline. + */ async function gwizSetSc(ctx, amount) { if (!requireAdmin(ctx)) return; const user = getUser(ctx); @@ -7606,6 +10515,16 @@ bot.action('gwiz_sc_custom', async (ctx) => { }); // ── Duration step (Step 4) ────────────────────────────────────────────────── +/** + * gwizSetDuration executes its scoped Runewager logic and participates in menu/command or utility flow composition. + * Parameters: See the function signature for exact argument names and accepted values. + * Returns: Returns the computed value or a Promise resolving to the operation result; may return void for side-effect handlers. + * Side effects: May mutate runtime stores, pendingAction state, menu state, persistence files, logs, and callback progression. + * Validation/safety: Uses existing guard utilities (admin checks, input checks, path checks, cooldown checks) where applicable. + * Timeouts/fallbacks: Timeout and fallback behavior are controlled by the calling flow and global handler/state machine conventions. + * Errors: Surfaces user-facing error replies and/or logs when inputs, permissions, or dependencies are invalid. + * System fit: This function is part of the Runewager command/callback/state orchestration pipeline. + */ async function gwizSetDuration(ctx, minutes) { if (!requireAdmin(ctx)) return; const user = getUser(ctx); @@ -7632,6 +10551,16 @@ bot.action('gwiz_dur_custom', async (ctx) => { }); // ── Min participants step (Step 5) ────────────────────────────────────────── +/** + * gwizSetMinParts executes its scoped Runewager logic and participates in menu/command or utility flow composition. + * Parameters: See the function signature for exact argument names and accepted values. + * Returns: Returns the computed value or a Promise resolving to the operation result; may return void for side-effect handlers. + * Side effects: May mutate runtime stores, pendingAction state, menu state, persistence files, logs, and callback progression. + * Validation/safety: Uses existing guard utilities (admin checks, input checks, path checks, cooldown checks) where applicable. + * Timeouts/fallbacks: Timeout and fallback behavior are controlled by the calling flow and global handler/state machine conventions. + * Errors: Surfaces user-facing error replies and/or logs when inputs, permissions, or dependencies are invalid. + * System fit: This function is part of the Runewager command/callback/state orchestration pipeline. + */ async function gwizSetMinParts(ctx, count) { if (!requireAdmin(ctx)) return; const user = getUser(ctx); @@ -7659,6 +10588,16 @@ bot.action('gwiz_minp_custom', async (ctx) => { }); // ── Join surface step (Step 6) — toggle group and DM independently ────────── +/** + * gwizToggleSurface executes its scoped Runewager logic and participates in menu/command or utility flow composition. + * Parameters: See the function signature for exact argument names and accepted values. + * Returns: Returns the computed value or a Promise resolving to the operation result; may return void for side-effect handlers. + * Side effects: May mutate runtime stores, pendingAction state, menu state, persistence files, logs, and callback progression. + * Validation/safety: Uses existing guard utilities (admin checks, input checks, path checks, cooldown checks) where applicable. + * Timeouts/fallbacks: Timeout and fallback behavior are controlled by the calling flow and global handler/state machine conventions. + * Errors: Surfaces user-facing error replies and/or logs when inputs, permissions, or dependencies are invalid. + * System fit: This function is part of the Runewager command/callback/state orchestration pipeline. + */ async function gwizToggleSurface(ctx, field) { if (!requireAdmin(ctx)) return; const user = getUser(ctx); @@ -7739,6 +10678,16 @@ bot.action('gw_create_yes', async (ctx) => { // ========================= // Helpers: Walkthrough // ========================= +/** + * sendWalkthroughStep executes its scoped Runewager logic and participates in menu/command or utility flow composition. + * Parameters: See the function signature for exact argument names and accepted values. + * Returns: Returns the computed value or a Promise resolving to the operation result; may return void for side-effect handlers. + * Side effects: May mutate runtime stores, pendingAction state, menu state, persistence files, logs, and callback progression. + * Validation/safety: Uses existing guard utilities (admin checks, input checks, path checks, cooldown checks) where applicable. + * Timeouts/fallbacks: Timeout and fallback behavior are controlled by the calling flow and global handler/state machine conventions. + * Errors: Surfaces user-facing error replies and/or logs when inputs, permissions, or dependencies are invalid. + * System fit: This function is part of the Runewager command/callback/state orchestration pipeline. + */ async function sendWalkthroughStep(ctx, user) { const idx = user.walkthrough.currentStep; const step = walkthroughCatalog[idx]; @@ -7762,6 +10711,26 @@ async function sendWalkthroughStep(ctx, user) { await ctx.reply(header, nav); } +/** + + * buildWalkthroughCatalog executes its scoped Runewager logic and participates in menu/command or utility flow composition. + + * Parameters: See the function signature for exact argument names and accepted values. + + * Returns: Returns the computed value or a Promise resolving to the operation result; may return void for side-effect handlers. + + * Side effects: May mutate runtime stores, pendingAction state, menu state, persistence files, logs, and callback progression. + + * Validation/safety: Uses existing guard utilities (admin checks, input checks, path checks, cooldown checks) where applicable. + + * Timeouts/fallbacks: Timeout and fallback behavior are controlled by the calling flow and global handler/state machine conventions. + + * Errors: Surfaces user-facing error replies and/or logs when inputs, permissions, or dependencies are invalid. + + * System fit: This function is part of the Runewager command/callback/state orchestration pipeline. + + */ + function buildWalkthroughCatalog() { return [ { title: '1) First Time Setup Wizard', body: 'Use /start, pass age gate, and open the main menu.' }, @@ -7802,6 +10771,26 @@ function buildWalkthroughCatalog() { ]; } +/** + + * buildGiveawayFeatureList executes its scoped Runewager logic and participates in menu/command or utility flow composition. + + * Parameters: See the function signature for exact argument names and accepted values. + + * Returns: Returns the computed value or a Promise resolving to the operation result; may return void for side-effect handlers. + + * Side effects: May mutate runtime stores, pendingAction state, menu state, persistence files, logs, and callback progression. + + * Validation/safety: Uses existing guard utilities (admin checks, input checks, path checks, cooldown checks) where applicable. + + * Timeouts/fallbacks: Timeout and fallback behavior are controlled by the calling flow and global handler/state machine conventions. + + * Errors: Surfaces user-facing error replies and/or logs when inputs, permissions, or dependencies are invalid. + + * System fit: This function is part of the Runewager command/callback/state orchestration pipeline. + + */ + function buildGiveawayFeatureList() { return [ 'Admin command /start_giveaway', @@ -7845,6 +10834,16 @@ function buildGiveawayFeatureList() { // ========================= // Helpers: Giveaway engine // ========================= +/** + * normalizeYesNo executes its scoped Runewager logic and participates in menu/command or utility flow composition. + * Parameters: See the function signature for exact argument names and accepted values. + * Returns: Returns the computed value or a Promise resolving to the operation result; may return void for side-effect handlers. + * Side effects: May mutate runtime stores, pendingAction state, menu state, persistence files, logs, and callback progression. + * Validation/safety: Uses existing guard utilities (admin checks, input checks, path checks, cooldown checks) where applicable. + * Timeouts/fallbacks: Timeout and fallback behavior are controlled by the calling flow and global handler/state machine conventions. + * Errors: Surfaces user-facing error replies and/or logs when inputs, permissions, or dependencies are invalid. + * System fit: This function is part of the Runewager command/callback/state orchestration pipeline. + */ function normalizeYesNo(text) { const t = String(text || '').toLowerCase(); if (['y', 'yes'].includes(t)) return true; @@ -7852,6 +10851,26 @@ function normalizeYesNo(text) { return null; } +/** + + * createGiveaway executes its scoped Runewager logic and participates in menu/command or utility flow composition. + + * Parameters: See the function signature for exact argument names and accepted values. + + * Returns: Returns the computed value or a Promise resolving to the operation result; may return void for side-effect handlers. + + * Side effects: May mutate runtime stores, pendingAction state, menu state, persistence files, logs, and callback progression. + + * Validation/safety: Uses existing guard utilities (admin checks, input checks, path checks, cooldown checks) where applicable. + + * Timeouts/fallbacks: Timeout and fallback behavior are controlled by the calling flow and global handler/state machine conventions. + + * Errors: Surfaces user-facing error replies and/or logs when inputs, permissions, or dependencies are invalid. + + * System fit: This function is part of the Runewager command/callback/state orchestration pipeline. + + */ + function createGiveaway(config) { const id = giveawayStore.counter; giveawayStore.counter += 1; @@ -7891,6 +10910,26 @@ function createGiveaway(config) { }; } +/** + + * announceGiveaway executes its scoped Runewager logic and participates in menu/command or utility flow composition. + + * Parameters: See the function signature for exact argument names and accepted values. + + * Returns: Returns the computed value or a Promise resolving to the operation result; may return void for side-effect handlers. + + * Side effects: May mutate runtime stores, pendingAction state, menu state, persistence files, logs, and callback progression. + + * Validation/safety: Uses existing guard utilities (admin checks, input checks, path checks, cooldown checks) where applicable. + + * Timeouts/fallbacks: Timeout and fallback behavior are controlled by the calling flow and global handler/state machine conventions. + + * Errors: Surfaces user-facing error replies and/or logs when inputs, permissions, or dependencies are invalid. + + * System fit: This function is part of the Runewager command/callback/state orchestration pipeline. + + */ + async function announceGiveaway(giveaway, botUsername) { const titleLine = giveaway.title ? `\n📌 *${escapeMarkdownV2(giveaway.title)}*\n` : ''; const minPartLine = giveaway.minParticipants > 0 @@ -7968,6 +11007,26 @@ async function announceGiveaway(giveaway, botUsername) { } } +/** + + * evaluateEligibility executes its scoped Runewager logic and participates in menu/command or utility flow composition. + + * Parameters: See the function signature for exact argument names and accepted values. + + * Returns: Returns the computed value or a Promise resolving to the operation result; may return void for side-effect handlers. + + * Side effects: May mutate runtime stores, pendingAction state, menu state, persistence files, logs, and callback progression. + + * Validation/safety: Uses existing guard utilities (admin checks, input checks, path checks, cooldown checks) where applicable. + + * Timeouts/fallbacks: Timeout and fallback behavior are controlled by the calling flow and global handler/state machine conventions. + + * Errors: Surfaces user-facing error replies and/or logs when inputs, permissions, or dependencies are invalid. + + * System fit: This function is part of the Runewager command/callback/state orchestration pipeline. + + */ + function evaluateEligibility(user, giveaway) { if (giveaway.requireLinked && needsLinkedUsername(user)) { return { ok: false, missingLink: true, message: 'You must link your Runewager username first using /link.' }; @@ -7998,12 +11057,52 @@ function evaluateEligibility(user, giveaway) { return { ok: true }; } +/** + + * resetGiveawayTimer executes its scoped Runewager logic and participates in menu/command or utility flow composition. + + * Parameters: See the function signature for exact argument names and accepted values. + + * Returns: Returns the computed value or a Promise resolving to the operation result; may return void for side-effect handlers. + + * Side effects: May mutate runtime stores, pendingAction state, menu state, persistence files, logs, and callback progression. + + * Validation/safety: Uses existing guard utilities (admin checks, input checks, path checks, cooldown checks) where applicable. + + * Timeouts/fallbacks: Timeout and fallback behavior are controlled by the calling flow and global handler/state machine conventions. + + * Errors: Surfaces user-facing error replies and/or logs when inputs, permissions, or dependencies are invalid. + + * System fit: This function is part of the Runewager command/callback/state orchestration pipeline. + + */ + function resetGiveawayTimer(giveaway) { if (giveaway.endTimer) clearTimeout(giveaway.endTimer); const ms = Math.max(1000, giveaway.endTime - Date.now()); giveaway.endTimer = setTimeout(() => finalizeGiveaway(giveaway.id), ms); } +/** + + * scheduleGiveawayReminders executes its scoped Runewager logic and participates in menu/command or utility flow composition. + + * Parameters: See the function signature for exact argument names and accepted values. + + * Returns: Returns the computed value or a Promise resolving to the operation result; may return void for side-effect handlers. + + * Side effects: May mutate runtime stores, pendingAction state, menu state, persistence files, logs, and callback progression. + + * Validation/safety: Uses existing guard utilities (admin checks, input checks, path checks, cooldown checks) where applicable. + + * Timeouts/fallbacks: Timeout and fallback behavior are controlled by the calling flow and global handler/state machine conventions. + + * Errors: Surfaces user-facing error replies and/or logs when inputs, permissions, or dependencies are invalid. + + * System fit: This function is part of the Runewager command/callback/state orchestration pipeline. + + */ + function scheduleGiveawayReminders(giveaway) { const points = [10, 5, 1]; points.forEach((m) => { @@ -8019,6 +11118,26 @@ function scheduleGiveawayReminders(giveaway) { }); } +/** + + * finalizeGiveaway executes its scoped Runewager logic and participates in menu/command or utility flow composition. + + * Parameters: See the function signature for exact argument names and accepted values. + + * Returns: Returns the computed value or a Promise resolving to the operation result; may return void for side-effect handlers. + + * Side effects: May mutate runtime stores, pendingAction state, menu state, persistence files, logs, and callback progression. + + * Validation/safety: Uses existing guard utilities (admin checks, input checks, path checks, cooldown checks) where applicable. + + * Timeouts/fallbacks: Timeout and fallback behavior are controlled by the calling flow and global handler/state machine conventions. + + * Errors: Surfaces user-facing error replies and/or logs when inputs, permissions, or dependencies are invalid. + + * System fit: This function is part of the Runewager command/callback/state orchestration pipeline. + + */ + async function finalizeGiveaway(gwId, forceEnd = false) { const giveaway = giveawayStore.running.get(gwId); if (!giveaway) return; @@ -8109,6 +11228,26 @@ async function finalizeGiveaway(gwId, forceEnd = false) { ); } +/** + + * pickRandomUnique executes its scoped Runewager logic and participates in menu/command or utility flow composition. + + * Parameters: See the function signature for exact argument names and accepted values. + + * Returns: Returns the computed value or a Promise resolving to the operation result; may return void for side-effect handlers. + + * Side effects: May mutate runtime stores, pendingAction state, menu state, persistence files, logs, and callback progression. + + * Validation/safety: Uses existing guard utilities (admin checks, input checks, path checks, cooldown checks) where applicable. + + * Timeouts/fallbacks: Timeout and fallback behavior are controlled by the calling flow and global handler/state machine conventions. + + * Errors: Surfaces user-facing error replies and/or logs when inputs, permissions, or dependencies are invalid. + + * System fit: This function is part of the Runewager command/callback/state orchestration pipeline. + + */ + function pickRandomUnique(arr, count) { const copy = [...arr]; for (let i = copy.length - 1; i > 0; i -= 1) { @@ -8118,6 +11257,26 @@ function pickRandomUnique(arr, count) { return copy.slice(0, count); } +/** + + * renderWinnersList executes its scoped Runewager logic and participates in menu/command or utility flow composition. + + * Parameters: See the function signature for exact argument names and accepted values. + + * Returns: Returns the computed value or a Promise resolving to the operation result; may return void for side-effect handlers. + + * Side effects: May mutate runtime stores, pendingAction state, menu state, persistence files, logs, and callback progression. + + * Validation/safety: Uses existing guard utilities (admin checks, input checks, path checks, cooldown checks) where applicable. + + * Timeouts/fallbacks: Timeout and fallback behavior are controlled by the calling flow and global handler/state machine conventions. + + * Errors: Surfaces user-facing error replies and/or logs when inputs, permissions, or dependencies are invalid. + + * System fit: This function is part of the Runewager command/callback/state orchestration pipeline. + + */ + function renderWinnersList(winners) { if (!winners.length) return 'None'; return winners @@ -8128,20 +11287,100 @@ function renderWinnersList(winners) { .join('\n'); } -function renderWinnersText(giveaway) { - return `🎉 Giveaway ended!\nWinners (${giveaway.scPerWinner} SC each):\n${renderWinnersList(giveaway.winners)}\n\nAdmin will handle prize distribution.`; -} +/** + + * renderWinnersText executes its scoped Runewager logic and participates in menu/command or utility flow composition. + + * Parameters: See the function signature for exact argument names and accepted values. + + * Returns: Returns the computed value or a Promise resolving to the operation result; may return void for side-effect handlers. + + * Side effects: May mutate runtime stores, pendingAction state, menu state, persistence files, logs, and callback progression. + + * Validation/safety: Uses existing guard utilities (admin checks, input checks, path checks, cooldown checks) where applicable. + + * Timeouts/fallbacks: Timeout and fallback behavior are controlled by the calling flow and global handler/state machine conventions. + + * Errors: Surfaces user-facing error replies and/or logs when inputs, permissions, or dependencies are invalid. + + * System fit: This function is part of the Runewager command/callback/state orchestration pipeline. + + */ + +function renderWinnersText(giveaway) { + return `🎉 Giveaway ended!\nWinners (${giveaway.scPerWinner} SC each):\n${renderWinnersList(giveaway.winners)}\n\nAdmin will handle prize distribution.`; +} + +/** + + * renderGiveawaySummary executes its scoped Runewager logic and participates in menu/command or utility flow composition. + + * Parameters: See the function signature for exact argument names and accepted values. + + * Returns: Returns the computed value or a Promise resolving to the operation result; may return void for side-effect handlers. + + * Side effects: May mutate runtime stores, pendingAction state, menu state, persistence files, logs, and callback progression. + + * Validation/safety: Uses existing guard utilities (admin checks, input checks, path checks, cooldown checks) where applicable. + + * Timeouts/fallbacks: Timeout and fallback behavior are controlled by the calling flow and global handler/state machine conventions. + + * Errors: Surfaces user-facing error replies and/or logs when inputs, permissions, or dependencies are invalid. + + * System fit: This function is part of the Runewager command/callback/state orchestration pipeline. + + */ function renderGiveawaySummary(giveaway) { return `Giveaway #${giveaway.id}\nStatus: ${giveaway.status}\nChat: ${giveaway.chatId}\nWinners target: ${giveaway.maxWinners}\nSC each: ${giveaway.scPerWinner}\nParticipants: ${giveaway.participants.size}\nEnds: ${new Date(giveaway.endTime).toISOString()}`; } +/** + + * renderGiveawayExport executes its scoped Runewager logic and participates in menu/command or utility flow composition. + + * Parameters: See the function signature for exact argument names and accepted values. + + * Returns: Returns the computed value or a Promise resolving to the operation result; may return void for side-effect handlers. + + * Side effects: May mutate runtime stores, pendingAction state, menu state, persistence files, logs, and callback progression. + + * Validation/safety: Uses existing guard utilities (admin checks, input checks, path checks, cooldown checks) where applicable. + + * Timeouts/fallbacks: Timeout and fallback behavior are controlled by the calling flow and global handler/state machine conventions. + + * Errors: Surfaces user-facing error replies and/or logs when inputs, permissions, or dependencies are invalid. + + * System fit: This function is part of the Runewager command/callback/state orchestration pipeline. + + */ + function renderGiveawayExport(giveaway) { const participants = Array.from(giveaway.participants.values()); const participantLines = participants.map((p) => `- ${p.userId} @${p.tgUsername} | RW: ${p.runewagerUsername}`).join('\n') || 'None'; return `EXPORT GIVEAWAY #${giveaway.id}\nStatus: ${giveaway.status}\nChat ID: ${giveaway.chatId}\nCreated: ${new Date(giveaway.createdAt).toISOString()}\nEnd: ${new Date(giveaway.endTime).toISOString()}\nSC each: ${giveaway.scPerWinner}\nWinners count target: ${giveaway.maxWinners}\nParticipants (${participants.length}):\n${participantLines}\nWinners:\n${renderWinnersList(giveaway.winners)}\nPaid out: ${giveaway.paidOut ? 'yes' : 'no'}`; } +/** + + * findUserByRunewager executes its scoped Runewager logic and participates in menu/command or utility flow composition. + + * Parameters: See the function signature for exact argument names and accepted values. + + * Returns: Returns the computed value or a Promise resolving to the operation result; may return void for side-effect handlers. + + * Side effects: May mutate runtime stores, pendingAction state, menu state, persistence files, logs, and callback progression. + + * Validation/safety: Uses existing guard utilities (admin checks, input checks, path checks, cooldown checks) where applicable. + + * Timeouts/fallbacks: Timeout and fallback behavior are controlled by the calling flow and global handler/state machine conventions. + + * Errors: Surfaces user-facing error replies and/or logs when inputs, permissions, or dependencies are invalid. + + * System fit: This function is part of the Runewager command/callback/state orchestration pipeline. + + */ + function findUserByRunewager(runewagerUsername) { const needle = normalizeRunewagerUsername(runewagerUsername); if (!needle) return null; @@ -8149,6 +11388,26 @@ function findUserByRunewager(runewagerUsername) { .find((u) => normalizeRunewagerUsername(u.runewagerUsername) === needle) || null; } +/** + + * findUserByTelegramIdOrRunewager executes its scoped Runewager logic and participates in menu/command or utility flow composition. + + * Parameters: See the function signature for exact argument names and accepted values. + + * Returns: Returns the computed value or a Promise resolving to the operation result; may return void for side-effect handlers. + + * Side effects: May mutate runtime stores, pendingAction state, menu state, persistence files, logs, and callback progression. + + * Validation/safety: Uses existing guard utilities (admin checks, input checks, path checks, cooldown checks) where applicable. + + * Timeouts/fallbacks: Timeout and fallback behavior are controlled by the calling flow and global handler/state machine conventions. + + * Errors: Surfaces user-facing error replies and/or logs when inputs, permissions, or dependencies are invalid. + + * System fit: This function is part of the Runewager command/callback/state orchestration pipeline. + + */ + function findUserByTelegramIdOrRunewager(input) { const trimmed = String(input || '').trim(); if (!trimmed) return null; @@ -8157,6 +11416,26 @@ function findUserByTelegramIdOrRunewager(input) { return findUserByRunewager(trimmed); } +/** + + * renderWager30Lookup executes its scoped Runewager logic and participates in menu/command or utility flow composition. + + * Parameters: See the function signature for exact argument names and accepted values. + + * Returns: Returns the computed value or a Promise resolving to the operation result; may return void for side-effect handlers. + + * Side effects: May mutate runtime stores, pendingAction state, menu state, persistence files, logs, and callback progression. + + * Validation/safety: Uses existing guard utilities (admin checks, input checks, path checks, cooldown checks) where applicable. + + * Timeouts/fallbacks: Timeout and fallback behavior are controlled by the calling flow and global handler/state machine conventions. + + * Errors: Surfaces user-facing error replies and/or logs when inputs, permissions, or dependencies are invalid. + + * System fit: This function is part of the Runewager command/callback/state orchestration pipeline. + + */ + function renderWager30Lookup(u) { const wb = u.wagerBonus; const ts = wb.lastRequestAt ? new Date(wb.lastRequestAt).toISOString() : 'n/a'; @@ -8174,6 +11453,26 @@ function renderWager30Lookup(u) { + `Deny reason: ${wb.denyReason || 'n/a'}`; } +/** + + * buildWager30Stats executes its scoped Runewager logic and participates in menu/command or utility flow composition. + + * Parameters: See the function signature for exact argument names and accepted values. + + * Returns: Returns the computed value or a Promise resolving to the operation result; may return void for side-effect handlers. + + * Side effects: May mutate runtime stores, pendingAction state, menu state, persistence files, logs, and callback progression. + + * Validation/safety: Uses existing guard utilities (admin checks, input checks, path checks, cooldown checks) where applicable. + + * Timeouts/fallbacks: Timeout and fallback behavior are controlled by the calling flow and global handler/state machine conventions. + + * Errors: Surfaces user-facing error replies and/or logs when inputs, permissions, or dependencies are invalid. + + * System fit: This function is part of the Runewager command/callback/state orchestration pipeline. + + */ + function buildWager30Stats() { const stats = { pending: 0, approved: 0, bonusSent: 0, denied: 0, maxAttempts: 0 }; for (const [, u] of userStore) { @@ -8187,6 +11486,26 @@ function buildWager30Stats() { return stats; } +/** + + * runWeeklyWagerReminder executes its scoped Runewager logic and participates in menu/command or utility flow composition. + + * Parameters: See the function signature for exact argument names and accepted values. + + * Returns: Returns the computed value or a Promise resolving to the operation result; may return void for side-effect handlers. + + * Side effects: May mutate runtime stores, pendingAction state, menu state, persistence files, logs, and callback progression. + + * Validation/safety: Uses existing guard utilities (admin checks, input checks, path checks, cooldown checks) where applicable. + + * Timeouts/fallbacks: Timeout and fallback behavior are controlled by the calling flow and global handler/state machine conventions. + + * Errors: Surfaces user-facing error replies and/or logs when inputs, permissions, or dependencies are invalid. + + * System fit: This function is part of the Runewager command/callback/state orchestration pipeline. + + */ + async function runWeeklyWagerReminder() { for (const [, user] of userStore) { if (!user.wagerBonus) continue; @@ -8214,6 +11533,26 @@ async function runWeeklyWagerReminder() { } } +/** + + * notifyAdmins executes its scoped Runewager logic and participates in menu/command or utility flow composition. + + * Parameters: See the function signature for exact argument names and accepted values. + + * Returns: Returns the computed value or a Promise resolving to the operation result; may return void for side-effect handlers. + + * Side effects: May mutate runtime stores, pendingAction state, menu state, persistence files, logs, and callback progression. + + * Validation/safety: Uses existing guard utilities (admin checks, input checks, path checks, cooldown checks) where applicable. + + * Timeouts/fallbacks: Timeout and fallback behavior are controlled by the calling flow and global handler/state machine conventions. + + * Errors: Surfaces user-facing error replies and/or logs when inputs, permissions, or dependencies are invalid. + + * System fit: This function is part of the Runewager command/callback/state orchestration pipeline. + + */ + async function notifyAdmins(text) { const giveawayId = extractGwId(text); const shouldIncludeGiveawayActions = giveawayId > 0; @@ -8237,6 +11576,26 @@ async function notifyAdmins(text) { } } +/** + + * notifyAdminsPlain executes its scoped Runewager logic and participates in menu/command or utility flow composition. + + * Parameters: See the function signature for exact argument names and accepted values. + + * Returns: Returns the computed value or a Promise resolving to the operation result; may return void for side-effect handlers. + + * Side effects: May mutate runtime stores, pendingAction state, menu state, persistence files, logs, and callback progression. + + * Validation/safety: Uses existing guard utilities (admin checks, input checks, path checks, cooldown checks) where applicable. + + * Timeouts/fallbacks: Timeout and fallback behavior are controlled by the calling flow and global handler/state machine conventions. + + * Errors: Surfaces user-facing error replies and/or logs when inputs, permissions, or dependencies are invalid. + + * System fit: This function is part of the Runewager command/callback/state orchestration pipeline. + + */ + async function notifyAdminsPlain(text) { for (const adminId of ADMIN_IDS) { try { @@ -8278,6 +11637,26 @@ bot.action('page_noop', async (ctx) => { await ctx.answerCbQuery(); }); // 30 SC Bonus admin command helpers // ========================= +/** + + * bonusAdminListPending executes its scoped Runewager logic and participates in menu/command or utility flow composition. + + * Parameters: See the function signature for exact argument names and accepted values. + + * Returns: Returns the computed value or a Promise resolving to the operation result; may return void for side-effect handlers. + + * Side effects: May mutate runtime stores, pendingAction state, menu state, persistence files, logs, and callback progression. + + * Validation/safety: Uses existing guard utilities (admin checks, input checks, path checks, cooldown checks) where applicable. + + * Timeouts/fallbacks: Timeout and fallback behavior are controlled by the calling flow and global handler/state machine conventions. + + * Errors: Surfaces user-facing error replies and/or logs when inputs, permissions, or dependencies are invalid. + + * System fit: This function is part of the Runewager command/callback/state orchestration pipeline. + + */ + async function bonusAdminListPending(ctx, page = 1) { const pending = Array.from(userStore.values()).filter((u) => u.wagerBonus && u.wagerBonus.status === 'pending'); if (!pending.length) { @@ -8322,6 +11701,26 @@ bot.action(/^page_bonus_pending_(\d+)$/, async (ctx) => { await bonusAdminListPending(ctx, page); }); +/** + + * bonusAdminApprove executes its scoped Runewager logic and participates in menu/command or utility flow composition. + + * Parameters: See the function signature for exact argument names and accepted values. + + * Returns: Returns the computed value or a Promise resolving to the operation result; may return void for side-effect handlers. + + * Side effects: May mutate runtime stores, pendingAction state, menu state, persistence files, logs, and callback progression. + + * Validation/safety: Uses existing guard utilities (admin checks, input checks, path checks, cooldown checks) where applicable. + + * Timeouts/fallbacks: Timeout and fallback behavior are controlled by the calling flow and global handler/state machine conventions. + + * Errors: Surfaces user-facing error replies and/or logs when inputs, permissions, or dependencies are invalid. + + * System fit: This function is part of the Runewager command/callback/state orchestration pipeline. + + */ + async function bonusAdminApprove(ctx, rawId) { const target = findUserByTelegramIdOrRunewager(rawId); if (!target) return ctx.reply(`User not found: ${rawId}`); @@ -8341,6 +11740,26 @@ async function bonusAdminApprove(ctx, rawId) { } catch (e) { /* ignore */ } } +/** + + * bonusAdminDeny executes its scoped Runewager logic and participates in menu/command or utility flow composition. + + * Parameters: See the function signature for exact argument names and accepted values. + + * Returns: Returns the computed value or a Promise resolving to the operation result; may return void for side-effect handlers. + + * Side effects: May mutate runtime stores, pendingAction state, menu state, persistence files, logs, and callback progression. + + * Validation/safety: Uses existing guard utilities (admin checks, input checks, path checks, cooldown checks) where applicable. + + * Timeouts/fallbacks: Timeout and fallback behavior are controlled by the calling flow and global handler/state machine conventions. + + * Errors: Surfaces user-facing error replies and/or logs when inputs, permissions, or dependencies are invalid. + + * System fit: This function is part of the Runewager command/callback/state orchestration pipeline. + + */ + async function bonusAdminDeny(ctx, rawId, reason) { const target = findUserByTelegramIdOrRunewager(rawId); if (!target) return ctx.reply(`User not found: ${rawId}`); @@ -8359,6 +11778,26 @@ async function bonusAdminDeny(ctx, rawId, reason) { } catch (e) { /* ignore */ } } +/** + + * bonusAdminSent executes its scoped Runewager logic and participates in menu/command or utility flow composition. + + * Parameters: See the function signature for exact argument names and accepted values. + + * Returns: Returns the computed value or a Promise resolving to the operation result; may return void for side-effect handlers. + + * Side effects: May mutate runtime stores, pendingAction state, menu state, persistence files, logs, and callback progression. + + * Validation/safety: Uses existing guard utilities (admin checks, input checks, path checks, cooldown checks) where applicable. + + * Timeouts/fallbacks: Timeout and fallback behavior are controlled by the calling flow and global handler/state machine conventions. + + * Errors: Surfaces user-facing error replies and/or logs when inputs, permissions, or dependencies are invalid. + + * System fit: This function is part of the Runewager command/callback/state orchestration pipeline. + + */ + async function bonusAdminSent(ctx, rawId) { const target = findUserByTelegramIdOrRunewager(rawId); if (!target) return ctx.reply(`User not found: ${rawId}`); @@ -8377,6 +11816,26 @@ async function bonusAdminSent(ctx, rawId) { } catch (e) { /* ignore */ } } +/** + + * bonusAdminAdd executes its scoped Runewager logic and participates in menu/command or utility flow composition. + + * Parameters: See the function signature for exact argument names and accepted values. + + * Returns: Returns the computed value or a Promise resolving to the operation result; may return void for side-effect handlers. + + * Side effects: May mutate runtime stores, pendingAction state, menu state, persistence files, logs, and callback progression. + + * Validation/safety: Uses existing guard utilities (admin checks, input checks, path checks, cooldown checks) where applicable. + + * Timeouts/fallbacks: Timeout and fallback behavior are controlled by the calling flow and global handler/state machine conventions. + + * Errors: Surfaces user-facing error replies and/or logs when inputs, permissions, or dependencies are invalid. + + * System fit: This function is part of the Runewager command/callback/state orchestration pipeline. + + */ + async function bonusAdminAdd(ctx, rawId) { const target = findUserByTelegramIdOrRunewager(rawId); if (!target) return ctx.reply(`User not found: ${rawId}`); @@ -8390,11 +11849,51 @@ async function bonusAdminAdd(ctx, rawId) { await ctx.reply(`➕ Manual record added. TG ID: ${target.id} (@${addedName}) — status: bonus_sent`); } +/** + + * extractGwId executes its scoped Runewager logic and participates in menu/command or utility flow composition. + + * Parameters: See the function signature for exact argument names and accepted values. + + * Returns: Returns the computed value or a Promise resolving to the operation result; may return void for side-effect handlers. + + * Side effects: May mutate runtime stores, pendingAction state, menu state, persistence files, logs, and callback progression. + + * Validation/safety: Uses existing guard utilities (admin checks, input checks, path checks, cooldown checks) where applicable. + + * Timeouts/fallbacks: Timeout and fallback behavior are controlled by the calling flow and global handler/state machine conventions. + + * Errors: Surfaces user-facing error replies and/or logs when inputs, permissions, or dependencies are invalid. + + * System fit: This function is part of the Runewager command/callback/state orchestration pipeline. + + */ + function extractGwId(text) { const m = text.match(/#(\d+)/); return m ? Number(m[1]) : 0; } +/** + + * normalizeRunewagerUsername executes its scoped Runewager logic and participates in menu/command or utility flow composition. + + * Parameters: See the function signature for exact argument names and accepted values. + + * Returns: Returns the computed value or a Promise resolving to the operation result; may return void for side-effect handlers. + + * Side effects: May mutate runtime stores, pendingAction state, menu state, persistence files, logs, and callback progression. + + * Validation/safety: Uses existing guard utilities (admin checks, input checks, path checks, cooldown checks) where applicable. + + * Timeouts/fallbacks: Timeout and fallback behavior are controlled by the calling flow and global handler/state machine conventions. + + * Errors: Surfaces user-facing error replies and/or logs when inputs, permissions, or dependencies are invalid. + + * System fit: This function is part of the Runewager command/callback/state orchestration pipeline. + + */ + function normalizeRunewagerUsername(text) { return String(text || '').trim().replace(/^@+/, '').toLowerCase(); } @@ -8409,6 +11908,16 @@ bot.command('testall', async (ctx) => { // Category-scoped result accumulator const cats = {}; + /** + * addResult executes its scoped Runewager logic and participates in menu/command or utility flow composition. + * Parameters: See the function signature for exact argument names and accepted values. + * Returns: Returns the computed value or a Promise resolving to the operation result; may return void for side-effect handlers. + * Side effects: May mutate runtime stores, pendingAction state, menu state, persistence files, logs, and callback progression. + * Validation/safety: Uses existing guard utilities (admin checks, input checks, path checks, cooldown checks) where applicable. + * Timeouts/fallbacks: Timeout and fallback behavior are controlled by the calling flow and global handler/state machine conventions. + * Errors: Surfaces user-facing error replies and/or logs when inputs, permissions, or dependencies are invalid. + * System fit: This function is part of the Runewager command/callback/state orchestration pipeline. + */ function addResult(cat, ok, label, detail = '') { if (!cats[cat]) cats[cat] = []; cats[cat].push({ ok, label, detail }); @@ -8783,6 +12292,26 @@ bot.action(/^tgw_abort_(\d+)$/, async (ctx) => { await ctx.reply(`🧪 Test Giveaway #${gwId} aborted and cleaned up.`); }); +/** + + * runTestGiveawayFinale executes its scoped Runewager logic and participates in menu/command or utility flow composition. + + * Parameters: See the function signature for exact argument names and accepted values. + + * Returns: Returns the computed value or a Promise resolving to the operation result; may return void for side-effect handlers. + + * Side effects: May mutate runtime stores, pendingAction state, menu state, persistence files, logs, and callback progression. + + * Validation/safety: Uses existing guard utilities (admin checks, input checks, path checks, cooldown checks) where applicable. + + * Timeouts/fallbacks: Timeout and fallback behavior are controlled by the calling flow and global handler/state machine conventions. + + * Errors: Surfaces user-facing error replies and/or logs when inputs, permissions, or dependencies are invalid. + + * System fit: This function is part of the Runewager command/callback/state orchestration pipeline. + + */ + async function runTestGiveawayFinale(gw, adminId) { const gwId = gw.id; const allParticipants = Array.from(gw.participants.values()); @@ -8830,6 +12359,26 @@ async function runTestGiveawayFinale(gw, adminId) { } } +/** + + * startHealthServer executes its scoped Runewager logic and participates in menu/command or utility flow composition. + + * Parameters: See the function signature for exact argument names and accepted values. + + * Returns: Returns the computed value or a Promise resolving to the operation result; may return void for side-effect handlers. + + * Side effects: May mutate runtime stores, pendingAction state, menu state, persistence files, logs, and callback progression. + + * Validation/safety: Uses existing guard utilities (admin checks, input checks, path checks, cooldown checks) where applicable. + + * Timeouts/fallbacks: Timeout and fallback behavior are controlled by the calling flow and global handler/state machine conventions. + + * Errors: Surfaces user-facing error replies and/or logs when inputs, permissions, or dependencies are invalid. + + * System fit: This function is part of the Runewager command/callback/state orchestration pipeline. + + */ + function startHealthServer() { const port = Number(process.env.PORT || 3000); const tlsKeyPath = process.env.HTTPS_KEY_PATH; @@ -8940,6 +12489,16 @@ Falling back to HTTP-only health server.`); // ── Admin Activity Log store (Feature 9) ───────────────────────────────── const adminActivityLog = []; const MAX_ADMIN_LOG = 500; +/** + * logAdminAction executes its scoped Runewager logic and participates in menu/command or utility flow composition. + * Parameters: See the function signature for exact argument names and accepted values. + * Returns: Returns the computed value or a Promise resolving to the operation result; may return void for side-effect handlers. + * Side effects: May mutate runtime stores, pendingAction state, menu state, persistence files, logs, and callback progression. + * Validation/safety: Uses existing guard utilities (admin checks, input checks, path checks, cooldown checks) where applicable. + * Timeouts/fallbacks: Timeout and fallback behavior are controlled by the calling flow and global handler/state machine conventions. + * Errors: Surfaces user-facing error replies and/or logs when inputs, permissions, or dependencies are invalid. + * System fit: This function is part of the Runewager command/callback/state orchestration pipeline. + */ function logAdminAction(adminId, action, detail = '') { adminActivityLog.push({ ts: Date.now(), adminId, action, detail: String(detail).slice(0, 200) }); if (adminActivityLog.length > MAX_ADMIN_LOG) adminActivityLog.shift(); @@ -9043,6 +12602,16 @@ bot.command('scan_eligibility', safeAdminHandler('scan_eligibility', { usage: '/ })); // ── Feature 3: Auto-Expiring Referral Boosts cron ───────────────────────── +/** + * expireReferralBoosts executes its scoped Runewager logic and participates in menu/command or utility flow composition. + * Parameters: See the function signature for exact argument names and accepted values. + * Returns: Returns the computed value or a Promise resolving to the operation result; may return void for side-effect handlers. + * Side effects: May mutate runtime stores, pendingAction state, menu state, persistence files, logs, and callback progression. + * Validation/safety: Uses existing guard utilities (admin checks, input checks, path checks, cooldown checks) where applicable. + * Timeouts/fallbacks: Timeout and fallback behavior are controlled by the calling flow and global handler/state machine conventions. + * Errors: Surfaces user-facing error replies and/or logs when inputs, permissions, or dependencies are invalid. + * System fit: This function is part of the Runewager command/callback/state orchestration pipeline. + */ function expireReferralBoosts() { const now = Date.now(); for (const [, user] of userStore) { @@ -9133,6 +12702,29 @@ bot.command('pick_winner', safeAdminHandler('pick_winner', { usage: '/pick_winne })); // ── Feature 8: Group Validation ─────────────────────────────────────────── + +bot.command('register_chat', safeAdminHandler('register_chat', { usage: '/register_chat', example: '/register_chat (then forward a message)' }, async (ctx) => { + if (!requireAdmin(ctx)) return; + const user = getUser(ctx); + user.pendingAction = { type: 'await_register_chat_forward' }; + await ctx.reply('Forward any message from the target channel/group here. If the bot is in that chat with proper permissions, it will be registered for broadcasts and tips.'); +})); + +bot.command('verify_bot_setup', safeAdminHandler('verify_bot_setup', { usage: '/verify_bot_setup', example: '/verify_bot_setup' }, async (ctx) => { + if (!requireAdmin(ctx)) return; + const me = await bot.telegram.getMe(); + const checks = [ + `Bot: @${me.username} (${me.id})`, + `Can join groups: ${me.can_join_groups ? 'YES' : 'NO'}`, + `Can read all group messages: ${me.can_read_all_group_messages ? 'YES' : 'NO'} (BotFather privacy mode affects this)`, + `Supports inline queries: ${me.supports_inline_queries ? 'YES' : 'NO'}`, + `Announce channel target: ${broadcastConfigStore.targetChannel}`, + `Content Drops/broadcast group target: ${broadcastConfigStore.targetGroup}`, + `Approved broadcast/group chats tracked: ${approvedGroupsStore.size}`, + ]; + await ctx.reply(`🤖 *Bot Setup Verification*\n\n${checks.join('\n')}\n\nIf group visibility is limited, disable privacy mode in @BotFather and ensure admin rights in each target chat.`, { parse_mode: 'Markdown' }); +})); + bot.command('approve_group', safeAdminHandler('approve_group', { usage: '/approve_group ', example: '/approve_group -1001234567890' }, async (ctx) => { if (!requireAdmin(ctx)) return; const chatId = Number((ctx.message.text.split(/\s+/)[1])); @@ -9336,6 +12928,16 @@ bot.command('mygiveaways', async (ctx) => { }); // ── Feature 17: Auto-DM eligibility fix helper ──────────────────────────── +/** + * dmEligibilityFix executes its scoped Runewager logic and participates in menu/command or utility flow composition. + * Parameters: See the function signature for exact argument names and accepted values. + * Returns: Returns the computed value or a Promise resolving to the operation result; may return void for side-effect handlers. + * Side effects: May mutate runtime stores, pendingAction state, menu state, persistence files, logs, and callback progression. + * Validation/safety: Uses existing guard utilities (admin checks, input checks, path checks, cooldown checks) where applicable. + * Timeouts/fallbacks: Timeout and fallback behavior are controlled by the calling flow and global handler/state machine conventions. + * Errors: Surfaces user-facing error replies and/or logs when inputs, permissions, or dependencies are invalid. + * System fit: This function is part of the Runewager command/callback/state orchestration pipeline. + */ async function dmEligibilityFix(user, result) { if (!user || !result || result.ok) return; try { @@ -9457,54 +13059,20 @@ bot.command('gwhistory', async (ctx) => { // ── Feature 23: Promo Eligibility Check ────────────────────────────────── bot.command('promocheck', async (ctx) => { const user = getUser(ctx); - const activeCode = getActivePromoCodeForUser(user); - const issues = []; - if (!promoStore.active) issues.push('❌ Promo claims are currently *inactive*.'); - if (promoStore.remainingClaims <= 0) issues.push('❌ Promo claims are currently *exhausted*.'); - if (!activeCode) issues.push('❌ No active promo code is configured for your account tier right now.'); - - const cooldownRemaining = getPromoClaimCooldownRemainingMs(user); - if (cooldownRemaining > 0) { - issues.push(`⏳ Cooldown active — *${formatCooldown(cooldownRemaining)}* remaining before any next code claim.`); + const promos = listActivePromos(); + if (!promos.length) { + await ctx.reply('No active promos are configured in Promo Manager.'); + return; } - - if (!issues.length && activeCode) { - const requirementLabel = activeCode.requirementType === PROMO_REQUIREMENT_TYPE.NO_REQUIREMENT ? 'No extra wager requirement' : 'Wager requirement applies'; - await ctx.reply( - `✅ *You are eligible to claim a promo code!* - -Code: \`${activeCode.code}\` -Amount: *${activeCode.amountSC} SC* -Tier: *${activeCode.audience === PROMO_AUDIENCE.NEW_USER ? 'New User' : 'Existing User'}* -Requirement: *${requirementLabel}* - -Tap below to claim now.`, - { parse_mode: 'Markdown', ...Markup.inlineKeyboard([[Markup.button.callback('🎁 Claim Promo Now', 'menu_claim_bonus')]]) }, - ); - } else { - await ctx.reply(`📋 *Promo Eligibility Check* - -${issues.join('\n')} - -${AFFILIATE_REMINDER_TEXT}`, { - parse_mode: 'Markdown', - ...Markup.inlineKeyboard([[Markup.button.callback('📩 Contact Support', 'menu_bugreport')]]), - }); + const lines = ['📋 *Promo Eligibility Check*', '']; + for (const promo of promos) { + // eslint-disable-next-line no-await-in-loop + const result = await evaluatePromoEligibility(user, promo, { logFailure: true }); + lines.push(`• ${promo.name}: ${result.eligible ? '✅ eligible' : `❌ ${result.reasons.join('; ')}`}`); } + await ctx.reply(lines.join('\n'), { parse_mode: 'Markdown' }); }); - -// ── Feature 24: Language Info ───────────────────────────────────────────── -bot.command('language', async (ctx) => { - const user = getUser(ctx); - if (!user.language && ctx.from.language_code) user.language = ctx.from.language_code; - await ctx.reply( - `🌍 *Language Settings*\n\nDetected language: \`${user.language || ctx.from.language_code || 'unknown'}\`\n\nThis bot currently operates in *English only*.\nYour language code has been recorded for future multi-language support.`, - { parse_mode: 'Markdown' }, - ); -}); - -// ── Feature 25: DM-Only Support Portal ─────────────────────────────────── const SUPPORT_ISSUE_TYPES = ['Account not linked', 'Verification issue', 'Promo code problem', 'Giveaway issue', 'Bonus not received', 'Other']; bot.command('support', async (ctx) => { @@ -9532,6 +13100,16 @@ bot.action('support_cancel', async (ctx) => { }); // ── Feature 26: Weekly Auto-DM Boost Reminder ──────────────────────────── +/** + * runWeeklyBoostReminder executes its scoped Runewager logic and participates in menu/command or utility flow composition. + * Parameters: See the function signature for exact argument names and accepted values. + * Returns: Returns the computed value or a Promise resolving to the operation result; may return void for side-effect handlers. + * Side effects: May mutate runtime stores, pendingAction state, menu state, persistence files, logs, and callback progression. + * Validation/safety: Uses existing guard utilities (admin checks, input checks, path checks, cooldown checks) where applicable. + * Timeouts/fallbacks: Timeout and fallback behavior are controlled by the calling flow and global handler/state machine conventions. + * Errors: Surfaces user-facing error replies and/or logs when inputs, permissions, or dependencies are invalid. + * System fit: This function is part of the Runewager command/callback/state orchestration pipeline. + */ async function runWeeklyBoostReminder() { const now = Date.now(); const WEEK_MS_26 = 7 * 24 * 60 * 60 * 1000; let sent = 0; for (const [, user] of userStore) { @@ -9559,6 +13137,16 @@ async function runWeeklyBoostReminder() { // ========================= // Startup // ========================= +/** + * startBot executes its scoped Runewager logic and participates in menu/command or utility flow composition. + * Parameters: See the function signature for exact argument names and accepted values. + * Returns: Returns the computed value or a Promise resolving to the operation result; may return void for side-effect handlers. + * Side effects: May mutate runtime stores, pendingAction state, menu state, persistence files, logs, and callback progression. + * Validation/safety: Uses existing guard utilities (admin checks, input checks, path checks, cooldown checks) where applicable. + * Timeouts/fallbacks: Timeout and fallback behavior are controlled by the calling flow and global handler/state machine conventions. + * Errors: Surfaces user-facing error replies and/or logs when inputs, permissions, or dependencies are invalid. + * System fit: This function is part of the Runewager command/callback/state orchestration pipeline. + */ async function startBot() { try { loadPersistentData(); @@ -9595,7 +13183,7 @@ async function startBot() { } // Cleanup expired /sshv sessions every minute to keep memory usage bounded. -setInterval(() => { +const sshvGcTimer = setInterval(() => { const now = Date.now(); for (const [adminId, session] of sshvSessions.entries()) { if (now - (session.lastActivity || session.createdAt || 0) > SSHV_SESSION_TTL_MS) { @@ -9603,6 +13191,7 @@ setInterval(() => { } } }, 60 * 1000); +sshvGcTimer.unref(); // Fallback for unmatched callback_data: always acknowledge and provide recovery path. bot.action(/.*/, async (ctx) => { diff --git a/promo-message.js b/promo-message.js index 2dd8b2b..dab9d0a 100644 --- a/promo-message.js +++ b/promo-message.js @@ -4,6 +4,26 @@ const path = require('path'); const PROMO_MESSAGE_FILE = path.join(__dirname, 'data', 'promo_message.json'); const FALLBACK_PROMO_MESSAGE = 'Your promo is ready — tap below to claim.'; +/** + + * readPromoMessageFile executes its scoped Runewager logic and participates in menu/command or utility flow composition. + + * Parameters: See the function signature for exact argument names and accepted values. + + * Returns: Returns the computed value or a Promise resolving to the operation result; may return void for side-effect handlers. + + * Side effects: May mutate runtime stores, pendingAction state, menu state, persistence files, logs, and callback progression. + + * Validation/safety: Uses existing guard utilities (admin checks, input checks, path checks, cooldown checks) where applicable. + + * Timeouts/fallbacks: Timeout and fallback behavior are controlled by the calling flow and global handler/state machine conventions. + + * Errors: Surfaces user-facing error replies and/or logs when inputs, permissions, or dependencies are invalid. + + * System fit: This function is part of the Runewager command/callback/state orchestration pipeline. + + */ + function readPromoMessageFile() { try { if (!fs.existsSync(PROMO_MESSAGE_FILE)) return null; @@ -15,12 +35,52 @@ function readPromoMessageFile() { } } +/** + + * getPromoMessage executes its scoped Runewager logic and participates in menu/command or utility flow composition. + + * Parameters: See the function signature for exact argument names and accepted values. + + * Returns: Returns the computed value or a Promise resolving to the operation result; may return void for side-effect handlers. + + * Side effects: May mutate runtime stores, pendingAction state, menu state, persistence files, logs, and callback progression. + + * Validation/safety: Uses existing guard utilities (admin checks, input checks, path checks, cooldown checks) where applicable. + + * Timeouts/fallbacks: Timeout and fallback behavior are controlled by the calling flow and global handler/state machine conventions. + + * Errors: Surfaces user-facing error replies and/or logs when inputs, permissions, or dependencies are invalid. + + * System fit: This function is part of the Runewager command/callback/state orchestration pipeline. + + */ + function getPromoMessage() { return readPromoMessageFile() || String(process.env.DEFAULT_PROMO_MESSAGE || '').trim() || FALLBACK_PROMO_MESSAGE; } +/** + + * setPromoMessage executes its scoped Runewager logic and participates in menu/command or utility flow composition. + + * Parameters: See the function signature for exact argument names and accepted values. + + * Returns: Returns the computed value or a Promise resolving to the operation result; may return void for side-effect handlers. + + * Side effects: May mutate runtime stores, pendingAction state, menu state, persistence files, logs, and callback progression. + + * Validation/safety: Uses existing guard utilities (admin checks, input checks, path checks, cooldown checks) where applicable. + + * Timeouts/fallbacks: Timeout and fallback behavior are controlled by the calling flow and global handler/state machine conventions. + + * Errors: Surfaces user-facing error replies and/or logs when inputs, permissions, or dependencies are invalid. + + * System fit: This function is part of the Runewager command/callback/state orchestration pipeline. + + */ + function setPromoMessage(message) { const clean = String(message || '').trim(); if (!clean) throw new Error('Promo message cannot be empty.'); diff --git a/test/runtime.test.js b/test/runtime.test.js index 91cd4dc..9a38dc2 100644 --- a/test/runtime.test.js +++ b/test/runtime.test.js @@ -11,6 +11,26 @@ const runtimeState = path.join(dataDir, 'runtime-state.json'); const backupScript = path.join(rootDir, 'scripts', 'backup-runtime-state.sh'); const restoreScript = path.join(rootDir, 'scripts', 'restore-runtime-state.sh'); +/** + + * run executes its scoped Runewager logic and participates in menu/command or utility flow composition. + + * Parameters: See the function signature for exact argument names and accepted values. + + * Returns: Returns the computed value or a Promise resolving to the operation result; may return void for side-effect handlers. + + * Side effects: May mutate runtime stores, pendingAction state, menu state, persistence files, logs, and callback progression. + + * Validation/safety: Uses existing guard utilities (admin checks, input checks, path checks, cooldown checks) where applicable. + + * Timeouts/fallbacks: Timeout and fallback behavior are controlled by the calling flow and global handler/state machine conventions. + + * Errors: Surfaces user-facing error replies and/or logs when inputs, permissions, or dependencies are invalid. + + * System fit: This function is part of the Runewager command/callback/state orchestration pipeline. + + */ + function run(script, args) { return spawnSync('bash', [script, ...args], { encoding: 'utf8', cwd: rootDir, timeout: 10000 }); } diff --git a/test/smoke.test.js b/test/smoke.test.js index 108dfa0..6daa561 100644 --- a/test/smoke.test.js +++ b/test/smoke.test.js @@ -1,8 +1,366 @@ const { test } = require('node:test'); const assert = require('node:assert/strict'); const { spawnSync } = require('node:child_process'); +const fs = require('node:fs'); +const path = require('node:path'); + +/** + + * collectJsFiles executes its scoped Runewager logic and participates in menu/command or utility flow composition. + + * Parameters: See the function signature for exact argument names and accepted values. + + * Returns: Returns the computed value or a Promise resolving to the operation result; may return void for side-effect handlers. + + * Side effects: May mutate runtime stores, pendingAction state, menu state, persistence files, logs, and callback progression. + + * Validation/safety: Uses existing guard utilities (admin checks, input checks, path checks, cooldown checks) where applicable. + + * Timeouts/fallbacks: Timeout and fallback behavior are controlled by the calling flow and global handler/state machine conventions. + + * Errors: Surfaces user-facing error replies and/or logs when inputs, permissions, or dependencies are invalid. + + * System fit: This function is part of the Runewager command/callback/state orchestration pipeline. + + */ + +function collectJsFiles(rootDir) { + const files = []; + const skipDirs = new Set(['.git', 'node_modules', 'data', 'logs', 'test']); + + /** + + * walk executes its scoped Runewager logic and participates in menu/command or utility flow composition. + + * Parameters: See the function signature for exact argument names and accepted values. + + * Returns: Returns the computed value or a Promise resolving to the operation result; may return void for side-effect handlers. + + * Side effects: May mutate runtime stores, pendingAction state, menu state, persistence files, logs, and callback progression. + + * Validation/safety: Uses existing guard utilities (admin checks, input checks, path checks, cooldown checks) where applicable. + + * Timeouts/fallbacks: Timeout and fallback behavior are controlled by the calling flow and global handler/state machine conventions. + + * Errors: Surfaces user-facing error replies and/or logs when inputs, permissions, or dependencies are invalid. + + * System fit: This function is part of the Runewager command/callback/state orchestration pipeline. + + */ + + function walk(dir) { + for (const entry of fs.readdirSync(dir, { withFileTypes: true })) { + const fullPath = path.join(dir, entry.name); + let stats; + try { + stats = fs.lstatSync(fullPath); + } catch (_) { + continue; + } + if (stats.isSymbolicLink()) continue; + if (entry.isDirectory()) { + if (!skipDirs.has(entry.name)) walk(fullPath); + continue; + } + if (entry.isFile() && entry.name.endsWith('.js')) files.push(fullPath); + } + } + + walk(rootDir); + return files; +} + +/** + + * extractLiteralIds executes its scoped Runewager logic and participates in menu/command or utility flow composition. + + * Parameters: See the function signature for exact argument names and accepted values. + + * Returns: Returns the computed value or a Promise resolving to the operation result; may return void for side-effect handlers. + + * Side effects: May mutate runtime stores, pendingAction state, menu state, persistence files, logs, and callback progression. + + * Validation/safety: Uses existing guard utilities (admin checks, input checks, path checks, cooldown checks) where applicable. + + * Timeouts/fallbacks: Timeout and fallback behavior are controlled by the calling flow and global handler/state machine conventions. + + * Errors: Surfaces user-facing error replies and/or logs when inputs, permissions, or dependencies are invalid. + + * System fit: This function is part of the Runewager command/callback/state orchestration pipeline. + + */ + +function extractLiteralIds(source, kind) { + const regex = kind === 'callback' + ? /Markup\.button\.callback\([^,]+,\s*(["'`])((?:\\.|(?!\1).)*)\1\)/g + : /bot\.action\(\s*(["'`])((?:\\.|(?!\1).)*)\1/g; + const ids = []; + for (const m of source.matchAll(regex)) { + const quote = m[1]; + const id = m[2]; + // Skip templated callback ids (e.g. `help_page_${page + 1}`), they are dynamic. + if (kind === 'callback' && quote === '`' && id.includes('${')) continue; + ids.push(id); + } + return new Set(ids); +} + +/** + + * parseRegexLiteral executes its scoped Runewager logic and participates in menu/command or utility flow composition. + + * Parameters: See the function signature for exact argument names and accepted values. + + * Returns: Returns the computed value or a Promise resolving to the operation result; may return void for side-effect handlers. + + * Side effects: May mutate runtime stores, pendingAction state, menu state, persistence files, logs, and callback progression. + + * Validation/safety: Uses existing guard utilities (admin checks, input checks, path checks, cooldown checks) where applicable. + + * Timeouts/fallbacks: Timeout and fallback behavior are controlled by the calling flow and global handler/state machine conventions. + + * Errors: Surfaces user-facing error replies and/or logs when inputs, permissions, or dependencies are invalid. + + * System fit: This function is part of the Runewager command/callback/state orchestration pipeline. + + */ + +function parseRegexLiteral(source, slashIndex) { + let i = slashIndex + 1; + let escaped = false; + let inCharClass = false; + while (i < source.length) { + const ch = source[i]; + if (escaped) { + escaped = false; + } else if (ch === '\\') { + escaped = true; + } else if (ch === '[') { + inCharClass = true; + } else if (ch === ']' && inCharClass) { + inCharClass = false; + } else if (ch === '/' && !inCharClass) { + break; + } + i += 1; + } + if (i >= source.length) return null; + const patternSource = source.slice(slashIndex + 1, i); + let j = i + 1; + while (j < source.length && /[dgimsuvy]/.test(source[j])) j += 1; + const flags = source.slice(i + 1, j); + return { patternSource, flags, end: j }; +} + +/** + + * extractActionRegexPatterns executes its scoped Runewager logic and participates in menu/command or utility flow composition. + + * Parameters: See the function signature for exact argument names and accepted values. + + * Returns: Returns the computed value or a Promise resolving to the operation result; may return void for side-effect handlers. + + * Side effects: May mutate runtime stores, pendingAction state, menu state, persistence files, logs, and callback progression. + + * Validation/safety: Uses existing guard utilities (admin checks, input checks, path checks, cooldown checks) where applicable. + + * Timeouts/fallbacks: Timeout and fallback behavior are controlled by the calling flow and global handler/state machine conventions. + + * Errors: Surfaces user-facing error replies and/or logs when inputs, permissions, or dependencies are invalid. + + * System fit: This function is part of the Runewager command/callback/state orchestration pipeline. + + */ + +function extractActionRegexPatterns(source) { + const patterns = []; + const marker = 'bot.action('; + let start = 0; + + while (start < source.length) { + const idx = source.indexOf(marker, start); + if (idx === -1) break; + + let cursor = idx + marker.length; + while (cursor < source.length && /\s/.test(source[cursor])) cursor += 1; + if (source[cursor] !== '/') { + start = cursor; + continue; + } + + const parsed = parseRegexLiteral(source, cursor); + if (!parsed) { + start = cursor + 1; + continue; + } + + const { patternSource, flags, end } = parsed; + // Ignore generic catch-all handlers that would make coverage meaningless. + if (patternSource !== '.*' && patternSource !== '.+') { + try { + patterns.push(new RegExp(patternSource, flags)); + } catch (_) { + // Skip malformed patterns in this static smoke check. + } + } + + start = end; + } + + return patterns; +} + +/** + + * parseStringLiterals executes its scoped Runewager logic and participates in menu/command or utility flow composition. + + * Parameters: See the function signature for exact argument names and accepted values. + + * Returns: Returns the computed value or a Promise resolving to the operation result; may return void for side-effect handlers. + + * Side effects: May mutate runtime stores, pendingAction state, menu state, persistence files, logs, and callback progression. + + * Validation/safety: Uses existing guard utilities (admin checks, input checks, path checks, cooldown checks) where applicable. + + * Timeouts/fallbacks: Timeout and fallback behavior are controlled by the calling flow and global handler/state machine conventions. + + * Errors: Surfaces user-facing error replies and/or logs when inputs, permissions, or dependencies are invalid. + + * System fit: This function is part of the Runewager command/callback/state orchestration pipeline. + + */ + +function parseStringLiterals(source) { + const values = []; + let i = 0; + while (i < source.length) { + const ch = source[i]; + if (ch !== '"' && ch !== "'" && ch !== '`') { + i += 1; + continue; + } + const quote = ch; + let j = i + 1; + let escaped = false; + while (j < source.length) { + const cj = source[j]; + if (escaped) { + escaped = false; + } else if (cj === '\\') { + escaped = true; + } else if (cj === quote) { + break; + } + j += 1; + } + if (j >= source.length) break; + const value = source.slice(i + 1, j); + if (!(quote === '`' && value.includes('${'))) values.push(value); + i = j + 1; + } + return values; +} + +/** + + * extractRegisteredCommands executes its scoped Runewager logic and participates in menu/command or utility flow composition. + + * Parameters: See the function signature for exact argument names and accepted values. + + * Returns: Returns the computed value or a Promise resolving to the operation result; may return void for side-effect handlers. + + * Side effects: May mutate runtime stores, pendingAction state, menu state, persistence files, logs, and callback progression. + + * Validation/safety: Uses existing guard utilities (admin checks, input checks, path checks, cooldown checks) where applicable. + + * Timeouts/fallbacks: Timeout and fallback behavior are controlled by the calling flow and global handler/state machine conventions. + + * Errors: Surfaces user-facing error replies and/or logs when inputs, permissions, or dependencies are invalid. + + * System fit: This function is part of the Runewager command/callback/state orchestration pipeline. + + */ + +function extractRegisteredCommands(source) { + const marker = 'REGISTERED_COMMANDS'; + const idx = source.indexOf(marker); + if (idx === -1) return null; + + // Allow formatting variants (new Set([...]) or plain array/object assignment) by scanning for the first array literal after the marker. + const arrOpen = source.indexOf('[', idx); + if (arrOpen === -1) return null; + + let depth = 0; + let inString = null; + let escaped = false; + let arrClose = -1; + + for (let i = arrOpen; i < source.length; i += 1) { + const ch = source[i]; + if (inString) { + if (escaped) { + escaped = false; + } else if (ch === '\\') { + escaped = true; + } else if (ch === inString) { + inString = null; + } + continue; + } + if (ch === '"' || ch === "'" || ch === '`') { + inString = ch; + continue; + } + if (ch === '[') depth += 1; + if (ch === ']') { + depth -= 1; + if (depth === 0) { + arrClose = i; + break; + } + } + } + + if (arrClose === -1) return null; + const arrayBody = source.slice(arrOpen + 1, arrClose); + return new Set(parseStringLiterals(arrayBody)); +} test('index.js syntax is valid', () => { - const result = spawnSync(process.execPath, ['--check', 'index.js'], { encoding: 'utf8' }); + const rootDir = path.resolve(__dirname, '..'); + const result = spawnSync(process.execPath, ['--check', path.join(rootDir, 'index.js')], { encoding: 'utf8' }); assert.equal(result.status, 0, result.stderr || result.stdout); }); + +test('menu callback buttons map to handlers (literal or dynamic patterns)', () => { + const rootDir = path.resolve(__dirname, '..'); + const jsFiles = collectJsFiles(rootDir); + const combinedSource = jsFiles + .map((file) => fs.readFileSync(file, 'utf8')) + .join('\n\n'); + + const callbackButtons = extractLiteralIds(combinedSource, 'callback'); + const literalActionHandlers = extractLiteralIds(combinedSource, 'action'); + const dynamicActionPatterns = extractActionRegexPatterns(combinedSource); + + const uncovered = Array.from(callbackButtons) + .filter((id) => !literalActionHandlers.has(id) && !dynamicActionPatterns.some((pattern) => pattern.test(id))) + .sort(); + + assert.deepEqual(uncovered, [], `Uncovered callback handlers: ${uncovered.join(', ')}`); +}); + +test('REGISTERED_COMMANDS stays in sync with command handlers', () => { + const source = fs.readFileSync(path.resolve(__dirname, '..', 'index.js'), 'utf8'); + const registered = extractRegisteredCommands(source); + assert.ok(registered, 'REGISTERED_COMMANDS block not found'); + + const commandHandlers = new Set(Array.from(source.matchAll(/bot\.command\(\s*(["'`])((?:\\.|(?!\1).)*)\1/g), (m) => m[2])); + const allowedRegisteredOnly = new Set(['start']); // handled by bot.start(...), not bot.command('start') + + const missingInRegistered = Array.from(commandHandlers).filter((cmd) => !registered.has(cmd)).sort(); + const extraInRegistered = Array.from(registered).filter((cmd) => !commandHandlers.has(cmd) && !allowedRegisteredOnly.has(cmd)).sort(); + + assert.deepEqual(missingInRegistered, [], `Commands missing from REGISTERED_COMMANDS: ${missingInRegistered.join(', ')}`); + assert.deepEqual(extraInRegistered, [], `REGISTERED_COMMANDS entries without handlers: ${extraInRegistered.join(', ')}`); +}); diff --git a/test/unit.test.js b/test/unit.test.js index f6ea71f..51e5c9c 100644 --- a/test/unit.test.js +++ b/test/unit.test.js @@ -23,28 +23,148 @@ const ALLOWED_BONUS_STATUS_TRANSITIONS = { const WAGER_BONUS_IN_FLIGHT_STATUSES = new Set(['pending', 'approved']); const WAGER_BONUS_NON_REQUESTABLE_STATUSES = new Set(['bonus_sent', ...WAGER_BONUS_IN_FLIGHT_STATUSES]); +/** + + * isValidBonusTransition executes its scoped Runewager logic and participates in menu/command or utility flow composition. + + * Parameters: See the function signature for exact argument names and accepted values. + + * Returns: Returns the computed value or a Promise resolving to the operation result; may return void for side-effect handlers. + + * Side effects: May mutate runtime stores, pendingAction state, menu state, persistence files, logs, and callback progression. + + * Validation/safety: Uses existing guard utilities (admin checks, input checks, path checks, cooldown checks) where applicable. + + * Timeouts/fallbacks: Timeout and fallback behavior are controlled by the calling flow and global handler/state machine conventions. + + * Errors: Surfaces user-facing error replies and/or logs when inputs, permissions, or dependencies are invalid. + + * System fit: This function is part of the Runewager command/callback/state orchestration pipeline. + + */ + function isValidBonusTransition(fromStatus, toStatus) { if (fromStatus === toStatus) return true; const allowed = ALLOWED_BONUS_STATUS_TRANSITIONS[fromStatus] || new Set(); return allowed.has(toStatus); } +/** + + * isWagerBonusRequestLocked executes its scoped Runewager logic and participates in menu/command or utility flow composition. + + * Parameters: See the function signature for exact argument names and accepted values. + + * Returns: Returns the computed value or a Promise resolving to the operation result; may return void for side-effect handlers. + + * Side effects: May mutate runtime stores, pendingAction state, menu state, persistence files, logs, and callback progression. + + * Validation/safety: Uses existing guard utilities (admin checks, input checks, path checks, cooldown checks) where applicable. + + * Timeouts/fallbacks: Timeout and fallback behavior are controlled by the calling flow and global handler/state machine conventions. + + * Errors: Surfaces user-facing error replies and/or logs when inputs, permissions, or dependencies are invalid. + + * System fit: This function is part of the Runewager command/callback/state orchestration pipeline. + + */ + function isWagerBonusRequestLocked(status) { return WAGER_BONUS_NON_REQUESTABLE_STATUSES.has(status); } +/** + + * escapeMarkdownV2 executes its scoped Runewager logic and participates in menu/command or utility flow composition. + + * Parameters: See the function signature for exact argument names and accepted values. + + * Returns: Returns the computed value or a Promise resolving to the operation result; may return void for side-effect handlers. + + * Side effects: May mutate runtime stores, pendingAction state, menu state, persistence files, logs, and callback progression. + + * Validation/safety: Uses existing guard utilities (admin checks, input checks, path checks, cooldown checks) where applicable. + + * Timeouts/fallbacks: Timeout and fallback behavior are controlled by the calling flow and global handler/state machine conventions. + + * Errors: Surfaces user-facing error replies and/or logs when inputs, permissions, or dependencies are invalid. + + * System fit: This function is part of the Runewager command/callback/state orchestration pipeline. + + */ + function escapeMarkdownV2(text) { return String(text).replace(/[_*`[\]]/g, '\\$&'); } +/** + + * escapeMarkdownFull executes its scoped Runewager logic and participates in menu/command or utility flow composition. + + * Parameters: See the function signature for exact argument names and accepted values. + + * Returns: Returns the computed value or a Promise resolving to the operation result; may return void for side-effect handlers. + + * Side effects: May mutate runtime stores, pendingAction state, menu state, persistence files, logs, and callback progression. + + * Validation/safety: Uses existing guard utilities (admin checks, input checks, path checks, cooldown checks) where applicable. + + * Timeouts/fallbacks: Timeout and fallback behavior are controlled by the calling flow and global handler/state machine conventions. + + * Errors: Surfaces user-facing error replies and/or logs when inputs, permissions, or dependencies are invalid. + + * System fit: This function is part of the Runewager command/callback/state orchestration pipeline. + + */ + function escapeMarkdownFull(text) { return String(text).replace(/[_*[\]()~`>#+\-=|{}.!\\]/g, '\\$&'); } +/** + + * normalizeRunewagerUsername executes its scoped Runewager logic and participates in menu/command or utility flow composition. + + * Parameters: See the function signature for exact argument names and accepted values. + + * Returns: Returns the computed value or a Promise resolving to the operation result; may return void for side-effect handlers. + + * Side effects: May mutate runtime stores, pendingAction state, menu state, persistence files, logs, and callback progression. + + * Validation/safety: Uses existing guard utilities (admin checks, input checks, path checks, cooldown checks) where applicable. + + * Timeouts/fallbacks: Timeout and fallback behavior are controlled by the calling flow and global handler/state machine conventions. + + * Errors: Surfaces user-facing error replies and/or logs when inputs, permissions, or dependencies are invalid. + + * System fit: This function is part of the Runewager command/callback/state orchestration pipeline. + + */ + function normalizeRunewagerUsername(raw) { return String(raw || '').trim().toLowerCase().replace(/^@/, ''); } +/** + + * getNextOnboardingStep executes its scoped Runewager logic and participates in menu/command or utility flow composition. + + * Parameters: See the function signature for exact argument names and accepted values. + + * Returns: Returns the computed value or a Promise resolving to the operation result; may return void for side-effect handlers. + + * Side effects: May mutate runtime stores, pendingAction state, menu state, persistence files, logs, and callback progression. + + * Validation/safety: Uses existing guard utilities (admin checks, input checks, path checks, cooldown checks) where applicable. + + * Timeouts/fallbacks: Timeout and fallback behavior are controlled by the calling flow and global handler/state machine conventions. + + * Errors: Surfaces user-facing error replies and/or logs when inputs, permissions, or dependencies are invalid. + + * System fit: This function is part of the Runewager command/callback/state orchestration pipeline. + + */ + function getNextOnboardingStep(user) { if (!user.ageConfirmed) return 0; if (!user.verified) return 1;