Repository navigation
feat(bot): /voterewards command + GET /api/internal/votes/:userId - #730
Conversation
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.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
fc8caf4
into
feature/topgg-vote-webhook
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
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
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.
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
* 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
* 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
Summary
Closes the top.gg vote loop on top of #729.
Tier table (matches `docs/TOP_GG_SUBMISSION.md` §6)
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