Full giveaway system upgrade: admin menu, test-giveaway, pinned announcements, and payout manager - #95
Conversation
…inking + pinned announcements + payout flow + map sync
|
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 · |
|
Warning Rate limit exceeded
⌛ How to resolve this issue?After the wait time has elapsed, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout. Please see our FAQ for further information. 📒 Files selected for processing (2)
✨ 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 |
|
CodeAnt AI finished reviewing your PR. |
|
@sourcery-ai review |
Reviewer's GuideUpgrades the giveaway system by expanding admin tools, introducing an isolated test-giveaway flow with fake participants and weighted winner selection, enriching announcements/join UX, excluding test data from persistence and listings, and adding a bulk payout manager for real giveaways, with documentation synced in the functionality map. Sequence diagram for confirmation-gated test giveaway flowsequenceDiagram
actor Admin
participant Bot
participant ApprovedGroupsStore
participant GiveawayStore
participant UserStore
participant TelegramAPI
Admin->>Bot: press admin_cmd_testgiveaway
Bot->>Admin: show warning + groupHint + Confirm/Cancel
Admin->>Bot: press tgw_confirm_start
Bot->>ApprovedGroupsStore: hasAnyLinkedGroup
ApprovedGroupsStore-->>Bot: result
alt no linked group
Bot->>Admin: reply cannot start, show group linking tools
else has linked group
Bot->>Bot: startConfiguredTestGiveaway
Bot->>GiveawayStore: createGiveaway(testMode=true, dryRun=true)
Bot->>GiveawayStore: add 10 fake participants
Bot->>TelegramAPI: sendMessage buildGiveawayAnnouncementText
TelegramAPI-->>Bot: sent message_id
Bot->>GiveawayStore: store announcementMsgId, pinnedMsgId
Bot->>TelegramAPI: pinChatMessage
TelegramAPI-->>Bot: success or error
alt pin failed
Bot->>Admin: notifyAdmins pin failure
end
Bot->>GiveawayStore: set endTimer (2 minutes)
Bot->>Admin: reply test giveaway started summary
end
Note over Bot,GiveawayStore: After 2 minutes endTimer fires
Bot->>GiveawayStore: get test giveaway by id
alt still running
Bot->>Bot: runTestGiveawayFinale
Bot->>GiveawayStore: build weighted participant pool
Bot->>UserStore: get admin user for guaranteed winner
UserStore-->>Bot: admin user or null
Bot->>GiveawayStore: select winners with weighting + admin inclusion
Bot->>GiveawayStore: update status ended_test
Bot->>TelegramAPI: unpinChatMessage
Bot->>TelegramAPI: sendMessage test summary to chat
loop for each real winner
Bot->>TelegramAPI: sendMessage DM test winner notice
end
Bot->>TelegramAPI: sendMessage admin summary
Bot->>GiveawayStore: clear participants and winners, delete running giveaway
else not running
Bot->>Bot: no-op
end
Sequence diagram for Winner Payout Manager bulk payout flowsequenceDiagram
actor Admin
participant Bot
participant GiveawayStore
participant TelegramAPI
Admin->>Bot: press admin_gw_payout_manager
Bot->>GiveawayStore: get history giveaways
GiveawayStore-->>Bot: history list
Bot->>Bot: filter ended, not paidOut, not testMode, has winners
alt no unpaid
Bot->>Admin: reply no unpaid real giveaways
else unpaid exist
Bot->>Admin: reply payout summary + button gw_payout_all
end
Admin->>Bot: press gw_payout_all
Bot->>Bot: requireAdmin
Bot->>Admin: answerCbQuery Processing payouts...
Bot->>GiveawayStore: get history giveaways
GiveawayStore-->>Bot: history list
Bot->>Bot: filter unpaid real giveaways as targets
alt no targets
Bot->>Admin: reply No unpaid real giveaways found
else have targets
loop for each target giveaway
Bot->>Bot: set giveaway.paidOut = true
Bot->>Bot: increment giveawayCount
loop for each winner in giveaway.winners
Bot->>Bot: increment winnerCount
Bot->>TelegramAPI: sendMessage winner payout confirmation
TelegramAPI-->>Bot: success or error
end
Bot->>GiveawayStore: delete giveaway from history
end
Bot->>Admin: reply payout summary and logs wiped message
end
ER diagram for giveaways, users, and participants with test and weighting fieldserDiagram
Giveaway {
int id
string status
boolean testMode
boolean dryRun
boolean isTestGiveaway
boolean persistDisabled
boolean excludeFromLists
boolean paidOut
int maxWinners
int scPerWinner
int durationMinutes
int minParticipants
int chatId
string chatTitle
int pinnedMsgId
int announcementMsgId
}
User {
int id
string tgUsername
string firstName
string runewagerUsername
boolean verified
int boostExpiresAt
string giveawayHistory_json
}
Participant {
int userId
string tgUsername
string firstName
string runewagerUsername
boolean hasJoinedChannel
boolean hasJoinedGroup
int joinedAt
boolean isFake
int boostedWeight
}
Giveaway ||--o{ Participant : has_participants
User ||--o{ Participant : joins_as
Giveaway ||--o{ User : has_winners
Flow diagram for expanded Admin Giveaway Tools menu and actionsflowchart TD
A_AdminGiveawayToolsKeyboard["AdminGiveawayToolsKeyboard"]
A_AdminGiveawayToolsKeyboard --> B_StartGiveaway["Button: Start Giveaway (admin_cmd_start_giveaway)"]
A_AdminGiveawayToolsKeyboard --> C_GiveawayDefaults["Button: Giveaway Defaults (admin_gw_defaults)"]
A_AdminGiveawayToolsKeyboard --> D_EditGiveaway["Button: Edit Giveaway (admin_cmd_giveaway_status)"]
A_AdminGiveawayToolsKeyboard --> E_ExtendGiveaway["Button: Extend Giveaway (admin_cmd_giveaway_status)"]
A_AdminGiveawayToolsKeyboard --> F_EndGiveaway["Button: End Giveaway (admin_cmd_giveaway_status)"]
A_AdminGiveawayToolsKeyboard --> G_ViewParticipants["Button: View Participants (admin_cmd_giveaway_status)"]
A_AdminGiveawayToolsKeyboard --> H_GroupLinkingTools["Button: Group Linking Tools (admin_gw_group_linking)"]
A_AdminGiveawayToolsKeyboard --> I_AnnouncementBuilder["Button: Announcement Builder (admin_cmd_announce_start)"]
A_AdminGiveawayToolsKeyboard --> J_WinnerPayoutManager["Button: Winner Payout Manager (admin_gw_payout_manager)"]
A_AdminGiveawayToolsKeyboard --> K_RunTestGiveaway["Button: Run Test Giveaway (admin_cmd_testgiveaway)"]
A_AdminGiveawayToolsKeyboard --> L_GiveawayStatus["Button: Giveaway Status (admin_cmd_giveaway_status)"]
A_AdminGiveawayToolsKeyboard --> M_AdminDashboard["Button: Admin Dashboard (open_admin_dashboard)"]
A_AdminGiveawayToolsKeyboard --> N_MainMenu["Button: Main Menu (to_main_menu)"]
A_AdminGiveawayToolsKeyboard --> O_Cancel["Button: Cancel (to_main_menu)"]
C_GiveawayDefaults --> B_StartGiveaway
H_GroupLinkingTools --> K_RunTestGiveaway
J_WinnerPayoutManager --> P_PayoutAll["Action: gw_payout_all"]
K_RunTestGiveaway --> Q_TestConfirmPrompt["Prompt: Confirm Test Start (tgw_confirm_start)"]
File-Level Changes
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
There was a problem hiding this comment.
Hey - I've found 1 issue
Prompt for AI Agents
Please address the comments from this code review:
## Individual Comments
### Comment 1
<location path="index.js" line_range="11426-11434" />
<code_context>
}
- giveaway.winners = pickRandomUnique(participants, Math.min(giveaway.maxWinners, eligibleCount));
+ const weightedPool = [];
+ for (const participant of participants) {
+ const pUser = userStore.get(participant.userId);
+ const weight = pUser && pUser.boostExpiresAt > Date.now() ? 2 : 1;
+ for (let i = 0; i < weight; i += 1) weightedPool.push(participant);
+ }
+ const pickedIds = new Set();
+ const selected = [];
+ while (selected.length < Math.min(giveaway.maxWinners, eligibleCount) && weightedPool.length > 0) {
+ const candidate = weightedPool[Math.floor(Math.random() * weightedPool.length)];
+ if (!candidate || pickedIds.has(candidate.userId)) continue;
</code_context>
<issue_to_address>
**issue (bug_risk):** Weighted winner selection loop can get stuck when all remaining weighted entries are already picked.
In `finalizeGiveaway`, the loop draws from `weightedPool` but never removes entries. Once `pickedIds` contains every distinct `userId` in `weightedPool`, the `while` condition (`weightedPool.length > 0`) can remain true while every iteration hits the `continue`, so the loop may never terminate.
To avoid this, you should either remove the sampled element from `weightedPool` on each iteration, or pre-build a list of unique participants with associated weights and sample without replacement from that list. For example:
```js
const weightedPool = [];
for (const participant of participants) {
const pUser = userStore.get(participant.userId);
const weight = pUser && pUser.boostExpiresAt > Date.now() ? 2 : 1;
for (let i = 0; i < weight; i += 1) weightedPool.push(participant);
}
const target = Math.min(giveaway.maxWinners, eligibleCount);
const pickedIds = new Set();
const selected = [];
while (selected.length < target && weightedPool.length > 0) {
const idx = Math.floor(Math.random() * weightedPool.length);
const candidate = weightedPool[idx];
weightedPool.splice(idx, 1); // ensure progress
if (!candidate || pickedIds.has(candidate.userId)) continue;
selected.push(candidate);
pickedIds.add(candidate.userId);
}
giveaway.winners = selected;
```
</issue_to_address>Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
| const weightedPool = []; | ||
| for (const participant of participants) { | ||
| const pUser = userStore.get(participant.userId); | ||
| const weight = pUser && pUser.boostExpiresAt > Date.now() ? 2 : 1; | ||
| for (let i = 0; i < weight; i += 1) weightedPool.push(participant); | ||
| } | ||
| const pickedIds = new Set(); | ||
| const selected = []; | ||
| while (selected.length < Math.min(giveaway.maxWinners, eligibleCount) && weightedPool.length > 0) { |
There was a problem hiding this comment.
issue (bug_risk): Weighted winner selection loop can get stuck when all remaining weighted entries are already picked.
In finalizeGiveaway, the loop draws from weightedPool but never removes entries. Once pickedIds contains every distinct userId in weightedPool, the while condition (weightedPool.length > 0) can remain true while every iteration hits the continue, so the loop may never terminate.
To avoid this, you should either remove the sampled element from weightedPool on each iteration, or pre-build a list of unique participants with associated weights and sample without replacement from that list. For example:
const weightedPool = [];
for (const participant of participants) {
const pUser = userStore.get(participant.userId);
const weight = pUser && pUser.boostExpiresAt > Date.now() ? 2 : 1;
for (let i = 0; i < weight; i += 1) weightedPool.push(participant);
}
const target = Math.min(giveaway.maxWinners, eligibleCount);
const pickedIds = new Set();
const selected = [];
while (selected.length < target && weightedPool.length > 0) {
const idx = Math.floor(Math.random() * weightedPool.length);
const candidate = weightedPool[idx];
weightedPool.splice(idx, 1); // ensure progress
if (!candidate || pickedIds.has(candidate.userId)) continue;
selected.push(candidate);
pickedIds.add(candidate.userId);
}
giveaway.winners = selected;There was a problem hiding this comment.
Sure! I'm generating a new review now.
There was a problem hiding this comment.
Hey @gamblecodezcom, I've posted a new review for you!
There was a problem hiding this comment.
Hey - I've found 1 issue, and left some high level feedback:
- The referral weighting logic is now implemented in multiple places (e.g.,
gw_join_storingboostedWeight,finalizeGiveawayrecomputing fromuserStore, andrunTestGiveawayFinaleusingboostedWeight), which can drift over time; consider extracting a single helper for computing entry weights and using it consistently in both real and test winner selection. - Filtering out
testModegiveaways from snapshots and all the various listing surfaces is currently done with inline.filter((g) => !g.testMode)calls in several handlers; you might want to centralize this into a helper (or accessor ongiveawayStore) to reduce repetition and avoid missing a new surface in future changes.
Prompt for AI Agents
Please address the comments from this code review:
## Overall Comments
- The referral weighting logic is now implemented in multiple places (e.g., `gw_join_` storing `boostedWeight`, `finalizeGiveaway` recomputing from `userStore`, and `runTestGiveawayFinale` using `boostedWeight`), which can drift over time; consider extracting a single helper for computing entry weights and using it consistently in both real and test winner selection.
- Filtering out `testMode` giveaways from snapshots and all the various listing surfaces is currently done with inline `.filter((g) => !g.testMode)` calls in several handlers; you might want to centralize this into a helper (or accessor on `giveawayStore`) to reduce repetition and avoid missing a new surface in future changes.
## Individual Comments
### Comment 1
<location path="index.js" line_range="11219-11220" />
<code_context>
await bot.telegram.pinChatMessage(giveaway.chatId, sent.message_id, { disable_notification: true });
giveaway.pinnedMsgId = sent.message_id;
- } catch (_) { /* no pin permission — not fatal */ }
+ } catch (e) {
+ await notifyAdmins(`⚠️ Failed to pin giveaway #${giveaway.id} in chat ${giveaway.chatId}: ${e.message}`);
+ }
}
</code_context>
<issue_to_address>
**issue (bug_risk):** Pin failure now not only gets logged but also awaits `notifyAdmins`, which could cause extra failures or noise.
This change makes a previously non-fatal pin failure effectively fatal by awaiting `notifyAdmins` inside the `catch`:
- If `notifyAdmins` throws (e.g. network/rate limits), `announceGiveaway` will now reject on a pin-permission issue.
- Chats without pin permission could generate a lot of admin noise.
If you want to preserve the non-blocking behavior, you could treat admin notification as best-effort:
```js
} catch (e) {
notifyAdmins(`⚠️ Failed to pin giveaway #${giveaway.id} in chat ${giveaway.chatId}: ${e.message}`).catch(() => {});
}
```
so pin/admin-notify failures don’t break the main flow.
</issue_to_address>Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
| } catch (e) { | ||
| await notifyAdmins(`⚠️ Failed to pin giveaway #${giveaway.id} in chat ${giveaway.chatId}: ${e.message}`); |
There was a problem hiding this comment.
issue (bug_risk): Pin failure now not only gets logged but also awaits notifyAdmins, which could cause extra failures or noise.
This change makes a previously non-fatal pin failure effectively fatal by awaiting notifyAdmins inside the catch:
- If
notifyAdminsthrows (e.g. network/rate limits),announceGiveawaywill now reject on a pin-permission issue. - Chats without pin permission could generate a lot of admin noise.
If you want to preserve the non-blocking behavior, you could treat admin notification as best-effort:
} catch (e) {
notifyAdmins(`⚠️ Failed to pin giveaway #${giveaway.id} in chat ${giveaway.chatId}: ${e.message}`).catch(() => {});
}so pin/admin-notify failures don’t break the main flow.
User description
Motivation
Description
Giveaway Defaults,Edit/Extend/End,View Participants,Group Linking Tools,Announcement Builder,Winner Payout Manager, andRun Test Giveaway(all buttons/callbacks wired).gw_payout_all) for real giveaways only that DMs winners, marks giveaways as paid, and wipes real-giveaway logs after payout.Join Giveaway+Open Botbuttons, helpful tips block, referral boost explanation, live joined count and countdown, explicit pin attempts with admin error notifications, and announcement refresh in test mode.testModegiveaways out of snapshots and feeds.RUNEWAGER_FUNCTIONALITY_MAP.mdto reflect all new menu entries, callbacks, handlers, timeouts, and test/payout behaviors.index.js(core implementation) andRUNEWAGER_FUNCTIONALITY_MAP.md(map sync).Testing
npm run check, which passed successfully.npm test; all tests passed (TAP: 35 tests, 35 passed).npm test).Codex Task
CodeAnt-AI Description
Complete giveaway admin tools plus safe test giveaways and bulk payout manager
What Changed
Impact
✅ Clearer test giveaway isolation — tests never affect real users or payout records✅ Clearer join guidance — prompts to open bot and link account reduce failed joins✅ Fewer unpaid payout bookkeeping issues — bulk payout marks as paid and cleans logs💡 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:
This lets you have a chat with CodeAnt AI about your pull request, making it easier to understand and improve your code.
Example
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:
This helps CodeAnt AI learn and adapt to your team's coding style and standards.
Example
Retrigger review
Ask CodeAnt AI to review the PR again, by typing:
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 Sourcery
Upgrade the giveaway system with richer admin controls, safe test giveaways, improved user join/announcement UX, and a bulk payout manager while keeping test data fully isolated from production.
New Features:
Enhancements:
Documentation: