import TelegramBot from 'node-telegram-bot-api'; import { groupStateManager } from '../services/groupState'; import { logger, logUserAction } from '../services/logger'; import { messages } from '../messages'; import { GroupSettings, REMINDER_PRESETS, ANNOUNCEMENT_DELAY_OPTIONS, DEFAULT_GROUP_REMINDER_SLOTS, ReminderTime, formatReminderTime, reminderTimeToMinutes } from '../types/groups'; // Track settings messages for auto-deletion const settingsMessageTimeouts: Map = new Map(); const SETTINGS_MESSAGE_TTL = 2 * 60 * 1000; // 2 minutes /** * Check if a user is an admin in a group */ async function isGroupAdmin( bot: TelegramBot, chatId: number, userId: number ): Promise { try { const member = await bot.getChatMember(chatId, userId); return ['creator', 'administrator'].includes(member.status); } catch (error) { logger.error('Failed to check admin status', { error, chatId, userId }); return false; } } /** * Handle bot being added to a group */ export async function handleBotAddedToGroup( bot: TelegramBot, msg: TelegramBot.Message ): Promise { const chatId = msg.chat.id; const chatTitle = msg.chat.title || 'Unknown Group'; const addedBy = msg.from?.id || 0; logger.info('Bot added to group', { chatId, chatTitle, addedBy }); try { const settings = await groupStateManager.registerGroup(chatId, chatTitle, addedBy); await bot.sendMessage(chatId, messages.groups.welcome(chatTitle), { parse_mode: 'Markdown', }); } catch (error) { logger.error('Failed to register group', { error, chatId }); } } /** * Handle bot being removed from a group */ export async function handleBotRemovedFromGroup( bot: TelegramBot, msg: TelegramBot.Message ): Promise { const chatId = msg.chat.id; logger.info('Bot removed from group', { chatId }); try { await groupStateManager.removeGroup(chatId); } catch (error) { logger.error('Failed to remove group', { error, chatId }); } } /** * Handle /settings command (group admin only) */ export async function handleGroupSettings( bot: TelegramBot, msg: TelegramBot.Message ): Promise { const chatId = msg.chat.id; const userId = msg.from?.id; // Only works in groups if (msg.chat.type === 'private') { await bot.sendMessage(chatId, messages.groups.privateChat); return; } if (!userId) return; // Check if user is admin const isAdmin = await isGroupAdmin(bot, chatId, userId); if (!isAdmin) { await bot.sendMessage(chatId, messages.groups.adminOnly); return; } logUserAction(userId, 'Viewed group settings', { groupId: chatId }); try { const settings = await groupStateManager.getGroup(chatId); if (!settings) { // Register group if not found await groupStateManager.registerGroup( chatId, msg.chat.title || 'Group', userId ); } const currentSettings = settings || await groupStateManager.getGroup(chatId); if (!currentSettings) { await bot.sendMessage(chatId, messages.errors.generic); return; } const sentMessage = await bot.sendMessage( chatId, messages.groups.settingsOverview(currentSettings), { parse_mode: 'Markdown', reply_markup: getGroupSettingsKeyboard(currentSettings), } ); // Schedule auto-delete after 2 minutes scheduleSettingsMessageDeletion(bot, chatId, sentMessage.message_id); } catch (error) { logger.error('Error in handleGroupSettings', { error, chatId }); await bot.sendMessage(chatId, messages.errors.generic); } } /** * Schedule deletion of settings message after 2 minutes */ function scheduleSettingsMessageDeletion( bot: TelegramBot, chatId: number, messageId: number ): void { const key = `${chatId}:${messageId}`; // Clear any existing timeout for this message const existingTimeout = settingsMessageTimeouts.get(key); if (existingTimeout) { clearTimeout(existingTimeout); } // Schedule new deletion const timeout = setTimeout(async () => { try { await bot.deleteMessage(chatId, messageId); logger.debug('Auto-deleted settings message', { chatId, messageId }); } catch (error) { // Ignore errors (message might already be deleted) } settingsMessageTimeouts.delete(key); }, SETTINGS_MESSAGE_TTL); settingsMessageTimeouts.set(key, timeout); } /** * Handle group settings toggle callback */ export async function handleGroupSettingsCallback( bot: TelegramBot, query: TelegramBot.CallbackQuery, action: string ): Promise { const chatId = query.message?.chat.id; const userId = query.from.id; const messageId = query.message?.message_id; if (!chatId || !messageId) return; // Check if user is admin const isAdmin = await isGroupAdmin(bot, chatId, userId); if (!isAdmin) { await bot.answerCallbackQuery(query.id, { text: messages.groups.adminOnly, show_alert: true, }); return; } // Refresh auto-delete timer on any interaction scheduleSettingsMessageDeletion(bot, chatId, messageId); try { const currentSettings = await groupStateManager.getGroup(chatId); if (!currentSettings) { await bot.answerCallbackQuery(query.id, { text: 'Group not found' }); return; } let updatedSettings: GroupSettings | null = null; // Handle toggle actions if (action.startsWith('toggle_')) { let setting: 'enabled' | 'drawAnnouncements' | 'reminders' | 'ticketPurchaseAllowed' | 'newJackpotAnnouncement' | 'reminder1Enabled' | 'reminder2Enabled' | 'reminder3Enabled'; switch (action) { case 'toggle_enabled': setting = 'enabled'; break; case 'toggle_announcements': setting = 'drawAnnouncements'; break; case 'toggle_reminders': setting = 'reminders'; break; case 'toggle_purchases': setting = 'ticketPurchaseAllowed'; break; case 'toggle_newjackpot': setting = 'newJackpotAnnouncement'; break; case 'toggle_reminder1': setting = 'reminder1Enabled'; break; case 'toggle_reminder2': setting = 'reminder2Enabled'; break; case 'toggle_reminder3': setting = 'reminder3Enabled'; break; default: await bot.answerCallbackQuery(query.id); return; } const currentValue = currentSettings[setting] !== false; // Default true for new settings const newValue = !currentValue; updatedSettings = await groupStateManager.updateSetting(chatId, setting, newValue); if (updatedSettings) { logUserAction(userId, 'Updated group setting', { groupId: chatId, setting, newValue, }); await bot.answerCallbackQuery(query.id, { text: `${setting} ${newValue ? 'enabled' : 'disabled'}`, }); } } // Legacy handlers removed - now using 3-slot reminder system with toggle_reminder1/2/3 and time adjustments // Handle announcement delay selection if (action.startsWith('announce_delay_')) { const seconds = parseInt(action.replace('announce_delay_', ''), 10); if (!isNaN(seconds)) { updatedSettings = await groupStateManager.updateAnnouncementDelay(chatId, seconds); if (updatedSettings) { logUserAction(userId, 'Updated announcement delay', { groupId: chatId, seconds }); await bot.answerCallbackQuery(query.id, { text: seconds === 0 ? 'Announce immediately' : `Announce ${seconds}s after draw` }); } } } // 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) { const slot = parseInt(reminderTimeMatch[1], 10) as 1 | 2 | 3; const operation = reminderTimeMatch[2] as 'add' | 'sub'; const amount = parseInt(reminderTimeMatch[3], 10); const unit = reminderTimeMatch[4] as 'minutes' | 'hours' | 'days'; // Get current time for this slot const currentTimeKey = `reminder${slot}Time` as 'reminder1Time' | 'reminder2Time' | 'reminder3Time'; const defaultTimes: Record = { reminder1Time: { value: 1, unit: 'hours' }, reminder2Time: { value: 1, unit: 'days' }, reminder3Time: { value: 6, unit: 'days' }, }; const currentTime = currentSettings[currentTimeKey] || defaultTimes[currentTimeKey]; // Convert to minutes for calculation const currentMinutes = reminderTimeToMinutes(currentTime); const adjustMinutes = unit === 'minutes' ? amount : unit === 'hours' ? amount * 60 : amount * 60 * 24; const newMinutes = operation === 'add' ? currentMinutes + adjustMinutes : Math.max(1, currentMinutes - adjustMinutes); // Minimum 1 minute // Convert back to best unit let newTime: ReminderTime; if (newMinutes >= 1440 && newMinutes % 1440 === 0) { newTime = { value: newMinutes / 1440, unit: 'days' }; } else if (newMinutes >= 60 && newMinutes % 60 === 0) { newTime = { value: newMinutes / 60, unit: 'hours' }; } else { newTime = { value: newMinutes, unit: 'minutes' }; } updatedSettings = await groupStateManager.updateReminderTime(chatId, slot, newTime); if (updatedSettings) { logUserAction(userId, 'Updated reminder time', { groupId: chatId, slot, newTime }); await bot.answerCallbackQuery(query.id, { text: `Reminder ${slot}: ${formatReminderTime(newTime)} before draw` }); } } if (!updatedSettings) { await bot.answerCallbackQuery(query.id, { text: 'Failed to update' }); return; } // Update the message with new settings await bot.editMessageText( messages.groups.settingsOverview(updatedSettings), { chat_id: chatId, message_id: messageId, parse_mode: 'Markdown', reply_markup: getGroupSettingsKeyboard(updatedSettings), } ); } catch (error) { logger.error('Error in handleGroupSettingsCallback', { error, chatId, action }); await bot.answerCallbackQuery(query.id, { text: 'Error updating settings' }); } } /** * Format delay option for display */ function formatDelayOption(seconds: number): string { if (seconds === 0) return 'Instant'; if (seconds >= 60) { const minutes = seconds / 60; return minutes === 1 ? '1 min' : `${minutes} min`; } return `${seconds}s`; } /** * Get time adjustment buttons for a reminder slot */ function getReminderTimeAdjustButtons(slot: number, currentTime: ReminderTime): TelegramBot.InlineKeyboardButton[] { return [ { text: '−1m', callback_data: `group_reminder${slot}_sub_1_minutes` }, { text: '+1m', callback_data: `group_reminder${slot}_add_1_minutes` }, { text: '−1h', callback_data: `group_reminder${slot}_sub_1_hours` }, { text: '+1h', callback_data: `group_reminder${slot}_add_1_hours` }, { text: '−1d', callback_data: `group_reminder${slot}_sub_1_days` }, { text: '+1d', callback_data: `group_reminder${slot}_add_1_days` }, ]; } /** * Check if a reminder time is already set */ function hasReminder(settings: GroupSettings, rt: ReminderTime): boolean { if (!settings.reminderTimes) return false; return settings.reminderTimes.some( r => r.value === rt.value && r.unit === rt.unit ); } /** * Generate inline keyboard for group settings */ function getGroupSettingsKeyboard(settings: GroupSettings): TelegramBot.InlineKeyboardMarkup { const onOff = (val: boolean | undefined) => val !== false ? '✅' : '❌'; const selected = (current: number, option: number) => current === option ? '●' : '○'; const keyboard: TelegramBot.InlineKeyboardButton[][] = [ [{ text: `${onOff(settings.enabled)} Bot Enabled`, callback_data: 'group_toggle_enabled', }], [{ 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 announcement delay options if announcements are enabled if (settings.drawAnnouncements) { keyboard.push( ANNOUNCEMENT_DELAY_OPTIONS.map(seconds => ({ text: `${selected(settings.announcementDelaySeconds || 0, seconds)} ${formatDelayOption(seconds)}`, callback_data: `group_announce_delay_${seconds}`, })) ); } keyboard.push([{ text: `${onOff(settings.reminders)} Draw Reminders`, callback_data: 'group_toggle_reminders', }]); // Add 3-tier reminder options if reminders are enabled if (settings.reminders) { // Get default values with fallback for migration const r1Enabled = settings.reminder1Enabled !== false; const r2Enabled = settings.reminder2Enabled === true; const r3Enabled = settings.reminder3Enabled === true; // Get times with defaults const r1Time = settings.reminder1Time || { value: 1, unit: 'hours' as const }; const r2Time = settings.reminder2Time || { value: 1, unit: 'days' as const }; const r3Time = settings.reminder3Time || { value: 6, unit: 'days' as const }; // Reminder 1 keyboard.push([{ text: `${onOff(r1Enabled)} Reminder 1: ${formatReminderTime(r1Time)} before`, callback_data: 'group_toggle_reminder1', }]); if (r1Enabled) { keyboard.push(getReminderTimeAdjustButtons(1, r1Time)); } // Reminder 2 keyboard.push([{ text: `${onOff(r2Enabled)} Reminder 2: ${formatReminderTime(r2Time)} before`, callback_data: 'group_toggle_reminder2', }]); if (r2Enabled) { keyboard.push(getReminderTimeAdjustButtons(2, r2Time)); } // Reminder 3 keyboard.push([{ text: `${onOff(r3Enabled)} Reminder 3: ${formatReminderTime(r3Time)} before`, callback_data: 'group_toggle_reminder3', }]); if (r3Enabled) { keyboard.push(getReminderTimeAdjustButtons(3, r3Time)); } } keyboard.push( [{ text: `${onOff(settings.ticketPurchaseAllowed)} Allow Ticket Purchases`, callback_data: 'group_toggle_purchases', }], [{ text: '🔄 Refresh', callback_data: 'group_refresh', }] ); return { inline_keyboard: keyboard }; } /** * Handle refresh callback */ export async function handleGroupRefresh( bot: TelegramBot, query: TelegramBot.CallbackQuery ): Promise { const chatId = query.message?.chat.id; const messageId = query.message?.message_id; if (!chatId || !messageId) return; // Refresh auto-delete timer scheduleSettingsMessageDeletion(bot, chatId, messageId); await bot.answerCallbackQuery(query.id, { text: 'Refreshed!' }); const settings = await groupStateManager.getGroup(chatId); if (!settings) return; await bot.editMessageText( messages.groups.settingsOverview(settings), { chat_id: chatId, message_id: messageId, parse_mode: 'Markdown', reply_markup: getGroupSettingsKeyboard(settings), } ); } /** * Send draw announcement to all enabled groups */ export async function broadcastDrawAnnouncement( bot: TelegramBot, announcement: string ): Promise { const groups = await groupStateManager.getGroupsWithFeature('drawAnnouncements'); let sent = 0; for (const group of groups) { try { await bot.sendMessage(group.groupId, announcement, { parse_mode: 'Markdown' }); sent++; } catch (error) { logger.error('Failed to send announcement to group', { groupId: group.groupId, error, }); // If bot was removed from group, clean up if ((error as any)?.response?.statusCode === 403) { await groupStateManager.removeGroup(group.groupId); } } } logger.info('Broadcast draw announcement', { sent, total: groups.length }); return sent; } /** * Send draw reminder to all enabled groups */ export async function broadcastDrawReminder( bot: TelegramBot, reminder: string ): Promise { const groups = await groupStateManager.getGroupsWithFeature('reminders'); let sent = 0; for (const group of groups) { try { await bot.sendMessage(group.groupId, reminder, { parse_mode: 'Markdown' }); sent++; } catch (error) { logger.error('Failed to send reminder to group', { groupId: group.groupId, error, }); if ((error as any)?.response?.statusCode === 403) { await groupStateManager.removeGroup(group.groupId); } } } logger.info('Broadcast draw reminder', { sent, total: groups.length }); return sent; } export default { handleBotAddedToGroup, handleBotRemovedFromGroup, handleGroupSettings, handleGroupSettingsCallback, handleGroupRefresh, broadcastDrawAnnouncement, broadcastDrawReminder, };