From fb378d56c5806a629f80c5b5835ff95dabcdca7b Mon Sep 17 00:00:00 2001 From: Michilis Date: Tue, 9 Dec 2025 01:15:40 +0000 Subject: [PATCH] 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 --- .../src/services/notificationScheduler.ts | 245 +++++++++++------- 1 file changed, 149 insertions(+), 96 deletions(-) diff --git a/telegram_bot/src/services/notificationScheduler.ts b/telegram_bot/src/services/notificationScheduler.ts index d87159d..5021334 100644 --- a/telegram_bot/src/services/notificationScheduler.ts +++ b/telegram_bot/src/services/notificationScheduler.ts @@ -136,86 +136,112 @@ class NotificationScheduler { // Get participants for the previous cycle const participants = await stateManager.getCycleParticipants(previousCycleId); - const hasParticipants = participants.length > 0; logger.info('Processing previous cycle completion', { cycleId: previousCycleId, 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(); + 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 winnerTicketNumber = '0000'; + let winnerTelegramId: number | null = null; let potSats = 0; - - // Notify each participant about their result - for (const participant of participants) { - try { - const user = await stateManager.getUser(participant.telegramId); - if (!user) continue; + let prizeSats = 0; + let payoutStatus = 'processing'; - // Check if they won - const status = await apiClient.getTicketStatus(participant.purchaseId); - if (!status) continue; + for (const [telegramId, purchaseIds] of userPurchases) { + for (const purchaseId of purchaseIds) { + try { + const status = await apiClient.getTicketStatus(purchaseId); + if (!status) continue; - 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'); - } + potSats = status.cycle.pot_total_sats || 0; - // Send winner notification if user has drawResults enabled - if (user.notifications?.drawResults !== false) { - const prizeSats = status.result.payout?.amount_sats || potSats; - const payoutStatus = status.result.payout?.status || 'processing'; - await this.bot.sendMessage( - participant.telegramId, - messages.notifications.winner( - prizeSats.toLocaleString(), - winnerTicketNumber, - payoutStatus - ), - { parse_mode: 'Markdown' } - ); - logger.info('Sent winner notification', { telegramId: participant.telegramId }); + 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 || potSats; + 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(); + + 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 { - // Send loser notification if user has drawResults enabled - if (user.notifications?.drawResults !== false) { - await this.bot.sendMessage( - participant.telegramId, - messages.notifications.loser( - winnerTicketNumber, - potSats.toLocaleString() - ), - { parse_mode: 'Markdown' } - ); - logger.debug('Sent draw result to participant', { telegramId: participant.telegramId }); - } + // Send loser notification with actual winning ticket number + await this.bot.sendMessage( + telegramId, + messages.notifications.loser( + winnerTicketNumber, + potSats.toLocaleString() + ), + { parse_mode: 'Markdown' } + ); + logger.debug('Sent draw result to participant', { telegramId }); } } catch (error) { - logger.error('Failed to notify participant', { - telegramId: participant.telegramId, - error - }); + logger.error('Failed to notify participant', { telegramId, error }); } } - // Send group announcements (even if no participants - groups might want to know) - if (hasParticipants) { - await this.sendGroupDrawAnnouncementsImmediate( - previousCycleId, - winnerDisplayName, - winnerTicketNumber, - potSats, - participants.length - ); - } + // Send group announcements + const uniqueUserCount = userPurchases.size; + await this.sendGroupDrawAnnouncementsImmediate( + previousCycleId, + winnerDisplayName, + winnerTicketNumber, + potSats, + uniqueUserCount + ); } /** @@ -333,7 +359,6 @@ class NotificationScheduler { // Get participants for this cycle const participants = await stateManager.getCycleParticipants(cycle.id); - const hasParticipants = participants.length > 0; logger.info('Processing draw completion', { cycleId: cycle.id, @@ -342,40 +367,70 @@ class NotificationScheduler { }); // Only proceed if there were participants - if (!hasParticipants) { + if (participants.length === 0) { logger.info('No participants in cycle, skipping notifications', { cycleId: cycle.id }); return; } - // Get draw result details for winner announcement + // Group participants by telegramId to avoid spamming users with multiple purchases + const userPurchases = new Map(); + 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 winnerTicketNumber = '0000'; - - // Notify each participant about their result - for (const participant of participants) { - try { - const user = await stateManager.getUser(participant.telegramId); - if (!user || !user.notifications?.drawResults) continue; + let winnerTelegramId: number | null = null; + let prizeSats = 0; + let payoutStatus = 'processing'; - // Check if they won - const status = await apiClient.getTicketStatus(participant.purchaseId); - if (!status) continue; - - 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'); - } + for (const [telegramId, purchaseIds] of userPurchases) { + for (const purchaseId of purchaseIds) { + try { + const status = await apiClient.getTicketStatus(purchaseId); + if (!status) continue; + 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(); + + 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 - const prizeSats = status.result.payout?.amount_sats || cycle.pot_total_sats; - const payoutStatus = status.result.payout?.status || 'processing'; await this.bot.sendMessage( - participant.telegramId, + telegramId, messages.notifications.winner( prizeSats.toLocaleString(), winnerTicketNumber, @@ -383,29 +438,27 @@ class NotificationScheduler { ), { parse_mode: 'Markdown' } ); - logger.info('Sent winner notification', { telegramId: participant.telegramId }); + logger.info('Sent winner notification', { telegramId }); } else { - // Send loser notification + // Send loser notification with actual winning ticket number await this.bot.sendMessage( - participant.telegramId, + telegramId, messages.notifications.loser( winnerTicketNumber, cycle.pot_total_sats.toLocaleString() ), { parse_mode: 'Markdown' } ); - logger.debug('Sent draw result to participant', { telegramId: participant.telegramId }); + logger.debug('Sent draw result to participant', { telegramId }); } } catch (error) { - logger.error('Failed to notify participant', { - telegramId: participant.telegramId, - error - }); + logger.error('Failed to notify participant', { telegramId, error }); } } - // Send group announcements (only if there were participants) - await this.sendGroupDrawAnnouncements(cycle, winnerDisplayName, winnerTicketNumber, participants.length); + // Send group announcements + const uniqueUserCount = userPurchases.size; + await this.sendGroupDrawAnnouncements(cycle, winnerDisplayName, winnerTicketNumber, uniqueUserCount); } /**