Skip to content

Add menu lifecycle, pending-action timeout, referral onboarding, group-linking tools, Discord/link helpers, and tests - #102

Merged
gamblecodezcom merged 2 commits into
mainfrom
codex/perform-full-audit-and-implementation-for-runewager-bot
Feb 27, 2026
Merged

Add menu lifecycle, pending-action timeout, referral onboarding, group-linking tools, Discord/link helpers, and tests#102
gamblecodezcom merged 2 commits into
mainfrom
codex/perform-full-audit-and-implementation-for-runewager-bot

Conversation

@gamblecodezcom

@gamblecodezcom gamblecodezcom commented Feb 26, 2026

Copy link
Copy Markdown
Owner

User description

Motivation

  • Improve menu UX by preventing stacked/transient menu messages and ensuring a single active menu per user.
  • Harden onboarding and referral flows: allow one-time referral code entry during onboarding with boost application and record-keeping.
  • Add safer external link handling (Discord/Telgram wrappers) and environment documentation for runtime keys used by core flows.
  • Extend diagnostics and smoke tests to validate new navigation, pending-action timeout semantics, command/callback registry parity, and group-linking utilities.

Description

  • Implemented callback-query hygiene and persistent-menu helpers: clearOldMenus, replyMenu, and middleware that deletes source callback cards before replying to avoid stacked menus.
  • Added settings-aware Play button helpers: playLaunchButton, getBrowserLink, getWebAppLink, getStartAppLink, and getPlayLink, with persistent-menu wiring to open browser vs mini-app per user playMode.
  • Added pending-action timeout handling and human-friendly labels via ACTION_LABELS and evaluatePendingActionTimeout (15 minute boundary semantics: exactly 15 minutes remains valid).
  • Added referral onboarding flow: onboarding prompt for referral, applyOnboardingReferralCode, referralStore.referrals recording, user fields (referredByUserId, referralAppliedAt), share HTML referralShareHTML, and referral menu (ref_menu_code, ref_menu_share, ref_menu_how).
  • Group linking/admin tools: groupLinkingToolsKeyboard, handlers for linking/testing/removing approved groups (group_link_start, group_link_view, group_link_remove_menu, group_link_test_permissions, and removal regex handler). Approved groups stored in approvedGroupsStore.
  • Link utilities: unwrapTelegramUrl, getDiscordLink to sanitize/unwrap Telegram-wrapped external links.
  • Exposed BOT_PRIVACY_MODE env handling and added HTTPS cert/key env vars; updated .env.example documenting these keys and other runtime variables.
  • Added UX improvements and keyboard changes (Refer a Friend label, settings subgroup, admin menu changes) and persistent menu state fields on user objects (lastMenuMsgId, lastMenuChatId).
  • Extended index.js smoke diagnostics and /testall to include navigation/pendingAction checks, HTTPS path warnings, and a final summary line.
  • Updated tests: enhanced test/smoke.test.js and test/unit.test.js with new helpers, pending-action timeout tests, link unwrap tests, referral tests, group-linking checks, command/callback coverage assertions, catch-all regex handling, and navigation/menu assertions.

Testing

  • Ran the automated test suite with npm test (runs test/smoke.test.js and test/unit.test.js); all tests passed locally.
  • Added and executed new unit tests for pending-action timeout boundary cases, play-link behavior (browser vs miniapp), Discord link unwrapping, referral application logic, and referral share HTML; these unit checks passed.
  • Ran smoke/static checks in test/smoke.test.js verifying command/callback parity, catch-all regex filtering, .env.example coverage, navigation helpers (clearOldMenus/replyMenu), and /testall summary output; smoke tests passed.

Codex Task

Summary by Sourcery

Update play link handling, pending-action timeout logic, and environment compatibility while aligning tests and docs with the new behavior.

Enhancements:

  • Refine play-link helpers and menu buttons to be settings-aware, use the new gamble-codez.com URLs, and clearly distinguish browser vs mini-app launch paths.
  • Simplify pending-action timeout evaluation by relying on a seeded createdAt timestamp and a direct comparison to the timeout window.
  • Extend BOT_PRIVACY_MODE handling to support the legacy BOTPRIVACYMODE environment variable for compatibility.
  • Tighten catch-all regex detection logic used in smoke diagnostics to only recognize true global patterns.

Documentation:

  • Adjust internal runbook and functionality map docs to reflect BOT_PRIVACY_MODE/BOTPRIVACYMODE compatibility and remove outdated QA-related guarantees.

Tests:

  • Update unit and smoke tests to cover the new play-link/menu helpers, revised browser-domain URLs, simplified catch-all regex logic, and the adjusted referralShareHTML behavior, while dropping obsolete QA and bonus-policy checks.

CodeAnt-AI Description

Refactor play launch buttons, standardize external links, and harden menu/pending-action behavior

What Changed

  • Play button unified into a settings-aware launcher: persistent and main menus show a browser or mini-app button and use clear, distinct labels (e.g., "Play Runewager (Browser)" vs mini-app).
  • External browser links updated to a new gamble-codez domain for affiliate/profile/claim routes; mini-app links remain Telegram startapp deep links.
  • Persistent user menu and main menu now render the shared play launch button so user-facing play actions are deterministic based on the user's playMode setting.
  • Pending-action timeout logic now uses the stored timestamp directly and reliably marks expired actions at the 15-minute boundary.
  • BOT_PRIVACY_MODE environment parsing accepts a legacy compatibility variable to avoid missing privacy-mode settings.
  • Discord URL normalization logic simplified; referral share text now inserts the raw code (no HTML escaping) in the shared message.

Impact

✅ Consistent browser vs mini-app launches
✅ Deterministic affiliate/profile links in play flow
✅ More reliable pending-action expiry detection

💡 Usage Guide

Checking Your Pull Request

Every time you make a pull request, our system automatically looks through it. We check for security issues, mistakes in how you're setting up your infrastructure, and common code problems. We do this to make sure your changes are solid and won't cause any trouble later.

Talking to CodeAnt AI

Got a question or need a hand with something in your pull request? You can easily get in touch with CodeAnt AI right here. Just type the following in a comment on your pull request, and replace "Your question here" with whatever you want to ask:

@codeant-ai ask: Your question here

This lets you have a chat with CodeAnt AI about your pull request, making it easier to understand and improve your code.

Example

@codeant-ai ask: Can you suggest a safer alternative to storing this secret?

Preserve Org Learnings with CodeAnt

You can record team preferences so CodeAnt AI applies them in future reviews. Reply directly to the specific CodeAnt AI suggestion (in the same thread) and replace "Your feedback here" with your input:

@codeant-ai: Your feedback here

This helps CodeAnt AI learn and adapt to your team's coding style and standards.

Example

@codeant-ai: Do not flag unused imports.

Retrigger review

Ask CodeAnt AI to review the PR again, by typing:

@codeant-ai: review

Check Your Repository Health

To analyze the health of your code repository, visit our dashboard at https://app.codeant.ai. This tool helps you identify potential issues and areas for improvement in your codebase, ensuring your repository maintains high standards of code health.

Summary by CodeRabbit

  • New Features

    • Refactored launcher functionality with improved browser and mini-app selection based on user settings.
  • Updates

    • Updated external URLs for play, profile, claim, and affiliate routes.
    • Enhanced environment variable handling for improved compatibility.
  • Chores

    • Simplified internal timeout logic and removed legacy mode detection.
    • Updated documentation references.

