Update API client and notification scheduler services

This commit is contained in:
Michilis
2025-12-20 02:26:40 +00:00
parent 1dce27ea42
commit 83298dc4ca
2 changed files with 120 additions and 52 deletions

View File

@@ -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();

View File

@@ -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);
}
/**