Add SQLite database for Telegram bot user/group settings
- Replace Redis/in-memory storage with SQLite for persistence - Add database.ts service with tables for users, groups, purchases, participants - Update state.ts and groupState.ts to use SQLite backend - Fix buyer_name to use display name instead of Telegram ID - Remove legacy reminder array handlers (now using 3-slot system) - Add better-sqlite3 dependency, remove ioredis - Update env.example with BOT_DATABASE_PATH option - Add data/ directory to .gitignore for database files
This commit is contained in:
661
telegram_bot/src/services/notificationScheduler.ts
Normal file
661
telegram_bot/src/services/notificationScheduler.ts
Normal file
@@ -0,0 +1,661 @@
|
||||
import TelegramBot from 'node-telegram-bot-api';
|
||||
import { groupStateManager } from './groupState';
|
||||
import { stateManager } from './state';
|
||||
import { apiClient } from './api';
|
||||
import { logger } from './logger';
|
||||
import { messages } from '../messages';
|
||||
import { GroupSettings, reminderTimeToMinutes, formatReminderTime, ReminderTime, DEFAULT_GROUP_REMINDER_SLOTS } from '../types/groups';
|
||||
import { TelegramUser } from '../types';
|
||||
|
||||
interface CycleInfo {
|
||||
id: string;
|
||||
scheduled_at: string;
|
||||
status: string;
|
||||
pot_total_sats: number;
|
||||
}
|
||||
|
||||
interface ScheduledReminder {
|
||||
groupId?: number;
|
||||
telegramId?: number;
|
||||
cycleId: string;
|
||||
reminderKey: string;
|
||||
scheduledFor: Date;
|
||||
timeout: NodeJS.Timeout;
|
||||
}
|
||||
|
||||
class NotificationScheduler {
|
||||
private bot: TelegramBot | null = null;
|
||||
private pollInterval: NodeJS.Timeout | null = null;
|
||||
private scheduledReminders: Map<string, ScheduledReminder> = new Map();
|
||||
private lastCycleId: string | null = null;
|
||||
private lastCycleStatus: string | null = null;
|
||||
private isRunning = false;
|
||||
private announcedCycles: Set<string> = new Set(); // Track announced cycles
|
||||
|
||||
/**
|
||||
* Initialize the scheduler with the bot instance
|
||||
*/
|
||||
init(bot: TelegramBot): void {
|
||||
this.bot = bot;
|
||||
logger.info('Notification scheduler initialized');
|
||||
}
|
||||
|
||||
/**
|
||||
* Start the scheduler
|
||||
*/
|
||||
start(): void {
|
||||
if (this.isRunning || !this.bot) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.isRunning = true;
|
||||
logger.info('Starting notification scheduler');
|
||||
|
||||
// Poll every 30 seconds
|
||||
this.pollInterval = setInterval(() => this.poll(), 30 * 1000);
|
||||
|
||||
// Run immediately
|
||||
this.poll();
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop the scheduler
|
||||
*/
|
||||
stop(): void {
|
||||
if (this.pollInterval) {
|
||||
clearInterval(this.pollInterval);
|
||||
this.pollInterval = null;
|
||||
}
|
||||
|
||||
// Clear all scheduled reminders
|
||||
for (const reminder of this.scheduledReminders.values()) {
|
||||
clearTimeout(reminder.timeout);
|
||||
}
|
||||
this.scheduledReminders.clear();
|
||||
|
||||
this.isRunning = false;
|
||||
logger.info('Notification scheduler stopped');
|
||||
}
|
||||
|
||||
/**
|
||||
* Main poll loop
|
||||
*/
|
||||
private async poll(): Promise<void> {
|
||||
try {
|
||||
const jackpot = await apiClient.getNextJackpot();
|
||||
if (!jackpot?.cycle) {
|
||||
return;
|
||||
}
|
||||
|
||||
const cycle = jackpot.cycle;
|
||||
const lottery = jackpot.lottery;
|
||||
|
||||
// Check for draw completion (same cycle, status changed to completed)
|
||||
if (this.lastCycleId === cycle.id &&
|
||||
this.lastCycleStatus !== 'completed' &&
|
||||
cycle.status === 'completed') {
|
||||
await this.handleDrawCompleted(cycle);
|
||||
}
|
||||
|
||||
// Check for new cycle (new jackpot started)
|
||||
// IMPORTANT: When we detect a new cycle, the old one is completed
|
||||
// Send draw completion for the old cycle BEFORE new cycle announcement
|
||||
if (this.lastCycleId && this.lastCycleId !== cycle.id) {
|
||||
// The previous cycle has completed - announce the draw result first
|
||||
await this.handlePreviousCycleCompleted(this.lastCycleId);
|
||||
|
||||
// Then announce the new cycle
|
||||
await this.handleNewCycle(cycle, lottery);
|
||||
}
|
||||
|
||||
// Schedule reminders for current cycle
|
||||
await this.scheduleGroupReminders(cycle);
|
||||
await this.scheduleUserReminders(cycle);
|
||||
|
||||
this.lastCycleId = cycle.id;
|
||||
this.lastCycleStatus = cycle.status;
|
||||
} catch (error) {
|
||||
logger.error('Error in notification scheduler poll', { error });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle previous cycle completion when we detect a new cycle
|
||||
*/
|
||||
private async handlePreviousCycleCompleted(previousCycleId: string): Promise<void> {
|
||||
if (!this.bot) return;
|
||||
|
||||
// Check if we've already announced this draw
|
||||
if (this.announcedCycles.has(`draw:${previousCycleId}`)) {
|
||||
return;
|
||||
}
|
||||
this.announcedCycles.add(`draw:${previousCycleId}`);
|
||||
|
||||
// Clear reminders for the old cycle
|
||||
this.clearRemindersForCycle(previousCycleId);
|
||||
|
||||
// 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
|
||||
let winnerDisplayName = 'Anon';
|
||||
let winnerTicketNumber = '0000';
|
||||
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;
|
||||
|
||||
// Check if they won
|
||||
const status = await apiClient.getTicketStatus(participant.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');
|
||||
}
|
||||
|
||||
// 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 });
|
||||
}
|
||||
} 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 });
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('Failed to notify participant', {
|
||||
telegramId: 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 draw announcements to groups immediately (no delay - for cycle transition)
|
||||
*/
|
||||
private async sendGroupDrawAnnouncementsImmediate(
|
||||
cycleId: string,
|
||||
winnerDisplayName: string,
|
||||
winnerTicketNumber: string,
|
||||
potSats: number,
|
||||
totalParticipants: number
|
||||
): Promise<void> {
|
||||
if (!this.bot) return;
|
||||
|
||||
const groups = await groupStateManager.getGroupsWithFeature('drawAnnouncements');
|
||||
|
||||
for (const group of groups) {
|
||||
try {
|
||||
const message = messages.notifications.drawAnnouncement(
|
||||
winnerDisplayName,
|
||||
`#${winnerTicketNumber}`,
|
||||
potSats.toLocaleString(),
|
||||
totalParticipants
|
||||
);
|
||||
|
||||
await this.bot.sendMessage(group.groupId, message, { parse_mode: 'Markdown' });
|
||||
logger.debug('Sent draw announcement to group', { groupId: group.groupId });
|
||||
} catch (error) {
|
||||
this.handleSendError(error, group.groupId);
|
||||
}
|
||||
}
|
||||
|
||||
logger.info('Draw announcements sent', {
|
||||
cycleId,
|
||||
groupCount: groups.length
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle new cycle announcement
|
||||
*/
|
||||
private async handleNewCycle(
|
||||
cycle: CycleInfo,
|
||||
lottery: { name: string; ticket_price_sats: number }
|
||||
): Promise<void> {
|
||||
if (!this.bot) return;
|
||||
|
||||
// Check if we've already announced this cycle
|
||||
if (this.announcedCycles.has(`new:${cycle.id}`)) {
|
||||
return;
|
||||
}
|
||||
this.announcedCycles.add(`new:${cycle.id}`);
|
||||
|
||||
const drawTime = new Date(cycle.scheduled_at);
|
||||
|
||||
// Send to groups
|
||||
const groups = await groupStateManager.getGroupsWithFeature('enabled');
|
||||
for (const group of groups) {
|
||||
if (group.newJackpotAnnouncement === false) continue;
|
||||
|
||||
try {
|
||||
const message = messages.notifications.newJackpot(
|
||||
lottery.name,
|
||||
lottery.ticket_price_sats,
|
||||
drawTime
|
||||
);
|
||||
await this.bot.sendMessage(group.groupId, message, { parse_mode: 'Markdown' });
|
||||
logger.debug('Sent new jackpot announcement to group', { groupId: group.groupId });
|
||||
} catch (error) {
|
||||
this.handleSendError(error, group.groupId);
|
||||
}
|
||||
}
|
||||
|
||||
// Send to users with new jackpot alerts enabled
|
||||
const users = await stateManager.getUsersWithNotification('newJackpotAlerts');
|
||||
for (const user of users) {
|
||||
try {
|
||||
const message = messages.notifications.newJackpot(
|
||||
lottery.name,
|
||||
lottery.ticket_price_sats,
|
||||
drawTime
|
||||
);
|
||||
await this.bot.sendMessage(user.telegramId, message, { parse_mode: 'Markdown' });
|
||||
logger.debug('Sent new jackpot alert to user', { telegramId: user.telegramId });
|
||||
} catch (error) {
|
||||
logger.error('Failed to send new jackpot alert', { telegramId: user.telegramId, error });
|
||||
}
|
||||
}
|
||||
|
||||
logger.info('New jackpot announcements sent', { cycleId: cycle.id });
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle draw completed - send notifications to participants and groups
|
||||
*/
|
||||
private async handleDrawCompleted(cycle: CycleInfo): Promise<void> {
|
||||
if (!this.bot) return;
|
||||
|
||||
// Check if we've already announced this draw
|
||||
if (this.announcedCycles.has(`draw:${cycle.id}`)) {
|
||||
return;
|
||||
}
|
||||
this.announcedCycles.add(`draw:${cycle.id}`);
|
||||
|
||||
// Clear reminders for this cycle
|
||||
this.clearRemindersForCycle(cycle.id);
|
||||
|
||||
// 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,
|
||||
participantCount: participants.length,
|
||||
potSats: cycle.pot_total_sats
|
||||
});
|
||||
|
||||
// Only proceed if there were participants
|
||||
if (!hasParticipants) {
|
||||
logger.info('No participants in cycle, skipping notifications', { cycleId: cycle.id });
|
||||
return;
|
||||
}
|
||||
|
||||
// Get draw result details for winner announcement
|
||||
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;
|
||||
|
||||
// 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');
|
||||
}
|
||||
|
||||
// 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,
|
||||
messages.notifications.winner(
|
||||
prizeSats.toLocaleString(),
|
||||
winnerTicketNumber,
|
||||
payoutStatus
|
||||
),
|
||||
{ parse_mode: 'Markdown' }
|
||||
);
|
||||
logger.info('Sent winner notification', { telegramId: participant.telegramId });
|
||||
} else {
|
||||
// Send loser notification
|
||||
await this.bot.sendMessage(
|
||||
participant.telegramId,
|
||||
messages.notifications.loser(
|
||||
winnerTicketNumber,
|
||||
cycle.pot_total_sats.toLocaleString()
|
||||
),
|
||||
{ parse_mode: 'Markdown' }
|
||||
);
|
||||
logger.debug('Sent draw result to participant', { telegramId: participant.telegramId });
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('Failed to notify participant', {
|
||||
telegramId: participant.telegramId,
|
||||
error
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Send group announcements (only if there were participants)
|
||||
await this.sendGroupDrawAnnouncements(cycle, winnerDisplayName, winnerTicketNumber, participants.length);
|
||||
}
|
||||
|
||||
/**
|
||||
* Send draw announcements to groups
|
||||
*/
|
||||
private async sendGroupDrawAnnouncements(
|
||||
cycle: CycleInfo,
|
||||
winnerDisplayName: string,
|
||||
winnerTicketNumber: string,
|
||||
totalParticipants: number
|
||||
): Promise<void> {
|
||||
if (!this.bot) return;
|
||||
|
||||
const groups = await groupStateManager.getGroupsWithFeature('drawAnnouncements');
|
||||
|
||||
for (const group of groups) {
|
||||
const delay = (group.announcementDelaySeconds || 0) * 1000;
|
||||
|
||||
setTimeout(async () => {
|
||||
try {
|
||||
const message = messages.notifications.drawAnnouncement(
|
||||
winnerDisplayName,
|
||||
`#${winnerTicketNumber}`,
|
||||
cycle.pot_total_sats.toLocaleString(),
|
||||
totalParticipants
|
||||
);
|
||||
|
||||
if (this.bot) {
|
||||
await this.bot.sendMessage(group.groupId, message, { parse_mode: 'Markdown' });
|
||||
logger.debug('Sent draw announcement to group', { groupId: group.groupId });
|
||||
}
|
||||
} catch (error) {
|
||||
this.handleSendError(error, group.groupId);
|
||||
}
|
||||
}, delay);
|
||||
}
|
||||
|
||||
logger.info('Draw announcements scheduled', {
|
||||
cycleId: cycle.id,
|
||||
groupCount: groups.length
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Schedule reminders for groups (3-tier system with custom times)
|
||||
*/
|
||||
private async scheduleGroupReminders(cycle: CycleInfo): Promise<void> {
|
||||
if (!this.bot || cycle.status === 'completed' || cycle.status === 'cancelled') {
|
||||
return;
|
||||
}
|
||||
|
||||
const drawTime = new Date(cycle.scheduled_at);
|
||||
const now = new Date();
|
||||
|
||||
const groups = await groupStateManager.getGroupsWithFeature('reminders');
|
||||
|
||||
for (const group of groups) {
|
||||
// Build list of enabled reminders from 3-tier system with custom times
|
||||
const enabledReminders: { slot: number; time: ReminderTime }[] = [];
|
||||
|
||||
// Check each of the 3 reminder slots with their custom times
|
||||
if (group.reminder1Enabled !== false) {
|
||||
const time = group.reminder1Time || DEFAULT_GROUP_REMINDER_SLOTS[0];
|
||||
enabledReminders.push({ slot: 1, time });
|
||||
}
|
||||
if (group.reminder2Enabled === true) {
|
||||
const time = group.reminder2Time || DEFAULT_GROUP_REMINDER_SLOTS[1];
|
||||
enabledReminders.push({ slot: 2, time });
|
||||
}
|
||||
if (group.reminder3Enabled === true) {
|
||||
const time = group.reminder3Time || DEFAULT_GROUP_REMINDER_SLOTS[2];
|
||||
enabledReminders.push({ slot: 3, time });
|
||||
}
|
||||
|
||||
if (enabledReminders.length === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const { slot, time: reminderTime } of enabledReminders) {
|
||||
const reminderKey = `slot${slot}_${formatReminderTime(reminderTime)}`;
|
||||
const uniqueKey = `group:${group.groupId}:${cycle.id}:${reminderKey}`;
|
||||
|
||||
if (this.scheduledReminders.has(uniqueKey)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const minutesBefore = reminderTimeToMinutes(reminderTime);
|
||||
const reminderDate = new Date(drawTime.getTime() - minutesBefore * 60 * 1000);
|
||||
|
||||
if (reminderDate <= now) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const delay = reminderDate.getTime() - now.getTime();
|
||||
|
||||
const timeout = setTimeout(async () => {
|
||||
await this.sendGroupReminder(group, cycle, reminderTime, drawTime);
|
||||
this.scheduledReminders.delete(uniqueKey);
|
||||
}, delay);
|
||||
|
||||
this.scheduledReminders.set(uniqueKey, {
|
||||
groupId: group.groupId,
|
||||
cycleId: cycle.id,
|
||||
reminderKey,
|
||||
scheduledFor: reminderDate,
|
||||
timeout,
|
||||
});
|
||||
|
||||
logger.debug('Scheduled group reminder', {
|
||||
groupId: group.groupId,
|
||||
cycleId: cycle.id,
|
||||
slot,
|
||||
reminderKey,
|
||||
scheduledFor: reminderDate.toISOString(),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Schedule reminders for individual users with drawReminders enabled
|
||||
*/
|
||||
private async scheduleUserReminders(cycle: CycleInfo): Promise<void> {
|
||||
if (!this.bot || cycle.status === 'completed' || cycle.status === 'cancelled') {
|
||||
return;
|
||||
}
|
||||
|
||||
const drawTime = new Date(cycle.scheduled_at);
|
||||
const now = new Date();
|
||||
|
||||
// Get users with draw reminders enabled
|
||||
const users = await stateManager.getUsersWithNotification('drawReminders');
|
||||
|
||||
// Default reminder: 15 minutes before
|
||||
const defaultReminder: ReminderTime = { value: 15, unit: 'minutes' };
|
||||
const reminderKey = formatReminderTime(defaultReminder);
|
||||
|
||||
for (const user of users) {
|
||||
const uniqueKey = `user:${user.telegramId}:${cycle.id}:${reminderKey}`;
|
||||
|
||||
if (this.scheduledReminders.has(uniqueKey)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const minutesBefore = reminderTimeToMinutes(defaultReminder);
|
||||
const reminderDate = new Date(drawTime.getTime() - minutesBefore * 60 * 1000);
|
||||
|
||||
if (reminderDate <= now) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const delay = reminderDate.getTime() - now.getTime();
|
||||
|
||||
const timeout = setTimeout(async () => {
|
||||
await this.sendUserReminder(user, cycle, defaultReminder, drawTime);
|
||||
this.scheduledReminders.delete(uniqueKey);
|
||||
}, delay);
|
||||
|
||||
this.scheduledReminders.set(uniqueKey, {
|
||||
telegramId: user.telegramId,
|
||||
cycleId: cycle.id,
|
||||
reminderKey,
|
||||
scheduledFor: reminderDate,
|
||||
timeout,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a reminder to a group
|
||||
*/
|
||||
private async sendGroupReminder(
|
||||
group: GroupSettings,
|
||||
cycle: CycleInfo,
|
||||
reminderTime: ReminderTime,
|
||||
drawTime: Date
|
||||
): Promise<void> {
|
||||
if (!this.bot) return;
|
||||
|
||||
try {
|
||||
const message = messages.notifications.drawReminder(
|
||||
reminderTime.value,
|
||||
reminderTime.unit,
|
||||
drawTime,
|
||||
cycle.pot_total_sats
|
||||
);
|
||||
|
||||
await this.bot.sendMessage(group.groupId, message, { parse_mode: 'Markdown' });
|
||||
logger.info('Sent draw reminder to group', {
|
||||
groupId: group.groupId,
|
||||
reminderKey: formatReminderTime(reminderTime)
|
||||
});
|
||||
} catch (error) {
|
||||
this.handleSendError(error, group.groupId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a reminder to a user
|
||||
*/
|
||||
private async sendUserReminder(
|
||||
user: TelegramUser,
|
||||
cycle: CycleInfo,
|
||||
reminderTime: ReminderTime,
|
||||
drawTime: Date
|
||||
): Promise<void> {
|
||||
if (!this.bot) return;
|
||||
|
||||
try {
|
||||
const message = messages.notifications.drawReminder(
|
||||
reminderTime.value,
|
||||
reminderTime.unit,
|
||||
drawTime,
|
||||
cycle.pot_total_sats
|
||||
);
|
||||
|
||||
await this.bot.sendMessage(user.telegramId, message, { parse_mode: 'Markdown' });
|
||||
logger.info('Sent draw reminder to user', { telegramId: user.telegramId });
|
||||
} catch (error) {
|
||||
logger.error('Failed to send reminder to user', { telegramId: user.telegramId, error });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle send errors (remove group if bot was kicked)
|
||||
*/
|
||||
private handleSendError(error: any, groupId: number): void {
|
||||
logger.error('Failed to send message to group', { groupId, error });
|
||||
if (error?.response?.statusCode === 403) {
|
||||
groupStateManager.removeGroup(groupId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all scheduled reminders for a cycle
|
||||
*/
|
||||
private clearRemindersForCycle(cycleId: string): void {
|
||||
for (const [key, reminder] of this.scheduledReminders.entries()) {
|
||||
if (reminder.cycleId === cycleId) {
|
||||
clearTimeout(reminder.timeout);
|
||||
this.scheduledReminders.delete(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get status info for debugging
|
||||
*/
|
||||
getStatus(): object {
|
||||
return {
|
||||
isRunning: this.isRunning,
|
||||
lastCycleId: this.lastCycleId,
|
||||
lastCycleStatus: this.lastCycleStatus,
|
||||
scheduledReminders: this.scheduledReminders.size,
|
||||
announcedCycles: this.announcedCycles.size,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export const notificationScheduler = new NotificationScheduler();
|
||||
export default notificationScheduler;
|
||||
Reference in New Issue
Block a user