From 86e2e0a321bb39740d4530323dae21e5f78d1653 Mon Sep 17 00:00:00 2001 From: Michilis Date: Mon, 8 Dec 2025 23:49:54 +0000 Subject: [PATCH] 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 --- README.md | 162 +++++++++++++++--- back_end/src/scheduler/index.ts | 26 +++ telegram_bot/botfather_commands.txt | 71 ++++++++ telegram_bot/src/handlers/groups.ts | 44 ++++- telegram_bot/src/index.ts | 7 + telegram_bot/src/messages/index.ts | 8 +- telegram_bot/src/services/database.ts | 16 ++ telegram_bot/src/services/groupState.ts | 10 ++ .../src/services/notificationScheduler.ts | 66 ++++--- telegram_bot/src/types/groups.ts | 7 + 10 files changed, 366 insertions(+), 51 deletions(-) create mode 100644 telegram_bot/botfather_commands.txt diff --git a/README.md b/README.md index dd748b1..551e455 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# ⚡ Lightning Lottery +# ⚡ Lightning Lotto A complete Bitcoin Lightning Network powered lottery system with instant payouts to Lightning Addresses. @@ -10,24 +10,27 @@ A complete Bitcoin Lightning Network powered lottery system with instant payouts - 🌐 **Anonymous or Nostr**: Buy tickets anonymously or login with Nostr - 📱 **Mobile First**: Beautiful responsive design optimized for all devices - 🏆 **Automatic Payouts**: Winners get paid automatically to their Lightning Address -- ⏰ **Multiple Cycles**: Support for hourly, daily, weekly, and monthly draws +- ⏰ **Flexible Cycles**: Configurable draw intervals (minutes, hours, days, weekly, or cron) - 🔄 **Auto-Redraw**: If payout fails, automatically selects a new winner - 🎰 **Random Tickets**: Ticket numbers are randomly generated, not sequential - 📊 **Swagger Docs**: Full API documentation at `/api-docs` +- 🤖 **Telegram Bot**: Full-featured bot for buying tickets and receiving notifications ## Architecture -The system consists of three main components: +The system consists of four main components: 1. **Backend API** (Node.js + TypeScript + Express) 2. **Frontend** (Next.js + React + TypeScript + TailwindCSS) -3. **Database** (PostgreSQL or SQLite) +3. **Telegram Bot** (Node.js + TypeScript + node-telegram-bot-api) +4. **Database** (PostgreSQL or SQLite) ### Backend - RESTful API with comprehensive endpoints - LNbits integration for Lightning payments - Automated scheduler for draws and cycle generation +- Configurable cycle types (minutes, hours, days, weekly, cron) - JWT authentication for Nostr users - Admin API for manual operations - Payment monitoring with polling fallback @@ -36,13 +39,23 @@ The system consists of three main components: ### Frontend - Server-side rendered Next.js application -- Redux state management - Real-time countdown timers - Invoice QR code display with payment animations - Automatic status polling - Nostr NIP-07 authentication - Draw animation with winner reveal - Past winners display +- Fully responsive mobile-first design + +### Telegram Bot + +- Buy tickets directly from Telegram +- Lightning Address management (with 21Tipbot & Bittip integration) +- View tickets and wins +- Notification preferences (draw reminders, results, new jackpot alerts) +- Group support with admin controls +- Customizable reminders (up to 3 reminder slots per group) +- SQLite database for persistent user/group settings ## Quick Start @@ -58,7 +71,8 @@ cd LightningLotto ```bash cp back_end/env.example back_end/.env cp front_end/env.example front_end/.env.local -# Edit both files with your configuration +cp telegram_bot/env.example telegram_bot/.env +# Edit all files with your configuration ``` 3. Start services: @@ -113,6 +127,22 @@ npm run build npm start ``` +#### Telegram Bot + +```bash +cd telegram_bot +npm install +cp env.example .env +# Edit .env with your bot token and API URL + +# Run development server +npm run dev + +# Or build and run production +npm run build +npm start +``` + ## Configuration ### Required Environment Variables @@ -138,6 +168,15 @@ ADMIN_API_KEY=your-admin-api-key DEFAULT_TICKET_PRICE_SATS=1000 DEFAULT_HOUSE_FEE_PERCENT=5 PAYOUT_MAX_ATTEMPTS_BEFORE_REDRAW=2 + +# Cycle Configuration (choose one type) +CYCLE_TYPE=hours # minutes | hours | days | weekly | cron +CYCLE_INTERVAL_MINUTES=30 # For 'minutes' type +CYCLE_INTERVAL_HOURS=1 # For 'hours' type +CYCLE_DAILY_TIME=20:00 # For 'days' type (HH:MM UTC) +CYCLE_WEEKLY_DAY=saturday # For 'weekly' type +CYCLE_WEEKLY_TIME=20:00 # For 'weekly' type (HH:MM UTC) +CYCLE_CRON_EXPRESSION=0 */6 * * * # For 'cron' type ``` #### Frontend (.env.local) @@ -146,7 +185,16 @@ PAYOUT_MAX_ATTEMPTS_BEFORE_REDRAW=2 NEXT_PUBLIC_API_BASE_URL=http://localhost:3000 ``` -See `back_end/env.example` and `front_end/env.example` for all configuration options. +#### Telegram Bot (.env) + +```bash +TELEGRAM_BOT_TOKEN=your-telegram-bot-token +API_BASE_URL=http://localhost:3000 +FRONTEND_BASE_URL=http://localhost:3001 +# BOT_DATABASE_PATH=./data/bot.db # Optional, defaults to ./data/bot.db +``` + +See `back_end/env.example`, `front_end/env.example`, and `telegram_bot/env.example` for all configuration options. ## API Endpoints @@ -186,9 +234,39 @@ See `back_end/env.example` and `front_end/env.example` for all configuration opt Full API documentation available at `/api-docs` (Swagger UI). +## Telegram Bot Commands + +### Private Chat Commands + +| Command | Description | +|---------|-------------| +| `/start` | Start the bot and register | +| `/lottomenu` | Show main menu | +| `/buyticket` | Buy lottery tickets | +| `/tickets` | View your tickets | +| `/wins` | View your wins | +| `/lottoaddress` | Set your Lightning Address | +| `/lottosettings` | Notification settings | +| `/lottohelp` | Show help | + +### Group Commands (Admin Only) + +| Command | Description | +|---------|-------------| +| `/lottosettings` | Configure group lottery settings | +| `/jackpot` | Show current jackpot info | + +### Group Features + +- Enable/disable draw announcements +- Enable/disable new jackpot alerts +- Up to 3 customizable draw reminders +- Adjustable reminder times (minutes/hours/days) +- Auto-deleting settings messages (2 min TTL) + ## Database Schema -7 main tables: +### Backend Database (7 main tables) - `lotteries` - Lottery configuration - `jackpot_cycles` - Draw cycles with status and winners @@ -198,15 +276,23 @@ Full API documentation available at `/api-docs` (Swagger UI). - `users` - Nostr user accounts (optional) - `draw_logs` - Audit trail for transparency +### Telegram Bot Database (SQLite) + +- `users` - Telegram user profiles and notification preferences +- `user_purchases` - Tracks purchases per user +- `cycle_participants` - Tracks participants per cycle +- `groups` - Group settings and reminder configurations + ## How It Works 1. **Cycle Generation**: Scheduler automatically creates future draw cycles -2. **Ticket Purchase**: Users buy tickets, receive Lightning invoice +2. **Ticket Purchase**: Users buy tickets via web or Telegram, receive Lightning invoice 3. **Payment Processing**: LNbits webhook or polling confirms payment 4. **Ticket Issuance**: Random ticket numbers assigned in database transaction 5. **Draw Execution**: At scheduled time, winner selected using CSPRNG -6. **Payout**: Winner's Lightning Address paid automatically -7. **Retry/Redraw**: Failed payouts retried; new winner drawn after max attempts +6. **Notifications**: Telegram bot sends draw results to participants and groups +7. **Payout**: Winner's Lightning Address paid automatically +8. **Retry/Redraw**: Failed payouts retried; new winner drawn after max attempts ## Security Features @@ -219,6 +305,7 @@ Full API documentation available at `/api-docs` (Swagger UI). - `crypto.randomBytes()` for winner selection - `crypto.randomInt()` for ticket number generation - No floating-point math (BIGINT for all sats) +- Lightning Address LNURL verification ## Frontend Pages @@ -233,21 +320,18 @@ Full API documentation available at `/api-docs` (Swagger UI). | `/dashboard/tickets` | User's ticket history | | `/dashboard/wins` | User's win history | -## Testing +## Production Deployment -### Backend Tests -```bash -cd back_end -npm test -``` +### Systemd Services & Nginx -### Frontend Tests -```bash -cd front_end -npm test -``` +The `setup/` folder contains production-ready configuration files: -## Deployment +- `lightning-lotto-backend.service` - Backend systemd service +- `lightning-lotto-frontend.service` - Frontend systemd service +- `lightning-lotto-telegram.service` - Telegram bot systemd service +- `nginx-lightning-lotto.conf` - Nginx reverse proxy with SSL + +See `setup/README.md` for detailed deployment instructions. ### Production Considerations @@ -298,11 +382,38 @@ LightningLotto/ │ ├── components/ # React components │ ├── config/ # Frontend config │ ├── constants/ # Text strings -│ ├── lib/ # API client and utilities -│ └── store/ # Redux state +│ └── lib/ # API client and utilities +├── telegram_bot/ +│ ├── src/ +│ │ ├── config/ # Bot configuration +│ │ ├── handlers/ # Command and callback handlers +│ │ ├── messages/ # Centralized message strings +│ │ ├── services/ # Database, API, notifications +│ │ ├── types/ # TypeScript types +│ │ └── utils/ # Keyboards, formatting +│ └── data/ # SQLite database for bot +├── setup/ # Production deployment configs +│ ├── *.service # Systemd service files +│ ├── nginx-*.conf # Nginx configuration +│ └── README.md # Deployment guide +├── App_info/ # Documentation └── docker-compose.yml ``` +## Testing + +### Backend Tests +```bash +cd back_end +npm test +``` + +### Frontend Tests +```bash +cd front_end +npm test +``` + ## License MIT License - see LICENSE file for details @@ -311,6 +422,7 @@ MIT License - see LICENSE file for details - Built with [LNbits](https://lnbits.com/) for Lightning Network integration - Uses [Nostr](https://nostr.com/) for decentralized authentication +- Telegram bot integration for mobile-first experience - Inspired by the Bitcoin Lightning Network community --- diff --git a/back_end/src/scheduler/index.ts b/back_end/src/scheduler/index.ts index 6d2543f..224a108 100644 --- a/back_end/src/scheduler/index.ts +++ b/back_end/src/scheduler/index.ts @@ -242,6 +242,8 @@ async function checkAndExecuteDraws(): Promise { for (const cycle of result.rows) { console.log(`Executing draw for cycle ${cycle.id} (${cycle.cycle_type})`); await executeDraw(cycle.id); + // Cancel unpaid invoices for this cycle after draw + await cancelUnpaidPurchases(cycle.id); } } catch (error) { @@ -249,6 +251,30 @@ async function checkAndExecuteDraws(): Promise { } } +/** + * Cancel unpaid purchases after a draw completes + * This prevents payments for invoices from past rounds + */ +async function cancelUnpaidPurchases(cycleId: string): Promise { + try { + const result = await db.query( + `UPDATE ticket_purchases + SET status = 'cancelled', updated_at = NOW() + WHERE cycle_id = $1 + AND status = 'pending' + RETURNING id`, + [cycleId] + ); + + const cancelledCount = result.rows.length; + if (cancelledCount > 0) { + console.log(`Cancelled ${cancelledCount} unpaid purchases for cycle ${cycleId}`); + } + } catch (error) { + console.error('Error cancelling unpaid purchases:', error); + } +} + /** * Update cycles to 'sales_open' status when appropriate */ diff --git a/telegram_bot/botfather_commands.txt b/telegram_bot/botfather_commands.txt new file mode 100644 index 0000000..30bef4d --- /dev/null +++ b/telegram_bot/botfather_commands.txt @@ -0,0 +1,71 @@ +================================ +BOTFATHER COMMAND SETUP +================================ + +Send to @BotFather: /setcommands +Then select your bot and paste the commands below: + +================================ +PRIVATE CHAT COMMANDS +================================ + +start - Start the bot and register +lottomenu - Show main menu +buyticket - Buy lottery tickets +tickets - View your tickets +wins - View your wins +lottoaddress - Set your Lightning Address +lottosettings - Notification settings +lottohelp - Show help +jackpot - Show current jackpot + +================================ +GROUP COMMANDS (Optional) +================================ + +If you want separate commands for groups, use /setcommands again +and select "Edit commands for groups": + +jackpot - Show current jackpot info +lottosettings - Configure lottery settings (admin only) +lottohelp - Show help and commands + +================================ +BOT DESCRIPTION +================================ + +Send to @BotFather: /setdescription + +Lightning Lotto - Win Bitcoin on the Lightning Network! ⚡ + +Buy lottery tickets with Lightning, get instant payouts if you win. +• Buy tickets with sats +• Set your Lightning Address for payouts +• Get notified about draws and wins +• Join groups to get jackpot alerts + +================================ +ABOUT TEXT +================================ + +Send to @BotFather: /setabouttext + +⚡ Lightning Lotto Bot ⚡ + +A provably fair Bitcoin lottery powered by the Lightning Network. + +🎟️ Buy tickets with Lightning +💰 Instant payouts to your Lightning Address +🔔 Draw reminders & win notifications +👥 Group support with announcements + +Start with /start to begin! + +================================ +SHORT DESCRIPTION +================================ + +Send to @BotFather: /setshortdescription + +Win Bitcoin on the Lightning Network! Buy lottery tickets, get instant payouts. ⚡ + diff --git a/telegram_bot/src/handlers/groups.ts b/telegram_bot/src/handlers/groups.ts index 1654ebc..b9fa47b 100644 --- a/telegram_bot/src/handlers/groups.ts +++ b/telegram_bot/src/handlers/groups.ts @@ -6,6 +6,7 @@ import { GroupSettings, REMINDER_PRESETS, ANNOUNCEMENT_DELAY_OPTIONS, + NEW_JACKPOT_DELAY_OPTIONS, DEFAULT_GROUP_REMINDER_SLOTS, ReminderTime, formatReminderTime, @@ -268,6 +269,20 @@ export async function handleGroupSettingsCallback( } } + // Handle new jackpot delay selection + if (action.startsWith('newjackpot_delay_')) { + const minutes = parseInt(action.replace('newjackpot_delay_', ''), 10); + if (!isNaN(minutes)) { + updatedSettings = await groupStateManager.updateNewJackpotDelay(chatId, minutes); + if (updatedSettings) { + logUserAction(userId, 'Updated new jackpot delay', { groupId: chatId, minutes }); + await bot.answerCallbackQuery(query.id, { + text: minutes === 0 ? 'Announce immediately' : `Announce ${minutes} min after new jackpot` + }); + } + } + } + // Handle reminder time adjustments (reminder1_add_1_hours, reminder2_sub_1_days, etc.) const reminderTimeMatch = action.match(/^reminder(\d)_(add|sub)_(\d+)_(minutes|hours|days)$/); if (reminderTimeMatch) { @@ -334,7 +349,7 @@ export async function handleGroupSettingsCallback( /** - * Format delay option for display + * Format delay option for display (seconds) */ function formatDelayOption(seconds: number): string { if (seconds === 0) return 'Instant'; @@ -345,6 +360,14 @@ function formatDelayOption(seconds: number): string { return `${seconds}s`; } +/** + * Format new jackpot delay option for display (minutes) + */ +function formatNewJackpotDelay(minutes: number): string { + if (minutes === 0) return 'Instant'; + return minutes === 1 ? '1 min' : `${minutes} min`; +} + /** * Get time adjustment buttons for a reminder slot */ @@ -385,12 +408,23 @@ function getGroupSettingsKeyboard(settings: GroupSettings): TelegramBot.InlineKe text: `${onOff(settings.newJackpotAnnouncement)} New Jackpot Announcement`, callback_data: 'group_toggle_newjackpot', }], - [{ - text: `${onOff(settings.drawAnnouncements)} Draw Result Announcements`, - callback_data: 'group_toggle_announcements', - }], ]; + // Add new jackpot delay options if enabled + if (settings.newJackpotAnnouncement !== false) { + keyboard.push( + NEW_JACKPOT_DELAY_OPTIONS.map(minutes => ({ + text: `${selected(settings.newJackpotDelayMinutes ?? 5, minutes)} ${formatNewJackpotDelay(minutes)}`, + callback_data: `group_newjackpot_delay_${minutes}`, + })) + ); + } + + keyboard.push([{ + text: `${onOff(settings.drawAnnouncements)} Draw Result Announcements`, + callback_data: 'group_toggle_announcements', + }]); + // Add announcement delay options if announcements are enabled if (settings.drawAnnouncements) { keyboard.push( diff --git a/telegram_bot/src/index.ts b/telegram_bot/src/index.ts index 594b194..2fdbef8 100644 --- a/telegram_bot/src/index.ts +++ b/telegram_bot/src/index.ts @@ -354,6 +354,13 @@ bot.on('callback_query', async (query) => { return; } + // Handle new jackpot announcement delay selection + if (data.startsWith('group_newjackpot_delay_')) { + const action = data.replace('group_', ''); + await handleGroupSettingsCallback(bot, query, action); + return; + } + // Handle group refresh if (data === 'group_refresh') { await handleGroupRefresh(bot, query); diff --git a/telegram_bot/src/messages/index.ts b/telegram_bot/src/messages/index.ts index 35e2dd3..aa6b5a8 100644 --- a/telegram_bot/src/messages/index.ts +++ b/telegram_bot/src/messages/index.ts @@ -603,6 +603,7 @@ To buy tickets privately, message me directly! 🎟`, reminder3Enabled?: boolean; reminder3Time?: { value: number; unit: string }; announcementDelaySeconds?: number; + newJackpotDelayMinutes?: number; }) => { const announceDelay = settings.announcementDelaySeconds ?? 10; const formatAnnounce = announceDelay === 0 @@ -610,6 +611,11 @@ To buy tickets privately, message me directly! 🎟`, : announceDelay >= 60 ? `${announceDelay / 60} min after draw` : `${announceDelay}s after draw`; + + const newJackpotDelay = settings.newJackpotDelayMinutes ?? 5; + const formatNewJackpotDelay = newJackpotDelay === 0 + ? 'Immediately' + : `${newJackpotDelay} min after start`; // Format helper for reminder times const formatTime = (t?: { value: number; unit: string }) => { @@ -639,7 +645,7 @@ To buy tickets privately, message me directly! 🎟`, *Current Configuration:* ${settings.enabled ? '✅' : '❌'} Bot Enabled -${newJackpot ? '✅' : '❌'} New Jackpot Announcements +${newJackpot ? '✅' : '❌'} New Jackpot Announcements ${newJackpot ? `_(${formatNewJackpotDelay})_` : ''} ${settings.drawAnnouncements ? '✅' : '❌'} Draw Announcements ${settings.drawAnnouncements ? `_(${formatAnnounce})_` : ''} ${settings.reminders ? '✅' : '❌'} Draw Reminders ${settings.reminders ? `_(${formatReminderList})_` : ''} ${settings.ticketPurchaseAllowed ? '✅' : '❌'} Ticket Purchases in Group diff --git a/telegram_bot/src/services/database.ts b/telegram_bot/src/services/database.ts index a2e423a..c9b2c75 100644 --- a/telegram_bot/src/services/database.ts +++ b/telegram_bot/src/services/database.ts @@ -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), diff --git a/telegram_bot/src/services/groupState.ts b/telegram_bot/src/services/groupState.ts index 524a8ff..5bd1386 100644 --- a/telegram_bot/src/services/groupState.ts +++ b/telegram_bot/src/services/groupState.ts @@ -81,6 +81,16 @@ class GroupStateManager { return botDatabase.updateAnnouncementDelay(groupId, seconds); } + /** + * Update new jackpot announcement delay + */ + async updateNewJackpotDelay( + groupId: number, + minutes: number + ): Promise { + return botDatabase.updateNewJackpotDelay(groupId, minutes); + } + /** * Get groups with specific feature enabled */ diff --git a/telegram_bot/src/services/notificationScheduler.ts b/telegram_bot/src/services/notificationScheduler.ts index 2ff15c9..8f98d8f 100644 --- a/telegram_bot/src/services/notificationScheduler.ts +++ b/telegram_bot/src/services/notificationScheduler.ts @@ -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(), }); } diff --git a/telegram_bot/src/types/groups.ts b/telegram_bot/src/types/groups.ts index 805cb44..36f2c35 100644 --- a/telegram_bot/src/types/groups.ts +++ b/telegram_bot/src/types/groups.ts @@ -27,6 +27,7 @@ export interface GroupSettings { // Legacy field (kept for backwards compat, no longer used) reminderTimes: ReminderTime[]; announcementDelaySeconds: number; // Delay after draw to send announcement (in seconds) + newJackpotDelayMinutes: number; // Delay after new jackpot starts to send announcement (in minutes) addedBy: number; addedAt: Date; updatedAt: Date; @@ -84,6 +85,11 @@ export function formatReminderTime(rt: ReminderTime): string { */ export const ANNOUNCEMENT_DELAY_OPTIONS = [0, 10, 30, 60, 120]; +/** + * Available new jackpot announcement delay options (minutes after start) + */ +export const NEW_JACKPOT_DELAY_OPTIONS = [0, 1, 5, 10, 15, 30]; + /** * Default group settings */ @@ -101,6 +107,7 @@ export const DEFAULT_GROUP_SETTINGS: Omit