Skip to content

feat(bot): /voterewards command + GET /api/internal/votes/:userId - #730

Merged
LucasSantana-Dev merged 1 commit into
feature/topgg-vote-webhookfrom
feature/voterewards-command
Apr 20, 2026
Merged

LucasSantana-Dev merged 1 commit into
feature/topgg-vote-webhookfrom
feature/voterewards-command

Conversation

@LucasSantana-Dev

Copy link
Copy Markdown
Owner

Summary

Closes the top.gg vote loop on top of #729.

  • backend: adds `GET /api/internal/votes/:userId` behind `LUCKY_NOTIFY_API_KEY`, returning `{ hasVoted, streak, nextVoteInSeconds }`. Kept in `routes/webhooks.ts` alongside the POST webhook so all vote-related routes live in one file.
  • bot: `/voterewards` slash command fetches the user's state and replies with a tier embed showing streak + next vote countdown. Graceful fallback when backend is unreachable or env vars are missing — the top.gg vote link is always surfaced.

Tier table (matches `docs/TOP_GG_SUBMISSION.md` §6)

Streak Tier Perk
1+ Lucky Supporter Our thanks 💛
7+ Lucky Fan Early access to new commands
14+ Lucky Regular Autoplay weighting + early access
30+ Lucky Legend Dashboard badge + weighting + priority support

Stacking