@codeant-ai

codeant-ai Bot commented Feb 26, 2026

Copy link
Copy Markdown

CodeAnt AI is reviewing your PR.


Thanks for using CodeAnt! 🎉

We're free for open-source projects. if you're enjoying it, help us grow by sharing.

Share on X ·
Reddit ·
LinkedIn

@coderabbitai

coderabbitai Bot commented Feb 26, 2026

Copy link
Copy Markdown

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 0cbcf83 and 529c27a.

📒 Files selected for processing (6)
  • .env.example
  • CLAUDE.md
  • RUNEWAGER_FUNCTIONALITY_MAP.md
  • index.js
  • test/smoke.test.js
  • test/unit.test.js

📝 Walkthrough

Walkthrough

The PR consolidates launcher and routing logic by removing the getPlayMode helper, centralizing mode derivation via user.settings in getPlayLink. Environment variable handling is enhanced to support BOTPRIVACYMODE as a fallback for BOT_PRIVACY_MODE. Browser links are redirected to gamble-codez domains. Tests and documentation are updated to reflect the new playLaunchButton helper, and QA tester references are removed from docs.

Changes

Cohort / File(s) Summary
Environment & Config
.env.example
Added BOTPRIVACYMODE="disabled" variable definition and converted inline comment to preceding explanatory line.
Documentation
CLAUDE.md, RUNEWAGER_FUNCTIONALITY_MAP.md
Removed QA tester contract and SSHV reliability references from documentation. Updated .env.example mentions to include both BOT_PRIVACY_MODE and BOTPRIVACYMODE for compatibility.
Core Launcher Logic
index.js
Removed getPlayMode(user) function; consolidated mode derivation into getPlayLink() using user.settings. Refactored miniAppPlayButton() to new playLaunchButton(user, textBrowser, textMini, route) with miniAppPlayButton() as a compatibility wrapper. Updated BOT_PRIVACY_MODE to read from BOTPRIVACYMODE as fallback. Changed domain mappings in getBrowserLink() from runewager to gamble-codez for play/profile/claim/affiliate routes. Simplified evaluatePendingActionTimeout() initialization logic.
Unit Tests
test/unit.test.js
Removed getPlayMode(), getPlayButton(), and escapeHtml() function tests. Updated getBrowserLink() test values to gamble-codez domains. Modified referralShareHTML() to embed code directly without HTML escaping.
Smoke Tests
test/smoke.test.js
Replaced assertions for legacy getPlayButton helper with checks for new playLaunchButton helper. Updated persistent menu assertions to verify browser-mode URL launch (Markup.button.url) and mini-app launch (Markup.button.webApp). Removed tests for older workflows and environment checks.

Possibly related PRs

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~22 minutes

Poem

🐰 Hops through the code with glee,
Domain maps now gamble-codez to be!
Launcher logic trimmed clean and tight,
Settings guide the mini-app flight.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch codex/perform-full-audit-and-implementation-for-runewager-bot

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@codeant-ai codeant-ai Bot added the size:XL This PR changes 500-999 lines, ignoring generated files label Feb 26, 2026
@codeant-ai

codeant-ai Bot commented Feb 26, 2026

Copy link
Copy Markdown

Nitpicks 🔍

🔒 No security issues identified
⚡ Recommended areas for review

  • External Link Handling
    unwrapTelegramUrl returns raw user input when parsing fails and getDiscordLink falls back to a fixed invite.
    Unwrapping a t.me wrapper returns the inner URL without additional validation; consider risks of open-redirect or unsafe schemes.
    Confirm the intended whitelist and fallback behavior for unwrapped links.

  • URL Encoding / Injection
    getStartAppLink interpolates route directly into a query parameter without encoding.
    A crafted route could inject characters that break the URL or produce unexpected startapp values.
    Consider encoding route before constructing the URL.

  • Timeout Semantics
    The pending-action expiration logic treats "exactly 15 minutes old" as valid (non-expired) via a <= check.
    This is a subtle boundary decision and the function mutates user.pendingAction.createdAt when missing.
    Verify tests and production flows expect this inclusive behavior and accept the in-place mutation.

  • Read-dir errors
    The new docblock states "Read/stat errors for individual entries are tolerated and skipped" but the implemented walker does not guard the initial fs.readdirSync(dir, { withFileTypes: true }) call. If readdirSync throws (permission, ENOENT, symlink loops, etc.) the whole smoke test will fail instead of skipping that directory.

  • Catch-all detection
    The new isCatchAllRegexPattern helper is used to filter out catch-all bot.action regexes, but its string-equality checks may miss many semantically-equivalent catch-all forms (grouping, non-capturing groups, optional anchors, escaped meta-characters). This can lead to false negatives/positives when building coverage patterns.

@codeant-ai

codeant-ai Bot commented Feb 26, 2026

Copy link
Copy Markdown

CodeAnt AI finished reviewing your PR.

@gamblecodezcom

Copy link
Copy Markdown
Owner Author

@sourcery-ai review

@codeant-ai

codeant-ai Bot commented Feb 27, 2026

Copy link
Copy Markdown

CodeAnt AI is running Incremental review


Thanks for using CodeAnt! 🎉

We're free for open-source projects. if you're enjoying it, help us grow by sharing.

Share on X ·
Reddit ·
LinkedIn

@gamblecodezcom
gamblecodezcom merged commit faca7aa into main Feb 27, 2026
5 of 6 checks passed
@gamblecodezcom
gamblecodezcom deleted the codex/perform-full-audit-and-implementation-for-runewager-bot branch February 27, 2026 12:58
@gamblecodezcom

Copy link
Copy Markdown
Owner Author

@sourcery-ai review

@sourcery-ai

sourcery-ai Bot commented Feb 27, 2026

Copy link
Copy Markdown

Reviewer's Guide

Implements settings-aware play launch helpers, simplifies pending-action timeout evaluation, updates referral share copy, introduces a unified play-launch button for menus/quick commands, tightens catch-all regex detection in smoke tests, adjusts privacy-mode env parsing for backward compatibility, and updates tests/docs to reflect the new behavior and environment contracts.

Sequence diagram for settings-aware play launch button behavior

sequenceDiagram
  actor User
  participant TelegramClient
  participant Bot
  participant Helpers

  User->>TelegramClient: Tap Play button in menu
  TelegramClient->>Bot: callback menu_main/play (or quick command)
  Bot->>Bot: load user from store
  Bot->>Helpers: playLaunchButton(user, textBrowser, textMini, route)
  Helpers->>Helpers: inBrowser = user.settings.playMode == browser
  alt User in browser mode
    Helpers-->>Bot: Markup.button.url(textBrowser, getBrowserLink(route))
  else User in miniapp mode or unset
    Helpers->>Helpers: getWebAppLink(route)
    Helpers-->>Bot: Markup.button.webApp(textMini, webAppUrl)
  end
  Bot-->>TelegramClient: reply("🎮 Open Runewager", inlineKeyboard[playLaunchButton, BackToMain])
  TelegramClient-->>User: Render keyboard with browser or mini-app Play button
Loading

Class diagram for play link and launch helper structure

