Update API client and notification scheduler services
This commit is contained in:
@@ -157,6 +157,32 @@ class ApiClient {
|
||||
return { enabled: false, pending: false, message: null };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get past wins (for group announcements)
|
||||
*/
|
||||
async getPastWins(limit: number = 1, offset: number = 0): Promise<PastWin[]> {
|
||||
try {
|
||||
const response = await this.client.get<ApiResponse<{ wins: PastWin[] }>>(
|
||||
`/jackpot/past-wins?limit=${limit}&offset=${offset}`
|
||||
);
|
||||
return response.data.data.wins || [];
|
||||
} catch (error) {
|
||||
logger.error('Failed to get past wins', { error });
|
||||
return [];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export interface PastWin {
|
||||
cycle_id: string;
|
||||
cycle_type: string;
|
||||
scheduled_at: string;
|
||||
pot_total_sats: number;
|
||||
pot_after_fee_sats: number | null;
|
||||
winner_name: string;
|
||||
winner_address: string | null;
|
||||
winning_ticket_serial: number | null;
|
||||
}
|
||||
|
||||
export const apiClient = new ApiClient();
|
||||
|
||||
@@ -122,6 +122,7 @@ class NotificationScheduler {
|
||||
|
||||
/**
|
||||
* Handle previous cycle completion when we detect a new cycle
|
||||
* Always sends group announcements, even if no participants tracked locally
|
||||
*/
|
||||
private async handlePreviousCycleCompleted(previousCycleId: string): Promise<void> {
|
||||
if (!this.bot) return;
|
||||
@@ -135,19 +136,41 @@ class NotificationScheduler {
|
||||
// Clear reminders for the old cycle
|
||||
this.clearRemindersForCycle(previousCycleId);
|
||||
|
||||
// Get participants for the previous cycle
|
||||
const participants = await stateManager.getCycleParticipants(previousCycleId);
|
||||
logger.info('Processing previous cycle completion', { cycleId: previousCycleId });
|
||||
|
||||
logger.info('Processing previous cycle completion', {
|
||||
// Fetch winner info from API (works even if no participants tracked locally)
|
||||
let winnerDisplayName = 'Anon';
|
||||
let winnerTicketNumber = '0000';
|
||||
let winnerLightningAddress = '';
|
||||
let potSats = 0;
|
||||
let totalTickets = 0;
|
||||
|
||||
try {
|
||||
const pastWins = await apiClient.getPastWins(1, 0);
|
||||
// Try to find this specific cycle, or use the latest
|
||||
const latestWin = pastWins.find(w => w.cycle_id === previousCycleId) || pastWins[0];
|
||||
|
||||
if (latestWin) {
|
||||
winnerDisplayName = latestWin.winner_name || 'Anon';
|
||||
winnerLightningAddress = latestWin.winner_address || '';
|
||||
potSats = latestWin.pot_total_sats || 0;
|
||||
if (latestWin.winning_ticket_serial !== null) {
|
||||
winnerTicketNumber = latestWin.winning_ticket_serial.toString().padStart(4, '0');
|
||||
}
|
||||
logger.info('Got winner info from API', {
|
||||
cycleId: previousCycleId,
|
||||
participantCount: participants.length
|
||||
winnerName: winnerDisplayName,
|
||||
potSats
|
||||
});
|
||||
|
||||
if (participants.length === 0) {
|
||||
return;
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('Failed to fetch past wins for announcement', { error, cycleId: previousCycleId });
|
||||
}
|
||||
|
||||
// Group participants by telegramId to avoid spamming users with multiple purchases
|
||||
// Get participants for DM notifications
|
||||
const participants = await stateManager.getCycleParticipants(previousCycleId);
|
||||
|
||||
// Group participants by telegramId
|
||||
const userPurchases = new Map<number, string[]>();
|
||||
for (const participant of participants) {
|
||||
const existing = userPurchases.get(participant.telegramId) || [];
|
||||
@@ -155,13 +178,11 @@ class NotificationScheduler {
|
||||
userPurchases.set(participant.telegramId, existing);
|
||||
}
|
||||
|
||||
// First pass: Find the winning ticket info and pot amount
|
||||
let winnerDisplayName = 'Anon';
|
||||
let winnerTicketNumber = '0000';
|
||||
totalTickets = userPurchases.size;
|
||||
|
||||
// Find winner among tracked participants for DM notifications
|
||||
let winnerTelegramId: number | null = null;
|
||||
let winnerLightningAddress = '';
|
||||
let potSats = 0;
|
||||
let prizeSats = 0;
|
||||
let prizeSats = potSats;
|
||||
let payoutStatus = 'processing';
|
||||
|
||||
for (const [telegramId, purchaseIds] of userPurchases) {
|
||||
@@ -170,13 +191,17 @@ class NotificationScheduler {
|
||||
const status = await apiClient.getTicketStatus(purchaseId);
|
||||
if (!status) continue;
|
||||
|
||||
potSats = status.cycle.pot_total_sats || 0;
|
||||
if (status.cycle.pot_total_sats) {
|
||||
potSats = status.cycle.pot_total_sats;
|
||||
}
|
||||
|
||||
if (status.result.is_winner) {
|
||||
const user = await stateManager.getUser(telegramId);
|
||||
winnerTelegramId = telegramId;
|
||||
winnerDisplayName = user ? stateManager.getDisplayName(user) : 'Anon';
|
||||
winnerLightningAddress = status.purchase.lightning_address || '';
|
||||
if (user) {
|
||||
winnerDisplayName = stateManager.getDisplayName(user);
|
||||
}
|
||||
winnerLightningAddress = status.purchase.lightning_address || winnerLightningAddress;
|
||||
|
||||
const winningTicket = status.tickets.find(t => t.is_winning_ticket);
|
||||
if (winningTicket) {
|
||||
@@ -185,16 +210,16 @@ class NotificationScheduler {
|
||||
|
||||
prizeSats = status.result.payout?.amount_sats || potSats;
|
||||
payoutStatus = status.result.payout?.status || 'processing';
|
||||
break; // Found winner, stop searching
|
||||
break;
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('Error checking purchase status', { purchaseId, error });
|
||||
}
|
||||
}
|
||||
if (winnerTelegramId) break; // Found winner, stop searching
|
||||
if (winnerTelegramId) break;
|
||||
}
|
||||
|
||||
// Second pass: Send ONE notification per user
|
||||
// Send DM notifications to tracked participants
|
||||
const notifiedUsers = new Set<number>();
|
||||
|
||||
for (const [telegramId, purchaseIds] of userPurchases) {
|
||||
@@ -208,7 +233,6 @@ class NotificationScheduler {
|
||||
const isWinner = telegramId === winnerTelegramId;
|
||||
|
||||
if (isWinner) {
|
||||
// Send winner notification
|
||||
await this.bot.sendMessage(
|
||||
telegramId,
|
||||
messages.notifications.winner(
|
||||
@@ -220,7 +244,6 @@ class NotificationScheduler {
|
||||
);
|
||||
logger.info('Sent winner notification', { telegramId });
|
||||
} else {
|
||||
// Send loser notification with actual winning ticket number
|
||||
await this.bot.sendMessage(
|
||||
telegramId,
|
||||
messages.notifications.loser(
|
||||
@@ -236,14 +259,13 @@ class NotificationScheduler {
|
||||
}
|
||||
}
|
||||
|
||||
// Send group announcements
|
||||
const uniqueUserCount = userPurchases.size;
|
||||
// ALWAYS send group announcements (regardless of tracked participants)
|
||||
await this.sendGroupDrawAnnouncementsImmediate(
|
||||
previousCycleId,
|
||||
winnerDisplayName,
|
||||
winnerTicketNumber,
|
||||
potSats,
|
||||
uniqueUserCount,
|
||||
totalTickets,
|
||||
winnerLightningAddress
|
||||
);
|
||||
}
|
||||
@@ -351,6 +373,7 @@ class NotificationScheduler {
|
||||
|
||||
/**
|
||||
* Handle draw completed - send notifications to participants and groups
|
||||
* Always sends group announcements, even if no participants tracked locally
|
||||
*/
|
||||
private async handleDrawCompleted(cycle: CycleInfo): Promise<void> {
|
||||
if (!this.bot) return;
|
||||
@@ -364,22 +387,43 @@ class NotificationScheduler {
|
||||
// Clear reminders for this cycle
|
||||
this.clearRemindersForCycle(cycle.id);
|
||||
|
||||
// Get participants for this cycle
|
||||
const participants = await stateManager.getCycleParticipants(cycle.id);
|
||||
|
||||
logger.info('Processing draw completion', {
|
||||
cycleId: cycle.id,
|
||||
participantCount: participants.length,
|
||||
potSats: cycle.pot_total_sats
|
||||
});
|
||||
|
||||
// Only proceed if there were participants
|
||||
if (participants.length === 0) {
|
||||
logger.info('No participants in cycle, skipping notifications', { cycleId: cycle.id });
|
||||
return;
|
||||
// Fetch winner info from API (works even if no participants tracked locally)
|
||||
let winnerDisplayName = 'Anon';
|
||||
let winnerTicketNumber = '0000';
|
||||
let winnerLightningAddress = '';
|
||||
let potSats = cycle.pot_total_sats;
|
||||
let totalTickets = 0;
|
||||
|
||||
try {
|
||||
const pastWins = await apiClient.getPastWins(1, 0);
|
||||
const latestWin = pastWins.find(w => w.cycle_id === cycle.id) || pastWins[0];
|
||||
|
||||
if (latestWin) {
|
||||
winnerDisplayName = latestWin.winner_name || 'Anon';
|
||||
winnerLightningAddress = latestWin.winner_address || '';
|
||||
potSats = latestWin.pot_total_sats || cycle.pot_total_sats;
|
||||
if (latestWin.winning_ticket_serial !== null) {
|
||||
winnerTicketNumber = latestWin.winning_ticket_serial.toString().padStart(4, '0');
|
||||
}
|
||||
logger.info('Got winner info from API', {
|
||||
cycleId: cycle.id,
|
||||
winnerName: winnerDisplayName,
|
||||
potSats
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('Failed to fetch past wins for announcement', { error, cycleId: cycle.id });
|
||||
}
|
||||
|
||||
// Group participants by telegramId to avoid spamming users with multiple purchases
|
||||
// Get participants for DM notifications
|
||||
const participants = await stateManager.getCycleParticipants(cycle.id);
|
||||
|
||||
// Group participants by telegramId
|
||||
const userPurchases = new Map<number, string[]>();
|
||||
for (const participant of participants) {
|
||||
const existing = userPurchases.get(participant.telegramId) || [];
|
||||
@@ -387,12 +431,11 @@ class NotificationScheduler {
|
||||
userPurchases.set(participant.telegramId, existing);
|
||||
}
|
||||
|
||||
// First pass: Find the winning ticket info
|
||||
let winnerDisplayName = 'Anon';
|
||||
let winnerTicketNumber = '0000';
|
||||
totalTickets = userPurchases.size;
|
||||
|
||||
// Find winner among tracked participants for DM notifications
|
||||
let winnerTelegramId: number | null = null;
|
||||
let winnerLightningAddress = '';
|
||||
let prizeSats = 0;
|
||||
let prizeSats = potSats;
|
||||
let payoutStatus = 'processing';
|
||||
|
||||
for (const [telegramId, purchaseIds] of userPurchases) {
|
||||
@@ -404,26 +447,28 @@ class NotificationScheduler {
|
||||
if (status.result.is_winner) {
|
||||
const user = await stateManager.getUser(telegramId);
|
||||
winnerTelegramId = telegramId;
|
||||
winnerDisplayName = user ? stateManager.getDisplayName(user) : 'Anon';
|
||||
winnerLightningAddress = status.purchase.lightning_address || '';
|
||||
if (user) {
|
||||
winnerDisplayName = stateManager.getDisplayName(user);
|
||||
}
|
||||
winnerLightningAddress = status.purchase.lightning_address || winnerLightningAddress;
|
||||
|
||||
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;
|
||||
prizeSats = status.result.payout?.amount_sats || potSats;
|
||||
payoutStatus = status.result.payout?.status || 'processing';
|
||||
break; // Found winner, stop searching
|
||||
break;
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('Error checking purchase status', { purchaseId, error });
|
||||
}
|
||||
}
|
||||
if (winnerTelegramId) break; // Found winner, stop searching
|
||||
if (winnerTelegramId) break;
|
||||
}
|
||||
|
||||
// Second pass: Send ONE notification per user
|
||||
// Send DM notifications to tracked participants
|
||||
const notifiedUsers = new Set<number>();
|
||||
|
||||
for (const [telegramId] of userPurchases) {
|
||||
@@ -437,7 +482,6 @@ class NotificationScheduler {
|
||||
const isWinner = telegramId === winnerTelegramId;
|
||||
|
||||
if (isWinner) {
|
||||
// Send winner notification
|
||||
await this.bot.sendMessage(
|
||||
telegramId,
|
||||
messages.notifications.winner(
|
||||
@@ -449,12 +493,11 @@ class NotificationScheduler {
|
||||
);
|
||||
logger.info('Sent winner notification', { telegramId });
|
||||
} else {
|
||||
// Send loser notification with actual winning ticket number
|
||||
await this.bot.sendMessage(
|
||||
telegramId,
|
||||
messages.notifications.loser(
|
||||
winnerTicketNumber,
|
||||
cycle.pot_total_sats.toLocaleString()
|
||||
potSats.toLocaleString()
|
||||
),
|
||||
{ parse_mode: 'Markdown' }
|
||||
);
|
||||
@@ -465,9 +508,8 @@ class NotificationScheduler {
|
||||
}
|
||||
}
|
||||
|
||||
// Send group announcements
|
||||
const uniqueUserCount = userPurchases.size;
|
||||
await this.sendGroupDrawAnnouncements(cycle, winnerDisplayName, winnerTicketNumber, uniqueUserCount, winnerLightningAddress);
|
||||
// ALWAYS send group announcements (regardless of tracked participants)
|
||||
await this.sendGroupDrawAnnouncements(cycle, winnerDisplayName, winnerTicketNumber, totalTickets, winnerLightningAddress);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user