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 };
|
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();
|
export const apiClient = new ApiClient();
|
||||||
|
|||||||
@@ -122,6 +122,7 @@ class NotificationScheduler {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Handle previous cycle completion when we detect a new cycle
|
* 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> {
|
private async handlePreviousCycleCompleted(previousCycleId: string): Promise<void> {
|
||||||
if (!this.bot) return;
|
if (!this.bot) return;
|
||||||
@@ -135,19 +136,41 @@ class NotificationScheduler {
|
|||||||
// Clear reminders for the old cycle
|
// Clear reminders for the old cycle
|
||||||
this.clearRemindersForCycle(previousCycleId);
|
this.clearRemindersForCycle(previousCycleId);
|
||||||
|
|
||||||
// Get participants for the previous cycle
|
logger.info('Processing previous cycle completion', { cycleId: previousCycleId });
|
||||||
const participants = await stateManager.getCycleParticipants(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,
|
cycleId: previousCycleId,
|
||||||
participantCount: participants.length
|
winnerName: winnerDisplayName,
|
||||||
|
potSats
|
||||||
});
|
});
|
||||||
|
}
|
||||||
if (participants.length === 0) {
|
} catch (error) {
|
||||||
return;
|
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[]>();
|
const userPurchases = new Map<number, string[]>();
|
||||||
for (const participant of participants) {
|
for (const participant of participants) {
|
||||||
const existing = userPurchases.get(participant.telegramId) || [];
|
const existing = userPurchases.get(participant.telegramId) || [];
|
||||||
@@ -155,13 +178,11 @@ class NotificationScheduler {
|
|||||||
userPurchases.set(participant.telegramId, existing);
|
userPurchases.set(participant.telegramId, existing);
|
||||||
}
|
}
|
||||||
|
|
||||||
// First pass: Find the winning ticket info and pot amount
|
totalTickets = userPurchases.size;
|
||||||
let winnerDisplayName = 'Anon';
|
|
||||||
let winnerTicketNumber = '0000';
|
// Find winner among tracked participants for DM notifications
|
||||||
let winnerTelegramId: number | null = null;
|
let winnerTelegramId: number | null = null;
|
||||||
let winnerLightningAddress = '';
|
let prizeSats = potSats;
|
||||||
let potSats = 0;
|
|
||||||
let prizeSats = 0;
|
|
||||||
let payoutStatus = 'processing';
|
let payoutStatus = 'processing';
|
||||||
|
|
||||||
for (const [telegramId, purchaseIds] of userPurchases) {
|
for (const [telegramId, purchaseIds] of userPurchases) {
|
||||||
@@ -170,13 +191,17 @@ class NotificationScheduler {
|
|||||||
const status = await apiClient.getTicketStatus(purchaseId);
|
const status = await apiClient.getTicketStatus(purchaseId);
|
||||||
if (!status) continue;
|
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) {
|
if (status.result.is_winner) {
|
||||||
const user = await stateManager.getUser(telegramId);
|
const user = await stateManager.getUser(telegramId);
|
||||||
winnerTelegramId = telegramId;
|
winnerTelegramId = telegramId;
|
||||||
winnerDisplayName = user ? stateManager.getDisplayName(user) : 'Anon';
|
if (user) {
|
||||||
winnerLightningAddress = status.purchase.lightning_address || '';
|
winnerDisplayName = stateManager.getDisplayName(user);
|
||||||
|
}
|
||||||
|
winnerLightningAddress = status.purchase.lightning_address || winnerLightningAddress;
|
||||||
|
|
||||||
const winningTicket = status.tickets.find(t => t.is_winning_ticket);
|
const winningTicket = status.tickets.find(t => t.is_winning_ticket);
|
||||||
if (winningTicket) {
|
if (winningTicket) {
|
||||||
@@ -185,16 +210,16 @@ class NotificationScheduler {
|
|||||||
|
|
||||||
prizeSats = status.result.payout?.amount_sats || potSats;
|
prizeSats = status.result.payout?.amount_sats || potSats;
|
||||||
payoutStatus = status.result.payout?.status || 'processing';
|
payoutStatus = status.result.payout?.status || 'processing';
|
||||||
break; // Found winner, stop searching
|
break;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Error checking purchase status', { purchaseId, 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>();
|
const notifiedUsers = new Set<number>();
|
||||||
|
|
||||||
for (const [telegramId, purchaseIds] of userPurchases) {
|
for (const [telegramId, purchaseIds] of userPurchases) {
|
||||||
@@ -208,7 +233,6 @@ class NotificationScheduler {
|
|||||||
const isWinner = telegramId === winnerTelegramId;
|
const isWinner = telegramId === winnerTelegramId;
|
||||||
|
|
||||||
if (isWinner) {
|
if (isWinner) {
|
||||||
// Send winner notification
|
|
||||||
await this.bot.sendMessage(
|
await this.bot.sendMessage(
|
||||||
telegramId,
|
telegramId,
|
||||||
messages.notifications.winner(
|
messages.notifications.winner(
|
||||||
@@ -220,7 +244,6 @@ class NotificationScheduler {
|
|||||||
);
|
);
|
||||||
logger.info('Sent winner notification', { telegramId });
|
logger.info('Sent winner notification', { telegramId });
|
||||||
} else {
|
} else {
|
||||||
// Send loser notification with actual winning ticket number
|
|
||||||
await this.bot.sendMessage(
|
await this.bot.sendMessage(
|
||||||
telegramId,
|
telegramId,
|
||||||
messages.notifications.loser(
|
messages.notifications.loser(
|
||||||
@@ -236,14 +259,13 @@ class NotificationScheduler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Send group announcements
|
// ALWAYS send group announcements (regardless of tracked participants)
|
||||||
const uniqueUserCount = userPurchases.size;
|
|
||||||
await this.sendGroupDrawAnnouncementsImmediate(
|
await this.sendGroupDrawAnnouncementsImmediate(
|
||||||
previousCycleId,
|
previousCycleId,
|
||||||
winnerDisplayName,
|
winnerDisplayName,
|
||||||
winnerTicketNumber,
|
winnerTicketNumber,
|
||||||
potSats,
|
potSats,
|
||||||
uniqueUserCount,
|
totalTickets,
|
||||||
winnerLightningAddress
|
winnerLightningAddress
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -351,6 +373,7 @@ class NotificationScheduler {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Handle draw completed - send notifications to participants and groups
|
* 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> {
|
private async handleDrawCompleted(cycle: CycleInfo): Promise<void> {
|
||||||
if (!this.bot) return;
|
if (!this.bot) return;
|
||||||
@@ -364,22 +387,43 @@ class NotificationScheduler {
|
|||||||
// Clear reminders for this cycle
|
// Clear reminders for this cycle
|
||||||
this.clearRemindersForCycle(cycle.id);
|
this.clearRemindersForCycle(cycle.id);
|
||||||
|
|
||||||
// Get participants for this cycle
|
|
||||||
const participants = await stateManager.getCycleParticipants(cycle.id);
|
|
||||||
|
|
||||||
logger.info('Processing draw completion', {
|
logger.info('Processing draw completion', {
|
||||||
cycleId: cycle.id,
|
cycleId: cycle.id,
|
||||||
participantCount: participants.length,
|
|
||||||
potSats: cycle.pot_total_sats
|
potSats: cycle.pot_total_sats
|
||||||
});
|
});
|
||||||
|
|
||||||
// Only proceed if there were participants
|
// Fetch winner info from API (works even if no participants tracked locally)
|
||||||
if (participants.length === 0) {
|
let winnerDisplayName = 'Anon';
|
||||||
logger.info('No participants in cycle, skipping notifications', { cycleId: cycle.id });
|
let winnerTicketNumber = '0000';
|
||||||
return;
|
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[]>();
|
const userPurchases = new Map<number, string[]>();
|
||||||
for (const participant of participants) {
|
for (const participant of participants) {
|
||||||
const existing = userPurchases.get(participant.telegramId) || [];
|
const existing = userPurchases.get(participant.telegramId) || [];
|
||||||
@@ -387,12 +431,11 @@ class NotificationScheduler {
|
|||||||
userPurchases.set(participant.telegramId, existing);
|
userPurchases.set(participant.telegramId, existing);
|
||||||
}
|
}
|
||||||
|
|
||||||
// First pass: Find the winning ticket info
|
totalTickets = userPurchases.size;
|
||||||
let winnerDisplayName = 'Anon';
|
|
||||||
let winnerTicketNumber = '0000';
|
// Find winner among tracked participants for DM notifications
|
||||||
let winnerTelegramId: number | null = null;
|
let winnerTelegramId: number | null = null;
|
||||||
let winnerLightningAddress = '';
|
let prizeSats = potSats;
|
||||||
let prizeSats = 0;
|
|
||||||
let payoutStatus = 'processing';
|
let payoutStatus = 'processing';
|
||||||
|
|
||||||
for (const [telegramId, purchaseIds] of userPurchases) {
|
for (const [telegramId, purchaseIds] of userPurchases) {
|
||||||
@@ -404,26 +447,28 @@ class NotificationScheduler {
|
|||||||
if (status.result.is_winner) {
|
if (status.result.is_winner) {
|
||||||
const user = await stateManager.getUser(telegramId);
|
const user = await stateManager.getUser(telegramId);
|
||||||
winnerTelegramId = telegramId;
|
winnerTelegramId = telegramId;
|
||||||
winnerDisplayName = user ? stateManager.getDisplayName(user) : 'Anon';
|
if (user) {
|
||||||
winnerLightningAddress = status.purchase.lightning_address || '';
|
winnerDisplayName = stateManager.getDisplayName(user);
|
||||||
|
}
|
||||||
|
winnerLightningAddress = status.purchase.lightning_address || winnerLightningAddress;
|
||||||
|
|
||||||
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;
|
prizeSats = status.result.payout?.amount_sats || potSats;
|
||||||
payoutStatus = status.result.payout?.status || 'processing';
|
payoutStatus = status.result.payout?.status || 'processing';
|
||||||
break; // Found winner, stop searching
|
break;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Error checking purchase status', { purchaseId, 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>();
|
const notifiedUsers = new Set<number>();
|
||||||
|
|
||||||
for (const [telegramId] of userPurchases) {
|
for (const [telegramId] of userPurchases) {
|
||||||
@@ -437,7 +482,6 @@ class NotificationScheduler {
|
|||||||
const isWinner = telegramId === winnerTelegramId;
|
const isWinner = telegramId === winnerTelegramId;
|
||||||
|
|
||||||
if (isWinner) {
|
if (isWinner) {
|
||||||
// Send winner notification
|
|
||||||
await this.bot.sendMessage(
|
await this.bot.sendMessage(
|
||||||
telegramId,
|
telegramId,
|
||||||
messages.notifications.winner(
|
messages.notifications.winner(
|
||||||
@@ -449,12 +493,11 @@ class NotificationScheduler {
|
|||||||
);
|
);
|
||||||
logger.info('Sent winner notification', { telegramId });
|
logger.info('Sent winner notification', { telegramId });
|
||||||
} else {
|
} else {
|
||||||
// Send loser notification with actual winning ticket number
|
|
||||||
await this.bot.sendMessage(
|
await this.bot.sendMessage(
|
||||||
telegramId,
|
telegramId,
|
||||||
messages.notifications.loser(
|
messages.notifications.loser(
|
||||||
winnerTicketNumber,
|
winnerTicketNumber,
|
||||||
cycle.pot_total_sats.toLocaleString()
|
potSats.toLocaleString()
|
||||||
),
|
),
|
||||||
{ parse_mode: 'Markdown' }
|
{ parse_mode: 'Markdown' }
|
||||||
);
|
);
|
||||||
@@ -465,9 +508,8 @@ class NotificationScheduler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Send group announcements
|
// ALWAYS send group announcements (regardless of tracked participants)
|
||||||
const uniqueUserCount = userPurchases.size;
|
await this.sendGroupDrawAnnouncements(cycle, winnerDisplayName, winnerTicketNumber, totalTickets, winnerLightningAddress);
|
||||||
await this.sendGroupDrawAnnouncements(cycle, winnerDisplayName, winnerTicketNumber, uniqueUserCount, winnerLightningAddress);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
Reference in New Issue
Block a user