Fix draw notification spam and winning ticket number

- Group participants by telegramId to send only ONE message per user
- First pass finds the winning ticket info before sending any messages
- Loser messages now show the actual winning ticket number instead of #0000
- Unique participant count used for group announcements
This commit is contained in:
Michilis
2025-12-09 01:15:40 +00:00
parent fcd180b7a4
commit fb378d56c5

View File

@@ -136,86 +136,112 @@ class NotificationScheduler {
// Get participants for the previous cycle // Get participants for the previous cycle
const participants = await stateManager.getCycleParticipants(previousCycleId); const participants = await stateManager.getCycleParticipants(previousCycleId);
const hasParticipants = participants.length > 0;
logger.info('Processing previous cycle completion', { logger.info('Processing previous cycle completion', {
cycleId: previousCycleId, cycleId: previousCycleId,
participantCount: participants.length participantCount: participants.length
}); });
// Get draw result details for winner announcement if (participants.length === 0) {
return;
}
// Group participants by telegramId to avoid spamming users with multiple purchases
const userPurchases = new Map<number, string[]>();
for (const participant of participants) {
const existing = userPurchases.get(participant.telegramId) || [];
existing.push(participant.purchaseId);
userPurchases.set(participant.telegramId, existing);
}
// First pass: Find the winning ticket info and pot amount
let winnerDisplayName = 'Anon'; let winnerDisplayName = 'Anon';
let winnerTicketNumber = '0000'; let winnerTicketNumber = '0000';
let winnerTelegramId: number | null = null;
let potSats = 0; let potSats = 0;
let prizeSats = 0;
// Notify each participant about their result let payoutStatus = 'processing';
for (const participant of participants) {
try {
const user = await stateManager.getUser(participant.telegramId);
if (!user) continue;
// Check if they won for (const [telegramId, purchaseIds] of userPurchases) {
const status = await apiClient.getTicketStatus(participant.purchaseId); for (const purchaseId of purchaseIds) {
if (!status) continue; try {
const status = await apiClient.getTicketStatus(purchaseId);
if (!status) continue;
potSats = status.cycle.pot_total_sats || 0; potSats = status.cycle.pot_total_sats || 0;
const isWinner = status.result.is_winner;
if (isWinner) {
// Store winner info for group announcement
winnerDisplayName = user.displayName || 'Anon';
const winningTicket = status.tickets.find(t => t.is_winning_ticket);
if (winningTicket) {
winnerTicketNumber = winningTicket.serial_number.toString().padStart(4, '0');
}
// Send winner notification if user has drawResults enabled if (status.result.is_winner) {
if (user.notifications?.drawResults !== false) { const user = await stateManager.getUser(telegramId);
const prizeSats = status.result.payout?.amount_sats || potSats; winnerTelegramId = telegramId;
const payoutStatus = status.result.payout?.status || 'processing'; winnerDisplayName = user?.displayName || 'Anon';
await this.bot.sendMessage(
participant.telegramId, const winningTicket = status.tickets.find(t => t.is_winning_ticket);
messages.notifications.winner( if (winningTicket) {
prizeSats.toLocaleString(), winnerTicketNumber = winningTicket.serial_number.toString().padStart(4, '0');
winnerTicketNumber, }
payoutStatus
), prizeSats = status.result.payout?.amount_sats || potSats;
{ parse_mode: 'Markdown' } payoutStatus = status.result.payout?.status || 'processing';
); break; // Found winner, stop searching
logger.info('Sent winner notification', { telegramId: participant.telegramId });
} }
} catch (error) {
logger.error('Error checking purchase status', { purchaseId, error });
}
}
if (winnerTelegramId) break; // Found winner, stop searching
}
// Second pass: Send ONE notification per user
const notifiedUsers = new Set<number>();
for (const [telegramId, purchaseIds] of userPurchases) {
if (notifiedUsers.has(telegramId)) continue;
notifiedUsers.add(telegramId);
try {
const user = await stateManager.getUser(telegramId);
if (!user || user.notifications?.drawResults === false) continue;
const isWinner = telegramId === winnerTelegramId;
if (isWinner) {
// Send winner notification
await this.bot.sendMessage(
telegramId,
messages.notifications.winner(
prizeSats.toLocaleString(),
winnerTicketNumber,
payoutStatus
),
{ parse_mode: 'Markdown' }
);
logger.info('Sent winner notification', { telegramId });
} else { } else {
// Send loser notification if user has drawResults enabled // Send loser notification with actual winning ticket number
if (user.notifications?.drawResults !== false) { await this.bot.sendMessage(
await this.bot.sendMessage( telegramId,
participant.telegramId, messages.notifications.loser(
messages.notifications.loser( winnerTicketNumber,
winnerTicketNumber, potSats.toLocaleString()
potSats.toLocaleString() ),
), { parse_mode: 'Markdown' }
{ parse_mode: 'Markdown' } );
); logger.debug('Sent draw result to participant', { telegramId });
logger.debug('Sent draw result to participant', { telegramId: participant.telegramId });
}
} }
} catch (error) { } catch (error) {
logger.error('Failed to notify participant', { logger.error('Failed to notify participant', { telegramId, error });
telegramId: participant.telegramId,
error
});
} }
} }
// Send group announcements (even if no participants - groups might want to know) // Send group announcements
if (hasParticipants) { const uniqueUserCount = userPurchases.size;
await this.sendGroupDrawAnnouncementsImmediate( await this.sendGroupDrawAnnouncementsImmediate(
previousCycleId, previousCycleId,
winnerDisplayName, winnerDisplayName,
winnerTicketNumber, winnerTicketNumber,
potSats, potSats,
participants.length uniqueUserCount
); );
}
} }
/** /**
@@ -333,7 +359,6 @@ class NotificationScheduler {
// Get participants for this cycle // Get participants for this cycle
const participants = await stateManager.getCycleParticipants(cycle.id); const participants = await stateManager.getCycleParticipants(cycle.id);
const hasParticipants = participants.length > 0;
logger.info('Processing draw completion', { logger.info('Processing draw completion', {
cycleId: cycle.id, cycleId: cycle.id,
@@ -342,40 +367,70 @@ class NotificationScheduler {
}); });
// Only proceed if there were participants // Only proceed if there were participants
if (!hasParticipants) { if (participants.length === 0) {
logger.info('No participants in cycle, skipping notifications', { cycleId: cycle.id }); logger.info('No participants in cycle, skipping notifications', { cycleId: cycle.id });
return; return;
} }
// Get draw result details for winner announcement // Group participants by telegramId to avoid spamming users with multiple purchases
const userPurchases = new Map<number, string[]>();
for (const participant of participants) {
const existing = userPurchases.get(participant.telegramId) || [];
existing.push(participant.purchaseId);
userPurchases.set(participant.telegramId, existing);
}
// First pass: Find the winning ticket info
let winnerDisplayName = 'Anon'; let winnerDisplayName = 'Anon';
let winnerTicketNumber = '0000'; let winnerTicketNumber = '0000';
let winnerTelegramId: number | null = null;
// Notify each participant about their result let prizeSats = 0;
for (const participant of participants) { let payoutStatus = 'processing';
try {
const user = await stateManager.getUser(participant.telegramId);
if (!user || !user.notifications?.drawResults) continue;
// Check if they won for (const [telegramId, purchaseIds] of userPurchases) {
const status = await apiClient.getTicketStatus(participant.purchaseId); for (const purchaseId of purchaseIds) {
if (!status) continue; try {
const status = await apiClient.getTicketStatus(purchaseId);
const isWinner = status.result.is_winner; if (!status) continue;
if (isWinner) {
// Store winner info for group announcement
winnerDisplayName = user.displayName || 'Anon';
const winningTicket = status.tickets.find(t => t.is_winning_ticket);
if (winningTicket) {
winnerTicketNumber = winningTicket.serial_number.toString().padStart(4, '0');
}
if (status.result.is_winner) {
const user = await stateManager.getUser(telegramId);
winnerTelegramId = telegramId;
winnerDisplayName = user?.displayName || 'Anon';
const winningTicket = status.tickets.find(t => t.is_winning_ticket);
if (winningTicket) {
winnerTicketNumber = winningTicket.serial_number.toString().padStart(4, '0');
}
prizeSats = status.result.payout?.amount_sats || cycle.pot_total_sats;
payoutStatus = status.result.payout?.status || 'processing';
break; // Found winner, stop searching
}
} catch (error) {
logger.error('Error checking purchase status', { purchaseId, error });
}
}
if (winnerTelegramId) break; // Found winner, stop searching
}
// Second pass: Send ONE notification per user
const notifiedUsers = new Set<number>();
for (const [telegramId] of userPurchases) {
if (notifiedUsers.has(telegramId)) continue;
notifiedUsers.add(telegramId);
try {
const user = await stateManager.getUser(telegramId);
if (!user || user.notifications?.drawResults === false) continue;
const isWinner = telegramId === winnerTelegramId;
if (isWinner) {
// Send winner notification // Send winner notification
const prizeSats = status.result.payout?.amount_sats || cycle.pot_total_sats;
const payoutStatus = status.result.payout?.status || 'processing';
await this.bot.sendMessage( await this.bot.sendMessage(
participant.telegramId, telegramId,
messages.notifications.winner( messages.notifications.winner(
prizeSats.toLocaleString(), prizeSats.toLocaleString(),
winnerTicketNumber, winnerTicketNumber,
@@ -383,29 +438,27 @@ class NotificationScheduler {
), ),
{ parse_mode: 'Markdown' } { parse_mode: 'Markdown' }
); );
logger.info('Sent winner notification', { telegramId: participant.telegramId }); logger.info('Sent winner notification', { telegramId });
} else { } else {
// Send loser notification // Send loser notification with actual winning ticket number
await this.bot.sendMessage( await this.bot.sendMessage(
participant.telegramId, telegramId,
messages.notifications.loser( messages.notifications.loser(
winnerTicketNumber, winnerTicketNumber,
cycle.pot_total_sats.toLocaleString() cycle.pot_total_sats.toLocaleString()
), ),
{ parse_mode: 'Markdown' } { parse_mode: 'Markdown' }
); );
logger.debug('Sent draw result to participant', { telegramId: participant.telegramId }); logger.debug('Sent draw result to participant', { telegramId });
} }
} catch (error) { } catch (error) {
logger.error('Failed to notify participant', { logger.error('Failed to notify participant', { telegramId, error });
telegramId: participant.telegramId,
error
});
} }
} }
// Send group announcements (only if there were participants) // Send group announcements
await this.sendGroupDrawAnnouncements(cycle, winnerDisplayName, winnerTicketNumber, participants.length); const uniqueUserCount = userPurchases.size;
await this.sendGroupDrawAnnouncements(cycle, winnerDisplayName, winnerTicketNumber, uniqueUserCount);
} }
/** /**