This is stacked on `feature/topgg-vote-webhook` (#729). Base branch is `feature/topgg-vote-webhook` — merge #729 first, then GitHub will auto-retarget this to `main` and it'll rebase cleanly.

Test plan

  • `npm run type:check --workspace=packages/backend` → clean
  • `npx tsc --noEmit` in `packages/bot` → clean
  • 9 backend unit tests pass (6 POST webhook + 3 GET endpoint)
  • After deploy: `curl -H "x-notify-key: $KEY" https://api.../api/internal/votes/` returns the expected shape
  • `/voterewards` in Discord shows the embed (both voted and not-voted cases)

Closes the top.gg vote loop:
- backend: adds GET /api/internal/votes/:userId behind LUCKY_NOTIFY_API_KEY
  returning { hasVoted, streak, nextVoteInSeconds }. Keeps all vote-related
  routes in the same file (webhooks.ts) but separates the internal read
  (internal-key auth) from the external webhook (top.gg auth).
- bot: /voterewards slash command fetches the user's state and replies
  with tier + streak + next-vote countdown. Graceful fallback when the
  backend is unreachable or env vars are missing — surfaces the top.gg
  vote link unconditionally so the command is always useful.

Tier table (matches docs/TOP_GG_SUBMISSION.md §6):
  1+ streak → Lucky Supporter
  7+ streak → Lucky Fan (early access to new commands)
  14+ streak → Lucky Regular (autoplay weighting + early access)
  30+ streak → Lucky Legend (dashboard badge + weighting + priority support)

Stacked on feature/topgg-vote-webhook (#729) — merge #729 first, then this
rebases cleanly.

Tests: 9 total in webhooks.test.ts (+ 3 new covering the GET endpoint).
Type-check: backend + bot both clean.
@vercel

vercel Bot commented Apr 20, 2026

Copy link
Copy Markdown
Contributor

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
lucky Building Building Preview, Comment Apr 20, 2026 2:04am

Request Review

@coderabbitai

coderabbitai Bot commented Apr 20, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: f63a9d15-efaf-4319-b7d7-8359cd3fec9b

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/voterewards-command

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.

@LucasSantana-Dev
LucasSantana-Dev merged commit fc8caf4 into feature/topgg-vote-webhook Apr 20, 2026
5 of 7 checks passed
@LucasSantana-Dev
LucasSantana-Dev deleted the feature/voterewards-command branch April 20, 2026 02:04
LucasSantana-Dev added a commit that referenced this pull request Apr 20, 2026
Extends the top.gg flywheel to the authenticated web dashboard.

Authenticated user reads their own vote state + tier:

  GET /api/me/vote-status  (requireAuth)
  →  {
       hasVoted, streak, nextVoteInSeconds,
       tier: { label, threshold } | null,
       nextTier: { label, threshold } | null,
       voteUrl: "https://top.gg/bot/.../vote"
     }

Tier thresholds match the /voterewards slash command exactly:
  1+  → Lucky Supporter
  7+  → Lucky Fan
  14+ → Lucky Regular
  30+ → Lucky Legend

Reuses the existing readVoteState() helper — same Redis keys the webhook
writes to, so the three endpoints (POST webhook, GET internal, GET me)
stay in perfect sync. No schema change, no new env vars.

Stacked on feature/topgg-vote-webhook (builds on #729 + #730).

Tests: 3 new cases added to webhooks.test.ts (13 total now):
- 401 when not authenticated
- Streak-14 voter → tier=Regular, nextTier=Legend
- 0-streak user → tier=null, nextTier=Supporter
- 30+ streak → tier=Legend, nextTier=null
LucasSantana-Dev added a commit that referenced this pull request Apr 20, 2026
Extends the top.gg flywheel to the authenticated web dashboard.

Authenticated user reads their own vote state + tier:

  GET /api/me/vote-status  (requireAuth)
  →  {
       hasVoted, streak, nextVoteInSeconds,
       tier: { label, threshold } | null,
       nextTier: { label, threshold } | null,
       voteUrl: "https://top.gg/bot/.../vote"
     }

Tier thresholds match the /voterewards slash command exactly:
  1+  → Lucky Supporter
  7+  → Lucky Fan
  14+ → Lucky Regular
  30+ → Lucky Legend

Reuses the existing readVoteState() helper — same Redis keys the webhook
writes to, so the three endpoints (POST webhook, GET internal, GET me)
stay in perfect sync. No schema change, no new env vars.

Stacked on feature/topgg-vote-webhook (builds on #729 + #730).

Tests: 3 new cases added to webhooks.test.ts (13 total now):
- 401 when not authenticated
- Streak-14 voter → tier=Regular, nextTier=Legend
- 0-streak user → tier=null, nextTier=Supporter
- 30+ streak → tier=Legend, nextTier=null
LucasSantana-Dev added a commit that referenced this pull request Apr 22, 2026
Closes the top.gg vote loop:
- backend: adds GET /api/internal/votes/:userId behind LUCKY_NOTIFY_API_KEY
  returning { hasVoted, streak, nextVoteInSeconds }. Keeps all vote-related
  routes in the same file (webhooks.ts) but separates the internal read
  (internal-key auth) from the external webhook (top.gg auth).
- bot: /voterewards slash command fetches the user's state and replies
  with tier + streak + next-vote countdown. Graceful fallback when the
  backend is unreachable or env vars are missing — surfaces the top.gg
  vote link unconditionally so the command is always useful.

Tier table (matches docs/TOP_GG_SUBMISSION.md §6):
  1+ streak → Lucky Supporter
  7+ streak → Lucky Fan (early access to new commands)
  14+ streak → Lucky Regular (autoplay weighting + early access)
  30+ streak → Lucky Legend (dashboard badge + weighting + priority support)

Stacked on feature/topgg-vote-webhook (#729) — merge #729 first, then this
rebases cleanly.

Tests: 9 total in webhooks.test.ts (+ 3 new covering the GET endpoint).
Type-check: backend + bot both clean.
LucasSantana-Dev added a commit that referenced this pull request Apr 22, 2026
Extends the top.gg flywheel to the authenticated web dashboard.

Authenticated user reads their own vote state + tier:

  GET /api/me/vote-status  (requireAuth)
  →  {
       hasVoted, streak, nextVoteInSeconds,
       tier: { label, threshold } | null,
       nextTier: { label, threshold } | null,
       voteUrl: "https://top.gg/bot/.../vote"
     }

Tier thresholds match the /voterewards slash command exactly:
  1+  → Lucky Supporter
  7+  → Lucky Fan
  14+ → Lucky Regular
  30+ → Lucky Legend

Reuses the existing readVoteState() helper — same Redis keys the webhook
writes to, so the three endpoints (POST webhook, GET internal, GET me)
stay in perfect sync. No schema change, no new env vars.

Stacked on feature/topgg-vote-webhook (builds on #729 + #730).

Tests: 3 new cases added to webhooks.test.ts (13 total now):
- 401 when not authenticated
- Streak-14 voter → tier=Regular, nextTier=Legend
- 0-streak user → tier=null, nextTier=Supporter
- 30+ streak → tier=Legend, nextTier=null
LucasSantana-Dev added a commit that referenced this pull request Apr 22, 2026
* feat(backend): top.gg vote webhook endpoint

Adds POST /webhooks/topgg-votes to persist vote events for the upcoming
top.gg listing. Locked behind TOPGG_AUTH_TOKEN — returns 503 when unset
so the endpoint is inert until the listing goes live.

On each upvote:
- votes:<userId>            — 12h TTL (matches top.gg's 12h vote window)
- votes:streak:<userId>     — incremented, 36h TTL (12h grace around the
                              24h cycle so daily voters don't lose streaks)

Returns { ok: true, test: true } for top.gg's test pings.

Wiring: registered in routes/index.ts alongside the other non-API
endpoints (health, stats, internal notify). No /api/ prefix — matches
top.gg's expected webhook URL shape.

Env: TOPGG_AUTH_TOKEN added to .env.example (commented out).

Tests: 6 unit tests covering missing/wrong auth, test-type payload,
missing user id, and a successful upvote (verifies pipeline ops).

Follow-up (next PR): /voterewards slash command in packages/bot that
reads votes:streak:<user> and grants tier benefits. See
docs/TOP_GG_SUBMISSION.md §6 for the broader wiring plan.

* feat(bot): /voterewards command + GET /api/internal/votes/:userId (#730)

Closes the top.gg vote loop:
- backend: adds GET /api/internal/votes/:userId behind LUCKY_NOTIFY_API_KEY
  returning { hasVoted, streak, nextVoteInSeconds }. Keeps all vote-related
  routes in the same file (webhooks.ts) but separates the internal read
  (internal-key auth) from the external webhook (top.gg auth).
- bot: /voterewards slash command fetches the user's state and replies
  with tier + streak + next-vote countdown. Graceful fallback when the
  backend is unreachable or env vars are missing — surfaces the top.gg
  vote link unconditionally so the command is always useful.

Tier table (matches docs/TOP_GG_SUBMISSION.md §6):
  1+ streak → Lucky Supporter
  7+ streak → Lucky Fan (early access to new commands)
  14+ streak → Lucky Regular (autoplay weighting + early access)
  30+ streak → Lucky Legend (dashboard badge + weighting + priority support)

Stacked on feature/topgg-vote-webhook (#729) — merge #729 first, then this
rebases cleanly.

Tests: 9 total in webhooks.test.ts (+ 3 new covering the GET endpoint).
Type-check: backend + bot both clean.

* feat(backend): GET /api/me/vote-status for the dashboard (#736)

Extends the top.gg flywheel to the authenticated web dashboard.

Authenticated user reads their own vote state + tier:

  GET /api/me/vote-status  (requireAuth)
  →  {
       hasVoted, streak, nextVoteInSeconds,
       tier: { label, threshold } | null,
       nextTier: { label, threshold } | null,
       voteUrl: "https://top.gg/bot/.../vote"
     }

Tier thresholds match the /voterewards slash command exactly:
  1+  → Lucky Supporter
  7+  → Lucky Fan
  14+ → Lucky Regular
  30+ → Lucky Legend

Reuses the existing readVoteState() helper — same Redis keys the webhook
writes to, so the three endpoints (POST webhook, GET internal, GET me)
stay in perfect sync. No schema change, no new env vars.

Stacked on feature/topgg-vote-webhook (builds on #729 + #730).

Tests: 3 new cases added to webhooks.test.ts (13 total now):
- 401 when not authenticated
- Streak-14 voter → tier=Regular, nextTier=Legend
- 0-streak user → tier=null, nextTier=Supporter
- 30+ streak → tier=Legend, nextTier=null

* fix(webhooks): idempotent top.gg upvotes + timing-safe token compare + redis error listener

Addresses CodeRabbit feedback on #729:

- **Idempotency**: top.gg retries webhook deliveries on handler failure/timeout.
  The previous pipeline INCR'd votes:streak:<userId> on every successful POST,
  double-counting the streak on retries. Switched to SET NX as the idempotency
  gate — only the first write within the 12h vote window increments the streak;
  subsequent retries return { ok: true, duplicate: true } without touching the
  counter.
- **Timing-safe compare**: replaced raw !== on TOPGG_AUTH_TOKEN and
  LUCKY_NOTIFY_API_KEY with crypto.timingSafeEqual via a length-checked
  safeEqualString helper. Prevents length/prefix timing leaks over public
  internet.
- **Redis error listener**: attach .on('error', ...) to the cached ioredis
  client. Without this, connection failures during retry raise
  uncaughtException; we log and swallow since maxRetriesPerRequest=1 already
  surfaces errors per-request.
- **Explicit vote type check**: reject payloads where type is neither 'test'
  nor 'upvote'. Prevents future top.gg event types (e.g. downvotes) from
  silently incrementing streaks.

Tests: added 2 new cases (duplicate vote, unsupported type) + updated the
existing "records upvote" assertion to check SET NX. 15/15 passing.

* fix: gate vote state routes behind api limiter

* fix: make topgg vote writes atomic
LucasSantana-Dev added a commit that referenced this pull request May 13, 2026
* feat(backend): top.gg vote webhook endpoint

Adds POST /webhooks/topgg-votes to persist vote events for the upcoming
top.gg listing. Locked behind TOPGG_AUTH_TOKEN — returns 503 when unset
so the endpoint is inert until the listing goes live.

On each upvote:
- votes:<userId>            — 12h TTL (matches top.gg's 12h vote window)
- votes:streak:<userId>     — incremented, 36h TTL (12h grace around the
                              24h cycle so daily voters don't lose streaks)

Returns { ok: true, test: true } for top.gg's test pings.

Wiring: registered in routes/index.ts alongside the other non-API
endpoints (health, stats, internal notify). No /api/ prefix — matches
top.gg's expected webhook URL shape.

Env: TOPGG_AUTH_TOKEN added to .env.example (commented out).

Tests: 6 unit tests covering missing/wrong auth, test-type payload,
missing user id, and a successful upvote (verifies pipeline ops).

Follow-up (next PR): /voterewards slash command in packages/bot that
reads votes:streak:<user> and grants tier benefits. See
docs/TOP_GG_SUBMISSION.md §6 for the broader wiring plan.

* feat(bot): /voterewards command + GET /api/internal/votes/:userId (#730)

Closes the top.gg vote loop:
- backend: adds GET /api/internal/votes/:userId behind LUCKY_NOTIFY_API_KEY
  returning { hasVoted, streak, nextVoteInSeconds }. Keeps all vote-related
  routes in the same file (webhooks.ts) but separates the internal read
  (internal-key auth) from the external webhook (top.gg auth).
- bot: /voterewards slash command fetches the user's state and replies
  with tier + streak + next-vote countdown. Graceful fallback when the
  backend is unreachable or env vars are missing — surfaces the top.gg
  vote link unconditionally so the command is always useful.

Tier table (matches docs/TOP_GG_SUBMISSION.md §6):
  1+ streak → Lucky Supporter
  7+ streak → Lucky Fan (early access to new commands)
  14+ streak → Lucky Regular (autoplay weighting + early access)
  30+ streak → Lucky Legend (dashboard badge + weighting + priority support)

Stacked on feature/topgg-vote-webhook (#729) — merge #729 first, then this
rebases cleanly.

Tests: 9 total in webhooks.test.ts (+ 3 new covering the GET endpoint).
Type-check: backend + bot both clean.

* feat(backend): GET /api/me/vote-status for the dashboard (#736)

Extends the top.gg flywheel to the authenticated web dashboard.

Authenticated user reads their own vote state + tier:

  GET /api/me/vote-status  (requireAuth)
  →  {
       hasVoted, streak, nextVoteInSeconds,
       tier: { label, threshold } | null,
       nextTier: { label, threshold } | null,
       voteUrl: "https://top.gg/bot/.../vote"
     }

Tier thresholds match the /voterewards slash command exactly:
  1+  → Lucky Supporter
  7+  → Lucky Fan
  14+ → Lucky Regular
  30+ → Lucky Legend

Reuses the existing readVoteState() helper — same Redis keys the webhook
writes to, so the three endpoints (POST webhook, GET internal, GET me)
stay in perfect sync. No schema change, no new env vars.

Stacked on feature/topgg-vote-webhook (builds on #729 + #730).

Tests: 3 new cases added to webhooks.test.ts (13 total now):
- 401 when not authenticated
- Streak-14 voter → tier=Regular, nextTier=Legend
- 0-streak user → tier=null, nextTier=Supporter
- 30+ streak → tier=Legend, nextTier=null

* fix(webhooks): idempotent top.gg upvotes + timing-safe token compare + redis error listener

Addresses CodeRabbit feedback on #729:

- **Idempotency**: top.gg retries webhook deliveries on handler failure/timeout.
  The previous pipeline INCR'd votes:streak:<userId> on every successful POST,
  double-counting the streak on retries. Switched to SET NX as the idempotency
  gate — only the first write within the 12h vote window increments the streak;
  subsequent retries return { ok: true, duplicate: true } without touching the
  counter.
- **Timing-safe compare**: replaced raw !== on TOPGG_AUTH_TOKEN and
  LUCKY_NOTIFY_API_KEY with crypto.timingSafeEqual via a length-checked
  safeEqualString helper. Prevents length/prefix timing leaks over public
  internet.
- **Redis error listener**: attach .on('error', ...) to the cached ioredis
  client. Without this, connection failures during retry raise
  uncaughtException; we log and swallow since maxRetriesPerRequest=1 already
  surfaces errors per-request.
- **Explicit vote type check**: reject payloads where type is neither 'test'
  nor 'upvote'. Prevents future top.gg event types (e.g. downvotes) from
  silently incrementing streaks.

Tests: added 2 new cases (duplicate vote, unsupported type) + updated the
existing "records upvote" assertion to check SET NX. 15/15 passing.

* fix: gate vote state routes behind api limiter

* fix: make topgg vote writes atomic

This branch was successfully deployed

1 active deployment
Preview — 952bf4c6 Deployed Apr 20, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant