Fix reminder scheduling and add group features
- Fix reminder duplicate bug: use slot-only keys to prevent multiple reminders when settings change - Add New Jackpot announcement delay setting for groups (default 5 min) - Cancel unpaid purchases after draw completes (prevents payments for past rounds) - Add BotFather commands template file - Update README documentation
This commit is contained in:
@@ -105,6 +105,7 @@ class BotDatabase {
|
||||
reminder3_enabled INTEGER DEFAULT 0,
|
||||
reminder3_time TEXT DEFAULT '{"value":6,"unit":"days"}',
|
||||
announcement_delay_seconds INTEGER DEFAULT 10,
|
||||
new_jackpot_delay_minutes INTEGER DEFAULT 5,
|
||||
added_by INTEGER,
|
||||
added_at TEXT DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TEXT DEFAULT CURRENT_TIMESTAMP
|
||||
@@ -515,6 +516,7 @@ class BotDatabase {
|
||||
reminder3_enabled = ?,
|
||||
reminder3_time = ?,
|
||||
announcement_delay_seconds = ?,
|
||||
new_jackpot_delay_minutes = ?,
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
WHERE group_id = ?
|
||||
`).run(
|
||||
@@ -531,6 +533,7 @@ class BotDatabase {
|
||||
settings.reminder3Enabled ? 1 : 0,
|
||||
JSON.stringify(settings.reminder3Time),
|
||||
settings.announcementDelaySeconds,
|
||||
settings.newJackpotDelayMinutes ?? 5,
|
||||
settings.groupId
|
||||
);
|
||||
}
|
||||
@@ -580,6 +583,18 @@ class BotDatabase {
|
||||
return this.getGroup(groupId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update new jackpot announcement delay
|
||||
*/
|
||||
updateNewJackpotDelay(groupId: number, minutes: number): GroupSettings | null {
|
||||
const group = this.getGroup(groupId);
|
||||
if (!group) return null;
|
||||
|
||||
group.newJackpotDelayMinutes = minutes;
|
||||
this.saveGroup(group);
|
||||
return this.getGroup(groupId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get groups with a specific feature enabled
|
||||
*/
|
||||
@@ -627,6 +642,7 @@ class BotDatabase {
|
||||
reminder3Time: row.reminder3_time ? JSON.parse(row.reminder3_time) : { value: 6, unit: 'days' },
|
||||
reminderTimes: [], // Legacy field
|
||||
announcementDelaySeconds: row.announcement_delay_seconds || 10,
|
||||
newJackpotDelayMinutes: row.new_jackpot_delay_minutes ?? 5,
|
||||
addedBy: row.added_by,
|
||||
addedAt: new Date(row.added_at),
|
||||
updatedAt: new Date(row.updated_at),
|
||||
|
||||
@@ -81,6 +81,16 @@ class GroupStateManager {
|
||||
return botDatabase.updateAnnouncementDelay(groupId, seconds);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update new jackpot announcement delay
|
||||
*/
|
||||
async updateNewJackpotDelay(
|
||||
groupId: number,
|
||||
minutes: number
|
||||
): Promise<GroupSettings | null> {
|
||||
return botDatabase.updateNewJackpotDelay(groupId, minutes);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get groups with specific feature enabled
|
||||
*/
|
||||
|
||||
@@ -270,26 +270,34 @@ class NotificationScheduler {
|
||||
this.announcedCycles.add(`new:${cycle.id}`);
|
||||
|
||||
const drawTime = new Date(cycle.scheduled_at);
|
||||
const bot = this.bot;
|
||||
|
||||
// Send to groups
|
||||
// Send to groups (with configurable delay)
|
||||
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);
|
||||
}
|
||||
const delayMs = (group.newJackpotDelayMinutes ?? 5) * 60 * 1000;
|
||||
|
||||
setTimeout(async () => {
|
||||
try {
|
||||
const message = messages.notifications.newJackpot(
|
||||
lottery.name,
|
||||
lottery.ticket_price_sats,
|
||||
drawTime
|
||||
);
|
||||
await bot.sendMessage(group.groupId, message, { parse_mode: 'Markdown' });
|
||||
logger.debug('Sent new jackpot announcement to group', {
|
||||
groupId: group.groupId,
|
||||
delayMinutes: group.newJackpotDelayMinutes ?? 5
|
||||
});
|
||||
} catch (error) {
|
||||
this.handleSendError(error, group.groupId);
|
||||
}
|
||||
}, delayMs);
|
||||
}
|
||||
|
||||
// Send to users with new jackpot alerts enabled
|
||||
// Send to users with new jackpot alerts enabled (immediate)
|
||||
const users = await stateManager.getUsersWithNotification('newJackpotAlerts');
|
||||
for (const user of users) {
|
||||
try {
|
||||
@@ -305,7 +313,7 @@ class NotificationScheduler {
|
||||
}
|
||||
}
|
||||
|
||||
logger.info('New jackpot announcements sent', { cycleId: cycle.id });
|
||||
logger.info('New jackpot announcements scheduled/sent', { cycleId: cycle.id });
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -477,11 +485,29 @@ class NotificationScheduler {
|
||||
}
|
||||
|
||||
for (const { slot, time: reminderTime } of enabledReminders) {
|
||||
const reminderKey = `slot${slot}_${formatReminderTime(reminderTime)}`;
|
||||
const uniqueKey = `group:${group.groupId}:${cycle.id}:${reminderKey}`;
|
||||
// Use slot-only key to prevent duplicate reminders when settings change
|
||||
const uniqueKey = `group:${group.groupId}:${cycle.id}:slot${slot}`;
|
||||
|
||||
if (this.scheduledReminders.has(uniqueKey)) {
|
||||
continue;
|
||||
// Check if we already have a reminder for this slot
|
||||
const existingReminder = this.scheduledReminders.get(uniqueKey);
|
||||
if (existingReminder) {
|
||||
// If settings changed (different time), cancel old and reschedule
|
||||
const newMinutesBefore = reminderTimeToMinutes(reminderTime);
|
||||
const newReminderDate = new Date(drawTime.getTime() - newMinutesBefore * 60 * 1000);
|
||||
|
||||
// Only reschedule if the time actually changed
|
||||
if (existingReminder.scheduledFor.getTime() !== newReminderDate.getTime()) {
|
||||
clearTimeout(existingReminder.timeout);
|
||||
this.scheduledReminders.delete(uniqueKey);
|
||||
logger.debug('Cleared old reminder due to settings change', {
|
||||
groupId: group.groupId,
|
||||
slot,
|
||||
oldTime: existingReminder.scheduledFor.toISOString(),
|
||||
newTime: newReminderDate.toISOString(),
|
||||
});
|
||||
} else {
|
||||
continue; // Same time, skip
|
||||
}
|
||||
}
|
||||
|
||||
const minutesBefore = reminderTimeToMinutes(reminderTime);
|
||||
@@ -501,7 +527,7 @@ class NotificationScheduler {
|
||||
this.scheduledReminders.set(uniqueKey, {
|
||||
groupId: group.groupId,
|
||||
cycleId: cycle.id,
|
||||
reminderKey,
|
||||
reminderKey: `slot${slot}_${formatReminderTime(reminderTime)}`,
|
||||
scheduledFor: reminderDate,
|
||||
timeout,
|
||||
});
|
||||
@@ -510,7 +536,7 @@ class NotificationScheduler {
|
||||
groupId: group.groupId,
|
||||
cycleId: cycle.id,
|
||||
slot,
|
||||
reminderKey,
|
||||
time: formatReminderTime(reminderTime),
|
||||
scheduledFor: reminderDate.toISOString(),
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user