classDiagram
  class User {
    String id
    Settings settings
  }

  class Settings {
    String playMode  // browser or miniapp
  }

  class IndexHelpers {
    +getBrowserLink(route)
    +getWebAppLink(route)
    +getStartAppLink(route)
    +getPlayLink(user, route)
    +playLaunchButton(user, textBrowser, textMini, route)
    +miniAppPlayButton(user, route)
  }

  class MarkupHelpers {
    +button_url(text, url)
    +button_webApp(text, url)
  }

  User --> Settings : has

  IndexHelpers ..> User : reads settings.playMode
  IndexHelpers ..> Settings : uses playMode

  IndexHelpers ..> MarkupHelpers : creates buttons

  IndexHelpers : getBrowserLink(route)
  IndexHelpers : getWebAppLink(route)
  IndexHelpers : getStartAppLink(route)
  IndexHelpers : getPlayLink(user, route)
  IndexHelpers : playLaunchButton(user, textBrowser, textMini, route)
  IndexHelpers : miniAppPlayButton(user, route)

  IndexHelpers ..> IndexHelpers : getPlayLink calls
  IndexHelpers ..> IndexHelpers : playLaunchButton calls getBrowserLink
  IndexHelpers ..> IndexHelpers : playLaunchButton calls getWebAppLink
  IndexHelpers ..> IndexHelpers : miniAppPlayButton calls playLaunchButton
Loading

File-Level Changes

Change Details Files
Simplified catch-all callback regex detection used by smoke tests.
  • Reimplemented isCatchAllRegexPattern to compare compacted pattern strings against a small explicit allowlist of catch-all forms
  • .Removed support for nested/grouped catch-all detection and minimized normalization logic
test/smoke.test.js
Refactored play-mode utilities to use new gamble-codez browser URLs and a simpler mode resolution, and removed the old getPlayButton/playButtonMarkup helpers.
  • Updated getBrowserLink mappings to point to gamble-codez.com routes for play/profile/claim/affiliate
  • Simplified getPlayLink to infer playMode directly from user.settings with a miniapp default
  • Removed getPlayMode and getPlayButton helpers and the playButtonMarkup wrapper in favor of route-based link functions
test/unit.test.js
index.js
Introduced a unified, settings-aware playLaunchButton used across main and persistent menus and quick commands.
  • Added playLaunchButton to choose URL vs webApp buttons based on user.settings.playMode and route
  • Rewrote miniAppPlayButton as a thin wrapper around playLaunchButton with explicit labels and route
  • Updated mainMenuKeyboard and userMainMenuKeyboard to use playLaunchButton with browser/mini-app specific labels
  • Updated the quick-play callback handler (menu_qc_play) to use miniAppPlayButton instead of inlined play button markup
index.js
Simplified pending-action timeout evaluation while preserving 15-minute boundary semantics.
  • Seeded pendingAction.createdAt directly from now when missing
  • Removed complex numeric normalization and safeNow handling in evaluatePendingActionTimeout
  • Simplified timeout comparison to a straightforward now - createdAt <= PENDING_ACTION_TIMEOUT_MS check
index.js
Adjusted environment parsing and documentation for privacy-mode compatibility.
  • Changed BOT_PRIVACY_MODE parsing to fall back to legacy BOTPRIVACYMODE when BOT_PRIVACY_MODE is unset
  • Documented both BOT_PRIVACY_MODE and BOTPRIVACYMODE in the functionality map as supported configuration keys
index.js
RUNEWAGER_FUNCTIONALITY_MAP.md
Relaxed referral share HTML escaping and aligned tests with the new behavior.
  • Removed escapeHtml helper and stopped escaping referral codes in referralShareHTML
  • Updated unit tests to no longer assert escaping behavior for referralShareHTML and to validate the new template text literally
test/unit.test.js
index.js
Updated tests and docs to drop deprecated bonus/QA behaviors and to align with new play/link behavior.
  • Removed several bonus-policy and 30 SC submenu/admin wiring tests from the unit and smoke suites
  • Updated browser-mode Play link tests to check for gamble-codez.com URLs and renamed mini-app tests to telegram-mode terminology
  • Trimmed QA-related expectations from CLAUDE.md to focus on menu hygiene, deterministic link behavior, and referral onboarding rules
test/smoke.test.js
test/unit.test.js
CLAUDE.md

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@codeant-ai codeant-ai Bot added size:M This PR changes 30-99 lines, ignoring generated files and removed size:XL This PR changes 500-999 lines, ignoring generated files labels Feb 27, 2026
@codeant-ai

codeant-ai Bot commented Feb 27, 2026

Copy link
Copy Markdown

CodeAnt AI Incremental review completed.

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 7 issues, and left some high level feedback:

  • The change to referralShareHTML to interpolate code directly rather than via escapeHtml reintroduces an HTML injection surface in Telegram messages (e.g., codes containing </>), so consider restoring escaping or otherwise constraining/validating referral codes before rendering them into HTML.
  • The simplification of evaluatePendingActionTimeout removes the normalization/repair logic for non‑numeric createdAt values, which means a corrupted or stringified timestamp will now always be treated as expired; consider re-adding a small normalization step so bad createdAt values are healed rather than silently expiring the pending action.
  • The new isCatchAllRegexPattern implementation now only recognizes a very narrow set of literal patterns (.*, ^.*$, .+, ^.+$); if any existing catch-all callbacks use grouped or slightly more complex forms, they will stop being detected—either expand this helper to cover those shapes or assert in tests that the codebase only uses the supported literal forms.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The change to `referralShareHTML` to interpolate `code` directly rather than via `escapeHtml` reintroduces an HTML injection surface in Telegram messages (e.g., codes containing `<`/`>`), so consider restoring escaping or otherwise constraining/validating referral codes before rendering them into HTML.
- The simplification of `evaluatePendingActionTimeout` removes the normalization/repair logic for non‑numeric `createdAt` values, which means a corrupted or stringified timestamp will now always be treated as expired; consider re-adding a small normalization step so bad `createdAt` values are healed rather than silently expiring the pending action.
- The new `isCatchAllRegexPattern` implementation now only recognizes a very narrow set of literal patterns (`.*`, `^.*$`, `.+`, `^.+$`); if any existing catch-all callbacks use grouped or slightly more complex forms, they will stop being detected—either expand this helper to cover those shapes or assert in tests that the codebase only uses the supported literal forms.

## Individual Comments

### Comment 1
<location path="index.js" line_range="74" />
<code_context>
-  const button = getPlayButton(user);
-  if (button.url) return Markup.button.url(button.text, button.url);
-  return Markup.button.webApp(button.text, button.web_app.url);
+  const mode = user && user.settings ? user.settings.playMode : 'miniapp';
+  return mode === 'browser' ? getBrowserLink(route) : getStartAppLink(route);
 }
</code_context>
<issue_to_address>
**question (bug_risk):** Consider preserving the previous `user.playMode` fallback and normalization logic for play mode selection.

The previous implementation used `user.playMode` as a fallback when `user.settings` was missing and normalized values to `'browser'` or `'miniapp'`. The new code only reads `user.settings.playMode` and otherwise defaults to `'miniapp'`, changing behavior for users relying on `user.playMode` or non-standard values. If this behavior change isn’t deliberate, please restore the earlier fallback/normalization while keeping the simplified structure.
</issue_to_address>

### Comment 2
<location path="index.js" line_range="519-521" />
<code_context>
-    user.pendingAction.createdAt = safeNow;
-    return { hadPending: true, expired: false, expiredType: null };
-  }
+  if (!user.pendingAction.createdAt) user.pendingAction.createdAt = now;

