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:
@@ -136,45 +136,78 @@ 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;
|
||||||
|
let payoutStatus = 'processing';
|
||||||
|
|
||||||
// Notify each participant about their result
|
for (const [telegramId, purchaseIds] of userPurchases) {
|
||||||
for (const participant of participants) {
|
for (const purchaseId of purchaseIds) {
|
||||||
try {
|
try {
|
||||||
const user = await stateManager.getUser(participant.telegramId);
|
const status = await apiClient.getTicketStatus(purchaseId);
|
||||||
if (!user) continue;
|
|
||||||
|
|
||||||
// Check if they won
|
|
||||||
const status = await apiClient.getTicketStatus(participant.purchaseId);
|
|
||||||
if (!status) continue;
|
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) {
|
if (status.result.is_winner) {
|
||||||
// Store winner info for group announcement
|
const user = await stateManager.getUser(telegramId);
|
||||||
winnerDisplayName = user.displayName || 'Anon';
|
winnerTelegramId = telegramId;
|
||||||
|
winnerDisplayName = user?.displayName || 'Anon';
|
||||||
|
|
||||||
const winningTicket = status.tickets.find(t => t.is_winning_ticket);
|
const winningTicket = status.tickets.find(t => t.is_winning_ticket);
|
||||||
if (winningTicket) {
|
if (winningTicket) {
|
||||||
winnerTicketNumber = winningTicket.serial_number.toString().padStart(4, '0');
|
winnerTicketNumber = winningTicket.serial_number.toString().padStart(4, '0');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Send winner notification if user has drawResults enabled
|
prizeSats = status.result.payout?.amount_sats || potSats;
|
||||||
if (user.notifications?.drawResults !== false) {
|
payoutStatus = status.result.payout?.status || 'processing';
|
||||||
const prizeSats = status.result.payout?.amount_sats || potSats;
|
break; // Found winner, stop searching
|
||||||
const payoutStatus = status.result.payout?.status || 'processing';
|
}
|
||||||
|
} 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(
|
await this.bot.sendMessage(
|
||||||
participant.telegramId,
|
telegramId,
|
||||||
messages.notifications.winner(
|
messages.notifications.winner(
|
||||||
prizeSats.toLocaleString(),
|
prizeSats.toLocaleString(),
|
||||||
winnerTicketNumber,
|
winnerTicketNumber,
|
||||||
@@ -182,41 +215,34 @@ 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 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(
|
||||||
participant.telegramId,
|
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: 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 (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
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Send draw announcements to groups immediately (no delay - for cycle transition)
|
* Send draw announcements to groups immediately (no delay - for cycle transition)
|
||||||
@@ -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;
|
||||||
|
let prizeSats = 0;
|
||||||
|
let payoutStatus = 'processing';
|
||||||
|
|
||||||
// Notify each participant about their result
|
for (const [telegramId, purchaseIds] of userPurchases) {
|
||||||
for (const participant of participants) {
|
for (const purchaseId of purchaseIds) {
|
||||||
try {
|
try {
|
||||||
const user = await stateManager.getUser(participant.telegramId);
|
const status = await apiClient.getTicketStatus(purchaseId);
|
||||||
if (!user || !user.notifications?.drawResults) continue;
|
|
||||||
|
|
||||||
// Check if they won
|
|
||||||
const status = await apiClient.getTicketStatus(participant.purchaseId);
|
|
||||||
if (!status) continue;
|
if (!status) continue;
|
||||||
|
|
||||||
const isWinner = status.result.is_winner;
|
if (status.result.is_winner) {
|
||||||
|
const user = await stateManager.getUser(telegramId);
|
||||||
|
winnerTelegramId = telegramId;
|
||||||
|
winnerDisplayName = user?.displayName || 'Anon';
|
||||||
|
|
||||||
if (isWinner) {
|
|
||||||
// Store winner info for group announcement
|
|
||||||
winnerDisplayName = user.displayName || 'Anon';
|
|
||||||
const winningTicket = status.tickets.find(t => t.is_winning_ticket);
|
const winningTicket = status.tickets.find(t => t.is_winning_ticket);
|
||||||
if (winningTicket) {
|
if (winningTicket) {
|
||||||
winnerTicketNumber = winningTicket.serial_number.toString().padStart(4, '0');
|
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);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
Reference in New Issue
Block a user