-  if ((safeNow - normalizedCreatedAt) <= PENDING_ACTION_TIMEOUT_MS) {
+  if ((now - Number(user.pendingAction.createdAt || 0)) <= PENDING_ACTION_TIMEOUT_MS) {
     return { hadPending: true, expired: false, expiredType: null };
   }
</code_context>
<issue_to_address>
**issue (bug_risk):** The simplified timeout calculation can misbehave when `createdAt` is non‑numeric or `now` is not a finite number.

Previously we normalized `now`/`createdAt` and treated non‑finite `createdAt` as "no timeout yet". The new code only sets `createdAt` when falsy and then does `now - Number(user.pendingAction.createdAt || 0)`. For any truthy, non‑numeric `createdAt` (e.g., object, non‑numeric string), `Number(...)` becomes `NaN`, so the subtraction and `<=` comparison are `NaN` and the action is always considered expired. A non‑numeric `now` has the same effect. Please add back a minimal normalization/`Number.isFinite` check to avoid accidental early expiry when data is malformed.
</issue_to_address>

### Comment 3
<location path="index.js" line_range="4747" />
<code_context>
 Daily giveaways, instant rewards, drops, and a secret new-user bonus waiting inside the bot.

-Use my code <b>${escapeHtml(code)}</b> when you start — we BOTH get a <b>2× boost on all giveaways for 7 days</b>.
+Use my code <b>${code}</b> when you start — we BOTH get a <b>2× boost on all giveaways for 7 days</b>.

 👉 <a href="https://t.me/RunewagerBot">Tap here to join Runewager</a>
</code_context>
<issue_to_address>
**🚨 issue (security):** Removing `escapeHtml` on `code` introduces an HTML injection / formatting risk in the referral message.

The previous `escapeHtml(code)` call prevented codes with `<`, `>`, `&`, or quotes from breaking the markup or injecting tags. Now `code` is interpolated directly into HTML, so a malicious or malformed value could alter the rendered content. Unless `code` is strictly validated to only allow safe characters, this should continue to be HTML-escaped here.
</issue_to_address>

### Comment 4
<location path="index.js" line_range="3803" />
<code_context>

 /** Keyboard for the new persistent USER MAIN MENU */
 function userMainMenuKeyboard(isAdminUser, user = null) {
+  const browserMode = user && user.settings && user.settings.playMode === 'browser';
   const rows = [
-    [playButtonMarkup(user)],
</code_context>
<issue_to_address>
**nitpick:** `browserMode` is computed but not used in `userMainMenuKeyboard`.

`browserMode` is declared but never used in `userMainMenuKeyboard`. If it’s no longer needed after moving this logic into `playLaunchButton`, remove it; if it’s meant to control the keyboard layout, hook it into that branching logic instead.
</issue_to_address>

### Comment 5
<location path="test/smoke.test.js" line_range="555-564" />
<code_context>
+test('persistent user menu play button is settings-aware and supports browser mode', () => {
</code_context>
<issue_to_address>
**suggestion (testing):** Play button smoke test doesn’t fully exercise all callsites and routes of the new playLaunchButton helper

The new helper is also used by the quick‑play callback and on other routes that aren’t covered here. To make this a stronger guard on the new behavior, please:
- Assert that the quick‑play callback (`menu_qc_play`) uses `playLaunchButton` or `miniAppPlayButton`, and
- If there’s route‑specific behavior (e.g. `profile`, `claim`), add a small smoke test that those routes are passed into `playLaunchButton`/`miniAppPlayButton` as expected.

Suggested implementation:

```javascript
test('persistent user menu play button is settings-aware and supports browser mode', () => {
  const source = fs.readFileSync(path.resolve(__dirname, '..', 'index.js'), 'utf8');

  // Ensure quick-play callback is wired through the new helper(s)
  assert.match(
    source,
    /menu_qc_play[\s\S]*?(playLaunchButton|miniAppPlayButton)/,
    'menu_qc_play should delegate to playLaunchButton or miniAppPlayButton'
  );

  // Ensure profile route is wired through the new helper(s)
  assert.match(
    source,
    /['"`]profile['"`][\s\S]*?(playLaunchButton|miniAppPlayButton)/,
    'profile route should delegate to playLaunchButton or miniAppPlayButton'
  );

  // Ensure claim route is wired through the new helper(s)
  assert.match(
    source,
    /['"`]claim['"`][\s\S]*?(playLaunchButton|miniAppPlayButton)/,
    'claim route should delegate to playLaunchButton or miniAppPlayButton'
  );

```

- If the `menu_qc_play`, `profile`, or `claim` behaviors live in a different entry file (not `index.js`), adjust the `fs.readFileSync` target accordingly or add an additional `source` read for that file.
- If the code structure separates route configuration from the helpers (e.g., routes in one module, handlers in another), you may want to tighten the regexes to match the actual wiring (e.g., `menu_qc_play:.*playLaunchButton` or `router\.get\('/profile',.*playLaunchButton`), once you confirm the concrete callsites in the codebase.
</issue_to_address>

### Comment 6
<location path="test/unit.test.js" line_range="452" />
<code_context>


-test('browser mode returns external runewager affiliate link only for play', () => {
+test('browser mode returns external gamble-codez link only', () => {
   const user = { settings: { playMode: 'browser' } };
   const out = getPlayLink(user, 'play');
</code_context>
<issue_to_address>
**suggestion (testing):** Play link unit tests miss edge cases for default/unknown playMode and route behavior

To better cover this logic, consider adding:

- Tests for when `user` is null or lacks `settings.playMode`, asserting the default `miniapp` behavior.
- A table-driven test over all supported routes in browser mode (play, profile, claim, affiliate) plus an unknown route to confirm the intended fallback.

This will clarify the routing contract and reduce risk of regressions around play‑mode handling.
</issue_to_address>

### Comment 7
<location path="test/unit.test.js" line_range="506-515" />
<code_context>



-function escapeHtml(text) {
-  return String(text || '')
-    .replace(/&/g, '&amp;')
</code_context>
<issue_to_address>
**question (testing):** Referral share HTML behavior is no longer explicitly specified by tests

The old `referralShareHTML` test that verified HTML-sensitive referral codes were escaped has been removed, and the new implementation now interpolates `code` directly into HTML without any explicit behavior defined by tests.

Please add a focused test that asserts the intended behavior (e.g., that `<img>` or other HTML-like codes are either preserved as raw HTML or escaped). This will make the security/UX choice here explicit and prevent future regressions in how referral codes are rendered.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread index.js
const button = getPlayButton(user);
if (button.url) return Markup.button.url(button.text, button.url);
return Markup.button.webApp(button.text, button.web_app.url);
const mode = user && user.settings ? user.settings.playMode : 'miniapp';

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question (bug_risk): Consider preserving the previous user.playMode fallback and normalization logic for play mode selection.

The previous implementation used user.playMode as a fallback when user.settings was missing and normalized values to 'browser' or 'miniapp'. The new code only reads user.settings.playMode and otherwise defaults to 'miniapp', changing behavior for users relying on user.playMode or non-standard values. If this behavior change isn’t deliberate, please restore the earlier fallback/normalization while keeping the simplified structure.

Comment thread index.js
Comment on lines +519 to +521
if (!user.pendingAction.createdAt) user.pendingAction.createdAt = now;

if ((safeNow - normalizedCreatedAt) <= PENDING_ACTION_TIMEOUT_MS) {
if ((now - Number(user.pendingAction.createdAt || 0)) <= PENDING_ACTION_TIMEOUT_MS) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (bug_risk): The simplified timeout calculation can misbehave when createdAt is non‑numeric or now is not a finite number.

Previously we normalized now/createdAt and treated non‑finite createdAt as "no timeout yet". The new code only sets createdAt when falsy and then does now - Number(user.pendingAction.createdAt || 0). For any truthy, non‑numeric createdAt (e.g., object, non‑numeric string), Number(...) becomes NaN, so the subtraction and <= comparison are NaN and the action is always considered expired. A non‑numeric now has the same effect. Please add back a minimal normalization/Number.isFinite check to avoid accidental early expiry when data is malformed.

Comment thread index.js
Daily giveaways, instant rewards, drops, and a secret new-user bonus waiting inside the bot.

Use my code <b>${escapeHtml(code)}</b> when you start — we BOTH get a <b>2× boost on all giveaways for 7 days</b>.
Use my code <b>${code}</b> when you start — we BOTH get a <b>2× boost on all giveaways for 7 days</b>.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚨 issue (security): Removing escapeHtml on code introduces an HTML injection / formatting risk in the referral message.

The previous escapeHtml(code) call prevented codes with <, >, &, or quotes from breaking the markup or injecting tags. Now code is interpolated directly into HTML, so a malicious or malformed value could alter the rendered content. Unless code is strictly validated to only allow safe characters, this should continue to be HTML-escaped here.

Comment thread index.js

/** Keyboard for the new persistent USER MAIN MENU */
function userMainMenuKeyboard(isAdminUser, user = null) {
const browserMode = user && user.settings && user.settings.playMode === 'browser';

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nitpick: browserMode is computed but not used in userMainMenuKeyboard.

browserMode is declared but never used in userMainMenuKeyboard. If it’s no longer needed after moving this logic into playLaunchButton, remove it; if it’s meant to control the keyboard layout, hook it into that branching logic instead.

Comment thread test/smoke.test.js
Comment on lines +555 to +564
test('persistent user menu play button is settings-aware and supports browser mode', () => {
const source = fs.readFileSync(path.resolve(__dirname, '..', 'index.js'), 'utf8');
assert.ok(source.includes('function getPlayButton('), 'Expected unified getPlayButton helper');
const helperStart = source.indexOf('function getPlayButton(');
const helperBlock = source.slice(helperStart, helperStart + 650);
assert.ok(helperBlock.includes("url: 'https://runewager.com/r/GambleCodez'"), 'Expected browser mode external URL');
assert.ok(helperBlock.includes("web_app: { url: 'https://t.me/RuneWager_bot/Play' }"), 'Expected mini app web_app launch');
assert.ok(!helperBlock.includes('startapp='), 'Expected no startapp launch in getPlayButton helper');
});

test('all menu play buttons use unified getPlayButton path with no hardcoded mixed mode', () => {
const source = fs.readFileSync(path.resolve(__dirname, '..', 'index.js'), 'utf8');
assert.ok(source.includes('function playButtonMarkup('), 'Expected helper to convert getPlayButton to Markup button');

const playButtonUsageCount = (source.match(/\[playButtonMarkup\(user\)\]/g) || []).length;
assert.ok(playButtonUsageCount >= 2, 'Expected both main and persistent menus to use unified play button helper');

const quickStart = source.indexOf("bot.action('menu_qc_play'");
assert.ok(quickStart >= 0, 'Expected quick-play callback');
const quickBlock = source.slice(quickStart, quickStart + 350);
assert.ok(quickBlock.includes('playButtonMarkup(user)'), 'Expected quick-play to use unified play button helper');
});

test('settings play mode toggle persists, clears old menus, and re-renders settings', () => {
const source = fs.readFileSync(path.resolve(__dirname, '..', 'index.js'), 'utf8');
const start = source.indexOf("bot.action('settings_toggle_playmode'");
assert.ok(start >= 0, 'Expected settings_toggle_playmode callback');
assert.ok(source.includes('function playLaunchButton('), 'Expected shared playLaunchButton helper');
const helperStart = source.indexOf('function playLaunchButton(');
const helperBlock = source.slice(helperStart, helperStart + 700);
assert.ok(helperBlock.includes('Markup.button.url'), 'Expected browser-mode URL launch button');
assert.ok(helperBlock.includes('Markup.button.webApp'), 'Expected mini-app webApp launch button');

const start = source.indexOf('function userMainMenuKeyboard(');
assert.ok(start >= 0, 'Expected userMainMenuKeyboard');

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (testing): Play button smoke test doesn’t fully exercise all callsites and routes of the new playLaunchButton helper

The new helper is also used by the quick‑play callback and on other routes that aren’t covered here. To make this a stronger guard on the new behavior, please:

  • Assert that the quick‑play callback (menu_qc_play) uses playLaunchButton or miniAppPlayButton, and
  • If there’s route‑specific behavior (e.g. profile, claim), add a small smoke test that those routes are passed into playLaunchButton/miniAppPlayButton as expected.

Suggested implementation:

test('persistent user menu play button is settings-aware and supports browser mode', () => {
  const source = fs.readFileSync(path.resolve(__dirname, '..', 'index.js'), 'utf8');

  // Ensure quick-play callback is wired through the new helper(s)
  assert.match(
    source,
    /menu_qc_play[\s\S]*?(playLaunchButton|miniAppPlayButton)/,
    'menu_qc_play should delegate to playLaunchButton or miniAppPlayButton'
  );

  // Ensure profile route is wired through the new helper(s)
  assert.match(
    source,
    /['"`]profile['"`][\s\S]*?(playLaunchButton|miniAppPlayButton)/,
    'profile route should delegate to playLaunchButton or miniAppPlayButton'
  );

  // Ensure claim route is wired through the new helper(s)
  assert.match(
    source,
    /['"`]claim['"`][\s\S]*?(playLaunchButton|miniAppPlayButton)/,
    'claim route should delegate to playLaunchButton or miniAppPlayButton'
  );
  • If the menu_qc_play, profile, or claim behaviors live in a different entry file (not index.js), adjust the fs.readFileSync target accordingly or add an additional source read for that file.
  • If the code structure separates route configuration from the helpers (e.g., routes in one module, handlers in another), you may want to tighten the regexes to match the actual wiring (e.g., menu_qc_play:.*playLaunchButton or router\.get\('/profile',.*playLaunchButton), once you confirm the concrete callsites in the codebase.

Comment thread test/unit.test.js


test('browser mode returns external runewager affiliate link only for play', () => {
test('browser mode returns external gamble-codez link only', () => {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (testing): Play link unit tests miss edge cases for default/unknown playMode and route behavior

To better cover this logic, consider adding:

  • Tests for when user is null or lacks settings.playMode, asserting the default miniapp behavior.
  • A table-driven test over all supported routes in browser mode (play, profile, claim, affiliate) plus an unknown route to confirm the intended fallback.

This will clarify the routing contract and reduce risk of regressions around play‑mode handling.

Comment thread test/unit.test.js

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 7 issues, and left some high level feedback:

  • The change to referralShareHTML (and removal of escapeHtml) means referral codes are now interpolated directly into HTML without escaping, which allows malicious codes to inject arbitrary markup into messages; consider restoring HTML escaping for the referral code before embedding it.
  • evaluatePendingActionTimeout now assumes a numeric now and only seeds createdAt when it is falsy; if createdAt is a non-numeric truthy value, Number(createdAt) becomes NaN and the timeout logic will treat it as immediately expired—consider reintroducing basic numeric validation for both now and createdAt to keep behavior predictable and resilient to corrupt state.
  • getPlayLink no longer falls back to user.playMode and instead defaults to 'miniapp' when user.settings.playMode is missing, which will silently change behavior for any legacy users stored with playMode at the top level; if that legacy shape still exists in production data, you may want to preserve the old fallback logic during the transition.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The change to `referralShareHTML` (and removal of `escapeHtml`) means referral codes are now interpolated directly into HTML without escaping, which allows malicious codes to inject arbitrary markup into messages; consider restoring HTML escaping for the referral code before embedding it.
- `evaluatePendingActionTimeout` now assumes a numeric `now` and only seeds `createdAt` when it is falsy; if `createdAt` is a non-numeric truthy value, `Number(createdAt)` becomes `NaN` and the timeout logic will treat it as immediately expired—consider reintroducing basic numeric validation for both `now` and `createdAt` to keep behavior predictable and resilient to corrupt state.
- `getPlayLink` no longer falls back to `user.playMode` and instead defaults to `'miniapp'` when `user.settings.playMode` is missing, which will silently change behavior for any legacy users stored with `playMode` at the top level; if that legacy shape still exists in production data, you may want to preserve the old fallback logic during the transition.

## Individual Comments

### Comment 1
<location path="index.js" line_range="73-74" />
<code_context>
 }

 function getPlayLink(user, route = 'play') {
-  return getPlayMode(user) === 'browser' ? getBrowserLink(route) : getStartAppLink(route);
-}
-
-function getPlayButton(user = null) {
-  if (getPlayMode(user) === 'browser') {
-    return {
-      text: '🔵 Play & Win',
-      url: 'https://runewager.com/r/GambleCodez',
-    };
-  }
-  return {
-    text: '🔵 Play & Win',
-    web_app: { url: 'https://t.me/RuneWager_bot/Play' },
-  };
-}
-
-function playButtonMarkup(user = null) {
-  const button = getPlayButton(user);
-  if (button.url) return Markup.button.url(button.text, button.url);
-  return Markup.button.webApp(button.text, button.web_app.url);
+  const mode = user && user.settings ? user.settings.playMode : 'miniapp';
+  return mode === 'browser' ? getBrowserLink(route) : getStartAppLink(route);
 }
</code_context>
<issue_to_address>
**issue:** Revised play-mode resolution ignores legacy/user-level `playMode` and may change behavior for existing users.

Previously `getPlayMode` fell back to `user.playMode` when `user.settings.playMode` was missing; the new logic only checks `user.settings.playMode` (or defaults to `'miniapp'`). To preserve existing behavior for records that still use the legacy field, consider:

```js
const mode = user && user.settings
  ? (user.settings.playMode ?? user.playMode)
  : (user && user.playMode) || 'miniapp';
```
If `user.playMode` is guaranteed to be unused in production, this change is fine—but it would be good to confirm that assumption against existing data first.
</issue_to_address>

### Comment 2
<location path="index.js" line_range="519-521" />
<code_context>
-    user.pendingAction.createdAt = safeNow;
-    return { hadPending: true, expired: false, expiredType: null };
-  }
+  if (!user.pendingAction.createdAt) user.pendingAction.createdAt = now;

-  if ((safeNow - normalizedCreatedAt) <= PENDING_ACTION_TIMEOUT_MS) {
+  if ((now - Number(user.pendingAction.createdAt || 0)) <= PENDING_ACTION_TIMEOUT_MS) {
     return { hadPending: true, expired: false, expiredType: null };
   }
</code_context>
<issue_to_address>
**issue (bug_risk):** The simplified timeout logic assumes `now` and `createdAt` are always numeric and may misbehave if non-numeric values are passed.

Previously, both `now` and `createdAt` were normalized with `Number()` and validated with `Number.isFinite`, falling back to `Date.now()` when invalid. The new version:

- Uses `now` directly in arithmetic without validation.
- Coerces `createdAt` with `Number()` but doesn’t handle `NaN` specially.

If `now` or `createdAt` are non-numeric (e.g., unexpected types), `Number(createdAt)` may be `NaN`, making the comparison always `false` and incorrectly treating actions as expired. To retain robustness, consider reintroducing lightweight validation:

```js
const nowMs = Number(now);
const safeNow = Number.isFinite(nowMs) ? nowMs : Date.now();

if (!user.pendingAction.createdAt) {
  user.pendingAction.createdAt = safeNow;
}

const createdMs = Number(user.pendingAction.createdAt);
if (!Number.isFinite(createdMs)) {
  user.pendingAction.createdAt = safeNow;
  return { hadPending: true, expired: false, expiredType: null };
}

if ((safeNow - createdMs) <= PENDING_ACTION_TIMEOUT_MS) {
  // ...
}
```
</issue_to_address>

### Comment 3
<location path="test/smoke.test.js" line_range="210-212" />
<code_context>
  * Supports `.*`, `^.*$`, and `.+` with optional grouping/anchors.
  */
 function isCatchAllRegexPattern(patternSource) {
-  let compact = String(patternSource || '').replace(/\s+/g, '');
-  compact = compact.replace(/^\^/, '').replace(/\$$/, '');
-  while (compact.startsWith('(') && compact.endsWith(')')) compact = compact.slice(1, -1);
-  compact = compact.replace(/^\?:/, '');
-
-  if (['.*', '.+', '.*?', '.+?'].includes(compact)) return true;
-  if (/^\(\?:?\.?\*\)$/.test(String(patternSource || '').replace(/\s+/g, ''))) return true;
-  return false;
+  const compact = String(patternSource || '').replace(/\s+/g, '');
+  return compact === '.*' || compact === '^.*$' || compact === '.+' || compact === '^.+$';
 }

</code_context>
<issue_to_address>
**issue:** Catch-all regex detection in the smoke test is now too narrow and may miss real catch-all patterns.

The previous `isCatchAllRegexPattern` normalized the pattern (removing anchors, outer grouping, and `?:` prefixes) so grouped/non-capturing variants like `(?:.*)`, `(.*)`, `^ (?:.*)$`, and lazy quantifiers were still treated as catch-alls. The new version only recognizes four exact strings, so semantically equivalent catch-all patterns will now be treated as non–catch-all, causing the smoke test to incorrectly flag them as needing explicit handlers or as coverage mismatches.

Consider either restoring the normalization (strip `^`/`$`, surrounding `(...)`, and `?:`) or expanding the recognized patterns to cover common grouped/non-capturing forms so all true catch-all handlers are still ignored by the smoke test.
</issue_to_address>

### Comment 4
<location path="test/unit.test.js" line_range="452-457" />
<code_context>


-test('browser mode returns external runewager affiliate link only for play', () => {
+test('browser mode returns external gamble-codez link only', () => {
   const user = { settings: { playMode: 'browser' } };
   const out = getPlayLink(user, 'play');
-  assert.equal(out, 'https://runewager.com/r/GambleCodez');
+  assert.match(out, /^https:\/\/gamble-codez\.com\//);
   assert.ok(!out.includes('startapp='));
   assert.ok(!out.includes('t.me/'));
 });

</code_context>
<issue_to_address>
**suggestion (testing):** Browser-mode link test only asserts the domain, losing route-specific coverage.

This change weakens the test: we now only verify the domain instead of the specific `play` route URL that was previously asserted. To keep route-level coverage, consider asserting the exact expected `play` URL (e.g. `'https://gamble-codez.com/runewager'`) and/or adding similar assertions for `profile`, `claim`, and `affiliate` so misconfigured paths or typos are caught.

Suggested implementation:

```javascript
test('browser mode returns external gamble-codez play link only', () => {
  const user = { settings: { playMode: 'browser' } };
  const out = getPlayLink(user, 'play');
  assert.equal(out, 'https://gamble-codez.com/runewager');
  assert.ok(!out.includes('startapp='));
  assert.ok(!out.includes('t.me/'));
});

test('browser mode returns external gamble-codez profile link only', () => {
  const user = { settings: { playMode: 'browser' } };
  const out = getPlayLink(user, 'profile');
  assert.equal(out, 'https://gamble-codez.com/profile');
  assert.ok(!out.includes('startapp='));
  assert.ok(!out.includes('t.me/'));
});

test('browser mode returns external gamble-codez claim link only', () => {
  const user = { settings: { playMode: 'browser' } };
  const out = getPlayLink(user, 'claim');
  assert.equal(out, 'https://gamble-codez.com/claim');
  assert.ok(!out.includes('startapp='));
  assert.ok(!out.includes('t.me/'));
});

test('browser mode returns external gamble-codez affiliate link only', () => {
  const user = { settings: { playMode: 'browser' } };
  const out = getPlayLink(user, 'affiliate');
  assert.equal(out, 'https://gamble-codez.com/affiliate');
  assert.ok(!out.includes('startapp='));
  assert.ok(!out.includes('t.me/'));
});

```

These tests assume `getBrowserLink` maps:
- `play``'https://gamble-codez.com/runewager'`
- `profile``'https://gamble-codez.com/profile'`
- `claim``'https://gamble-codez.com/claim'`
- `affiliate``'https://gamble-codez.com/affiliate'`

If `getBrowserLink` uses different paths, update the expected URLs in the new assertions to match the actual mapping to keep the tests and implementation in sync.
</issue_to_address>

### Comment 5
<location path="test/unit.test.js" line_range="477-482" />
<code_context>
-    .replace(/'/g, '&#39;');
-}
-
 function referralShareHTML(code) {
   return `<b>🚨 YO! I’m farming free SC on Runewager and you need to get in NOW.</b>

 Daily giveaways, instant rewards, drops, and a secret new-user bonus waiting inside the bot.

-Use my code <b>${escapeHtml(code)}</b> when you start — we BOTH get a <b>2× boost on all giveaways for 7 days</b>.
+Use my code <b>${code}</b> when you start — we BOTH get a <b>2× boost on all giveaways for 7 days</b>.

 👉 <a href="https://t.me/RunewagerBot">Tap here to join Runewager</a>
</code_context>
<issue_to_address>
**suggestion (testing):** There is no longer any test that validates the referralShareHTML template or code interpolation.

With `escapeHtml` and its test removed, `referralShareHTML` no longer has direct test coverage here. Please add at least one test that:
- checks the returned HTML includes the expected static copy (headline, body, CTA link), and
- verifies a simple referral code (e.g. `'ABC123'`) is interpolated into the `<b>...</b>` span as expected.

This helps catch regressions in the copy and interpolation while matching the new non-escaping behavior.
</issue_to_address>

### Comment 6
<location path="test/unit.test.js" line_range="467" />
<code_context>
-  assert.equal(mini.url, undefined);
-});
-
 test('discord links remain external and unwrap telegram wrappers', () => {
   const wrapped = 'https://t.me/iv?url=https%3A%2F%2Fdiscord.gg%2Frunewagers';
   const out = getDiscordLink(wrapped);
</code_context>
<issue_to_address>
**suggestion (testing):** Extend Discord link tests to cover direct and non-Discord URLs as well.

To better exercise `getDiscordLink`, please also add tests for:
- a plain Discord URL (no Telegram wrapper) to verify it’s accepted and normalized, and
- a non-Discord or malformed URL to verify it falls back to the default invite link.

This will clarify the helper’s behavior and protect against regressions in URL handling.
</issue_to_address>

### Comment 7
<location path="RUNEWAGER_FUNCTIONALITY_MAP.md" line_range="197" />
<code_context>

 - Admin identity is based on `ADMIN_IDS` env list.
-- `.env.example` documents both `BOT_PRIVACY_MODE="disabled"` (BotFather non-privacy mode), plus Telegram/HTTPS/discovery keys used by runtime checks.
+- `.env.example` documents both `BOT_PRIVACY_MODE="disabled"` (BotFather non-privacy mode) and compatibility `BOTPRIVACYMODE`, plus Telegram/HTTPS/discovery keys used by runtime checks.
 - `requireAdmin(ctx)` gates command/callback execution.
 - Admin mode UI toggle changes visibility of admin UI, not true authorization.
</code_context>
<issue_to_address>
**nitpick (typo):** Consider clarifying the phrase "and compatibility `BOTPRIVACYMODE`" by adding a noun such as "alias".

Here “compatibility” is used without a noun, which reads a bit incomplete. Please rephrase to something like “and compatibility alias `BOTPRIVACYMODE`” or “and its compatibility alias `BOTPRIVACYMODE`” to improve the grammar while keeping the meaning.

```suggestion
- `.env.example` documents both `BOT_PRIVACY_MODE="disabled"` (BotFather non-privacy mode) and its compatibility alias `BOTPRIVACYMODE`, plus Telegram/HTTPS/discovery keys used by runtime checks.
```
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread index.js
Comment on lines 73 to +74
function getPlayLink(user, route = 'play') {
return getPlayMode(user) === 'browser' ? getBrowserLink(route) : getStartAppLink(route);
}

function getPlayButton(user = null) {
if (getPlayMode(user) === 'browser') {
return {
text: '🔵 Play & Win',
url: 'https://runewager.com/r/GambleCodez',
};
}
return {
text: '🔵 Play & Win',
web_app: { url: 'https://t.me/RuneWager_bot/Play' },
};
}

function playButtonMarkup(user = null) {
const button = getPlayButton(user);
if (button.url) return Markup.button.url(button.text, button.url);
return Markup.button.webApp(button.text, button.web_app.url);
const mode = user && user.settings ? user.settings.playMode : 'miniapp';

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue: Revised play-mode resolution ignores legacy/user-level playMode and may change behavior for existing users.

Previously getPlayMode fell back to user.playMode when user.settings.playMode was missing; the new logic only checks user.settings.playMode (or defaults to 'miniapp'). To preserve existing behavior for records that still use the legacy field, consider:

const mode = user && user.settings
  ? (user.settings.playMode ?? user.playMode)
  : (user && user.playMode) || 'miniapp';

If user.playMode is guaranteed to be unused in production, this change is fine—but it would be good to confirm that assumption against existing data first.

Comment thread index.js
Comment on lines +519 to +521
if (!user.pendingAction.createdAt) user.pendingAction.createdAt = now;

if ((safeNow - normalizedCreatedAt) <= PENDING_ACTION_TIMEOUT_MS) {
if ((now - Number(user.pendingAction.createdAt || 0)) <= PENDING_ACTION_TIMEOUT_MS) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (bug_risk): The simplified timeout logic assumes now and createdAt are always numeric and may misbehave if non-numeric values are passed.

Previously, both now and createdAt were normalized with Number() and validated with Number.isFinite, falling back to Date.now() when invalid. The new version:

  • Uses now directly in arithmetic without validation.
  • Coerces createdAt with Number() but doesn’t handle NaN specially.

If now or createdAt are non-numeric (e.g., unexpected types), Number(createdAt) may be NaN, making the comparison always false and incorrectly treating actions as expired. To retain robustness, consider reintroducing lightweight validation:

const nowMs = Number(now);
const safeNow = Number.isFinite(nowMs) ? nowMs : Date.now();

if (!user.pendingAction.createdAt) {
  user.pendingAction.createdAt = safeNow;
}

const createdMs = Number(user.pendingAction.createdAt);
if (!Number.isFinite(createdMs)) {
  user.pendingAction.createdAt = safeNow;
  return { hadPending: true, expired: false, expiredType: null };
}

if ((safeNow - createdMs) <= PENDING_ACTION_TIMEOUT_MS) {
  // ...
}

Comment thread test/smoke.test.js
Comment on lines 210 to -212
function isCatchAllRegexPattern(patternSource) {
let compact = String(patternSource || '').replace(/\s+/g, '');
compact = compact.replace(/^\^/, '').replace(/\$$/, '');

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue: Catch-all regex detection in the smoke test is now too narrow and may miss real catch-all patterns.

The previous isCatchAllRegexPattern normalized the pattern (removing anchors, outer grouping, and ?: prefixes) so grouped/non-capturing variants like (?:.*), (.*), ^ (?:.*)$, and lazy quantifiers were still treated as catch-alls. The new version only recognizes four exact strings, so semantically equivalent catch-all patterns will now be treated as non–catch-all, causing the smoke test to incorrectly flag them as needing explicit handlers or as coverage mismatches.

Consider either restoring the normalization (strip ^/$, surrounding (...), and ?:) or expanding the recognized patterns to cover common grouped/non-capturing forms so all true catch-all handlers are still ignored by the smoke test.

Comment thread test/unit.test.js
Comment on lines +452 to 457
test('browser mode returns external gamble-codez link only', () => {
const user = { settings: { playMode: 'browser' } };
const out = getPlayLink(user, 'play');
assert.equal(out, 'https://runewager.com/r/GambleCodez');
assert.match(out, /^https:\/\/gamble-codez\.com\//);
assert.ok(!out.includes('startapp='));
assert.ok(!out.includes('t.me/'));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (testing): Browser-mode link test only asserts the domain, losing route-specific coverage.

This change weakens the test: we now only verify the domain instead of the specific play route URL that was previously asserted. To keep route-level coverage, consider asserting the exact expected play URL (e.g. 'https://gamble-codez.com/runewager') and/or adding similar assertions for profile, claim, and affiliate so misconfigured paths or typos are caught.

Suggested implementation:

test('browser mode returns external gamble-codez play link only', () => {
  const user = { settings: { playMode: 'browser' } };
  const out = getPlayLink(user, 'play');
  assert.equal(out, 'https://gamble-codez.com/runewager');
  assert.ok(!out.includes('startapp='));
  assert.ok(!out.includes('t.me/'));
});

test('browser mode returns external gamble-codez profile link only', () => {
  const user = { settings: { playMode: 'browser' } };
  const out = getPlayLink(user, 'profile');
  assert.equal(out, 'https://gamble-codez.com/profile');
  assert.ok(!out.includes('startapp='));
  assert.ok(!out.includes('t.me/'));
});

test('browser mode returns external gamble-codez claim link only', () => {
  const user = { settings: { playMode: 'browser' } };
  const out = getPlayLink(user, 'claim');
  assert.equal(out, 'https://gamble-codez.com/claim');
  assert.ok(!out.includes('startapp='));
  assert.ok(!out.includes('t.me/'));
});

test('browser mode returns external gamble-codez affiliate link only', () => {
  const user = { settings: { playMode: 'browser' } };
  const out = getPlayLink(user, 'affiliate');
  assert.equal(out, 'https://gamble-codez.com/affiliate');
  assert.ok(!out.includes('startapp='));
  assert.ok(!out.includes('t.me/'));
});

These tests assume getBrowserLink maps:

  • play'https://gamble-codez.com/runewager'
  • profile'https://gamble-codez.com/profile'
  • claim'https://gamble-codez.com/claim'
  • affiliate'https://gamble-codez.com/affiliate'

If getBrowserLink uses different paths, update the expected URLs in the new assertions to match the actual mapping to keep the tests and implementation in sync.

Comment thread test/unit.test.js
Comment on lines 477 to +482
function referralShareHTML(code) {
return `<b>🚨 YO! I’m farming free SC on Runewager and you need to get in NOW.</b>

Daily giveaways, instant rewards, drops, and a secret new-user bonus waiting inside the bot.

Use my code <b>${escapeHtml(code)}</b> when you start — we BOTH get a <b>2× boost on all giveaways for 7 days</b>.
Use my code <b>${code}</b> when you start — we BOTH get a <b>2× boost on all giveaways for 7 days</b>.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (testing): There is no longer any test that validates the referralShareHTML template or code interpolation.

With escapeHtml and its test removed, referralShareHTML no longer has direct test coverage here. Please add at least one test that:

  • checks the returned HTML includes the expected static copy (headline, body, CTA link), and
  • verifies a simple referral code (e.g. 'ABC123') is interpolated into the <b>...</b> span as expected.

This helps catch regressions in the copy and interpolation while matching the new non-escaping behavior.

Comment thread test/unit.test.js
assert.equal(mini.url, undefined);
});

test('discord links remain external and unwrap telegram wrappers', () => {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (testing): Extend Discord link tests to cover direct and non-Discord URLs as well.

To better exercise getDiscordLink, please also add tests for:

  • a plain Discord URL (no Telegram wrapper) to verify it’s accepted and normalized, and
  • a non-Discord or malformed URL to verify it falls back to the default invite link.

This will clarify the helper’s behavior and protect against regressions in URL handling.


- Admin identity is based on `ADMIN_IDS` env list.
- `.env.example` documents both `BOT_PRIVACY_MODE="disabled"` (BotFather non-privacy mode), plus Telegram/HTTPS/discovery keys used by runtime checks.
- `.env.example` documents both `BOT_PRIVACY_MODE="disabled"` (BotFather non-privacy mode) and compatibility `BOTPRIVACYMODE`, plus Telegram/HTTPS/discovery keys used by runtime checks.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nitpick (typo): Consider clarifying the phrase "and compatibility BOTPRIVACYMODE" by adding a noun such as "alias".

Here “compatibility” is used without a noun, which reads a bit incomplete. Please rephrase to something like “and compatibility alias BOTPRIVACYMODE” or “and its compatibility alias BOTPRIVACYMODE” to improve the grammar while keeping the meaning.

Suggested change
- `.env.example` documents both `BOT_PRIVACY_MODE="disabled"` (BotFather non-privacy mode) and compatibility `BOTPRIVACYMODE`, plus Telegram/HTTPS/discovery keys used by runtime checks.
- `.env.example` documents both `BOT_PRIVACY_MODE="disabled"` (BotFather non-privacy mode) and its compatibility alias `BOTPRIVACYMODE`, plus Telegram/HTTPS/discovery keys used by runtime checks.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

codex size:M This PR changes 30-99 lines, ignoring generated files

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant