From 00f09236a3a53bfab9546b07806f3716e3a684ea Mon Sep 17 00:00:00 2001 From: Michilis Date: Fri, 12 Dec 2025 15:28:05 +0000 Subject: [PATCH] feat(telegram): improve group handling and default display name to @username - /lottosettings now opens settings in private DM instead of group - Bot only reacts to / commands in groups (keyboard buttons ignored) - Reply keyboard buttons removed from group messages - Default display name now uses @username instead of 'Anon' - Users can still manually update display name in settings - Updated all display name usages to use centralized getDisplayName() --- telegram_bot/src/handlers/buy.ts | 20 +- telegram_bot/src/handlers/groups.ts | 174 +++++++++++------- telegram_bot/src/handlers/settings.ts | 11 +- telegram_bot/src/index.ts | 44 ++--- telegram_bot/src/messages/index.ts | 3 +- telegram_bot/src/services/database.ts | 9 +- .../src/services/notificationScheduler.ts | 4 +- telegram_bot/src/services/state.ts | 10 +- 8 files changed, 164 insertions(+), 111 deletions(-) diff --git a/telegram_bot/src/handlers/buy.ts b/telegram_bot/src/handlers/buy.ts index 87bafd8..b9d9bd2 100644 --- a/telegram_bot/src/handlers/buy.ts +++ b/telegram_bot/src/handlers/buy.ts @@ -15,6 +15,13 @@ import { formatSats, formatDate, formatTimeUntil } from '../utils/format'; import { PendingPurchaseData, AwaitingPaymentData } from '../types'; import { messages } from '../messages'; +/** + * Check if chat is a group (negative ID for supergroups, or check type) + */ +function isGroupChat(chatId: number): boolean { + return chatId < 0; +} + /** * Handle /buy command or "Buy Tickets" button */ @@ -293,11 +300,11 @@ export async function handlePurchaseConfirmation( logUserAction(userId, 'Confirmed purchase', { tickets: pendingData.ticketCount }); - // Create invoice with user's display name + // Create invoice with user's display name (defaults to @username) const purchaseResult = await apiClient.buyTickets( pendingData.ticketCount, user.lightningAddress, - user.displayName || 'Anon' + stateManager.getDisplayName(user) ); logPaymentEvent(userId, purchaseResult.ticket_purchase_id, 'created', { @@ -352,7 +359,8 @@ export async function handlePurchaseConfirmation( } catch (error) { logger.error('Error in handlePurchaseConfirmation', { error, userId }); await bot.sendMessage(chatId, messages.errors.invoiceCreationFailed, { - reply_markup: getMainMenuKeyboard(), + // Only show reply keyboard in private chats, not in groups + ...(isGroupChat(chatId) ? {} : { reply_markup: getMainMenuKeyboard() }), }); await stateManager.clearUserStateData(userId); } @@ -413,7 +421,8 @@ async function pollPaymentStatus( await bot.sendMessage(chatId, messages.buy.invoiceExpired, { parse_mode: 'Markdown', - reply_markup: getMainMenuKeyboard(), + // Only show reply keyboard in private chats, not in groups + ...(isGroupChat(chatId) ? {} : { reply_markup: getMainMenuKeyboard() }), }); await stateManager.clearUserStateData(userId); return; @@ -467,7 +476,8 @@ async function pollPaymentStatus( await bot.sendMessage(chatId, messages.buy.invoiceExpiredShort, { parse_mode: 'Markdown', - reply_markup: getMainMenuKeyboard(), + // Only show reply keyboard in private chats, not in groups + ...(isGroupChat(chatId) ? {} : { reply_markup: getMainMenuKeyboard() }), }); await stateManager.clearUserStateData(userId); return; diff --git a/telegram_bot/src/handlers/groups.ts b/telegram_bot/src/handlers/groups.ts index b9fa47b..12946d6 100644 --- a/telegram_bot/src/handlers/groups.ts +++ b/telegram_bot/src/handlers/groups.ts @@ -77,6 +77,7 @@ export async function handleBotRemovedFromGroup( /** * Handle /settings command (group admin only) + * Opens settings in private DM, not in the group */ export async function handleGroupSettings( bot: TelegramBot, @@ -120,17 +121,33 @@ export async function handleGroupSettings( return; } - const sentMessage = await bot.sendMessage( - chatId, - messages.groups.settingsOverview(currentSettings), - { - parse_mode: 'Markdown', - reply_markup: getGroupSettingsKeyboard(currentSettings), - } - ); + // Send settings to user's private DM + try { + const sentMessage = await bot.sendMessage( + userId, + messages.groups.settingsOverview(currentSettings), + { + parse_mode: 'Markdown', + reply_markup: getGroupSettingsKeyboard(currentSettings), + } + ); - // Schedule auto-delete after 2 minutes - scheduleSettingsMessageDeletion(bot, chatId, sentMessage.message_id); + // Schedule auto-delete after 2 minutes + scheduleSettingsMessageDeletion(bot, userId, sentMessage.message_id); + + // Notify in the group that settings were sent to DM + await bot.sendMessage( + chatId, + `⚙️ @${msg.from?.username || 'Admin'}, I've sent the group settings to your DMs!` + ); + } catch (dmError) { + // If DM fails (user hasn't started the bot), prompt them to start it + logger.warn('Failed to send settings DM', { error: dmError, userId }); + await bot.sendMessage( + chatId, + `⚙️ @${msg.from?.username || 'Admin'}, please start a private chat with me first (@LightningLottoBot) so I can send you the settings.` + ); + } } catch (error) { logger.error('Error in handleGroupSettings', { error, chatId }); await bot.sendMessage(chatId, messages.errors.generic); @@ -167,22 +184,45 @@ function scheduleSettingsMessageDeletion( settingsMessageTimeouts.set(key, timeout); } +/** + * Extract group ID from the end of a callback action string + * e.g., "toggle_enabled_-123456789" -> { action: "toggle_enabled", groupId: -123456789 } + */ +function parseGroupAction(fullAction: string): { action: string; groupId: number } | null { + // Match action followed by underscore and group ID (negative or positive number) + const match = fullAction.match(/^(.+)_(-?\d+)$/); + if (!match) return null; + const groupId = parseInt(match[2], 10); + if (isNaN(groupId)) return null; + return { action: match[1], groupId }; +} + /** * Handle group settings toggle callback + * Settings can now be managed from DMs, so group ID is included in callback data */ export async function handleGroupSettingsCallback( bot: TelegramBot, query: TelegramBot.CallbackQuery, action: string ): Promise { - const chatId = query.message?.chat.id; + const dmChatId = query.message?.chat.id; const userId = query.from.id; const messageId = query.message?.message_id; - if (!chatId || !messageId) return; + if (!dmChatId || !messageId) return; - // Check if user is admin - const isAdmin = await isGroupAdmin(bot, chatId, userId); + // Parse the action to extract the group ID + const parsed = parseGroupAction(action); + if (!parsed) { + await bot.answerCallbackQuery(query.id, { text: 'Invalid action' }); + return; + } + + const { action: settingAction, groupId } = parsed; + + // Check if user is admin of the target group + const isAdmin = await isGroupAdmin(bot, groupId, userId); if (!isAdmin) { await bot.answerCallbackQuery(query.id, { text: messages.groups.adminOnly, @@ -191,11 +231,11 @@ export async function handleGroupSettingsCallback( return; } - // Refresh auto-delete timer on any interaction - scheduleSettingsMessageDeletion(bot, chatId, messageId); + // Refresh auto-delete timer on any interaction (in the DM) + scheduleSettingsMessageDeletion(bot, dmChatId, messageId); try { - const currentSettings = await groupStateManager.getGroup(chatId); + const currentSettings = await groupStateManager.getGroup(groupId); if (!currentSettings) { await bot.answerCallbackQuery(query.id, { text: 'Group not found' }); return; @@ -204,10 +244,10 @@ export async function handleGroupSettingsCallback( let updatedSettings: GroupSettings | null = null; // Handle toggle actions - if (action.startsWith('toggle_')) { + if (settingAction.startsWith('toggle_')) { let setting: 'enabled' | 'drawAnnouncements' | 'reminders' | 'ticketPurchaseAllowed' | 'newJackpotAnnouncement' | 'reminder1Enabled' | 'reminder2Enabled' | 'reminder3Enabled'; - switch (action) { + switch (settingAction) { case 'toggle_enabled': setting = 'enabled'; break; @@ -239,11 +279,11 @@ export async function handleGroupSettingsCallback( const currentValue = currentSettings[setting] !== false; // Default true for new settings const newValue = !currentValue; - updatedSettings = await groupStateManager.updateSetting(chatId, setting, newValue); + updatedSettings = await groupStateManager.updateSetting(groupId, setting, newValue); if (updatedSettings) { logUserAction(userId, 'Updated group setting', { - groupId: chatId, + groupId, setting, newValue, }); @@ -255,13 +295,14 @@ export async function handleGroupSettingsCallback( // 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); + // Handle announcement delay selection (announce_delay_30 -> seconds=30) + const announceDelayMatch = settingAction.match(/^announce_delay_(\d+)$/); + if (announceDelayMatch) { + const seconds = parseInt(announceDelayMatch[1], 10); if (!isNaN(seconds)) { - updatedSettings = await groupStateManager.updateAnnouncementDelay(chatId, seconds); + updatedSettings = await groupStateManager.updateAnnouncementDelay(groupId, seconds); if (updatedSettings) { - logUserAction(userId, 'Updated announcement delay', { groupId: chatId, seconds }); + logUserAction(userId, 'Updated announcement delay', { groupId, seconds }); await bot.answerCallbackQuery(query.id, { text: seconds === 0 ? 'Announce immediately' : `Announce ${seconds}s after draw` }); @@ -269,13 +310,14 @@ export async function handleGroupSettingsCallback( } } - // Handle new jackpot delay selection - if (action.startsWith('newjackpot_delay_')) { - const minutes = parseInt(action.replace('newjackpot_delay_', ''), 10); + // Handle new jackpot delay selection (newjackpot_delay_5 -> minutes=5) + const newJackpotDelayMatch = settingAction.match(/^newjackpot_delay_(\d+)$/); + if (newJackpotDelayMatch) { + const minutes = parseInt(newJackpotDelayMatch[1], 10); if (!isNaN(minutes)) { - updatedSettings = await groupStateManager.updateNewJackpotDelay(chatId, minutes); + updatedSettings = await groupStateManager.updateNewJackpotDelay(groupId, minutes); if (updatedSettings) { - logUserAction(userId, 'Updated new jackpot delay', { groupId: chatId, minutes }); + logUserAction(userId, 'Updated new jackpot delay', { groupId, minutes }); await bot.answerCallbackQuery(query.id, { text: minutes === 0 ? 'Announce immediately' : `Announce ${minutes} min after new jackpot` }); @@ -284,7 +326,7 @@ export async function handleGroupSettingsCallback( } // 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)$/); + const reminderTimeMatch = settingAction.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'; @@ -317,9 +359,9 @@ export async function handleGroupSettingsCallback( newTime = { value: newMinutes, unit: 'minutes' }; } - updatedSettings = await groupStateManager.updateReminderTime(chatId, slot, newTime); + updatedSettings = await groupStateManager.updateReminderTime(groupId, slot, newTime); if (updatedSettings) { - logUserAction(userId, 'Updated reminder time', { groupId: chatId, slot, newTime }); + logUserAction(userId, 'Updated reminder time', { groupId, slot, newTime }); await bot.answerCallbackQuery(query.id, { text: `Reminder ${slot}: ${formatReminderTime(newTime)} before draw` }); @@ -331,18 +373,18 @@ export async function handleGroupSettingsCallback( return; } - // Update the message with new settings + // Update the message with new settings (in the DM) await bot.editMessageText( messages.groups.settingsOverview(updatedSettings), { - chat_id: chatId, + chat_id: dmChatId, message_id: messageId, parse_mode: 'Markdown', reply_markup: getGroupSettingsKeyboard(updatedSettings), } ); } catch (error) { - logger.error('Error in handleGroupSettingsCallback', { error, chatId, action }); + logger.error('Error in handleGroupSettingsCallback', { error, groupId, action }); await bot.answerCallbackQuery(query.id, { text: 'Error updating settings' }); } } @@ -371,14 +413,14 @@ function formatNewJackpotDelay(minutes: number): string { /** * Get time adjustment buttons for a reminder slot */ -function getReminderTimeAdjustButtons(slot: number, currentTime: ReminderTime): TelegramBot.InlineKeyboardButton[] { +function getReminderTimeAdjustButtons(slot: number, currentTime: ReminderTime, groupId: number): 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` }, + { text: '−1m', callback_data: `group_reminder${slot}_sub_1_minutes_${groupId}` }, + { text: '+1m', callback_data: `group_reminder${slot}_add_1_minutes_${groupId}` }, + { text: '−1h', callback_data: `group_reminder${slot}_sub_1_hours_${groupId}` }, + { text: '+1h', callback_data: `group_reminder${slot}_add_1_hours_${groupId}` }, + { text: '−1d', callback_data: `group_reminder${slot}_sub_1_days_${groupId}` }, + { text: '+1d', callback_data: `group_reminder${slot}_add_1_days_${groupId}` }, ]; } @@ -394,19 +436,21 @@ function hasReminder(settings: GroupSettings, rt: ReminderTime): boolean { /** * Generate inline keyboard for group settings + * groupId is included in callback data so settings can be managed from DMs */ function getGroupSettingsKeyboard(settings: GroupSettings): TelegramBot.InlineKeyboardMarkup { const onOff = (val: boolean | undefined) => val !== false ? '✅' : '❌'; const selected = (current: number, option: number) => current === option ? '●' : '○'; + const gid = settings.groupId; // Include group ID in all callbacks const keyboard: TelegramBot.InlineKeyboardButton[][] = [ [{ text: `${onOff(settings.enabled)} Bot Enabled`, - callback_data: 'group_toggle_enabled', + callback_data: `group_toggle_enabled_${gid}`, }], [{ text: `${onOff(settings.newJackpotAnnouncement)} New Jackpot Announcement`, - callback_data: 'group_toggle_newjackpot', + callback_data: `group_toggle_newjackpot_${gid}`, }], ]; @@ -415,14 +459,14 @@ function getGroupSettingsKeyboard(settings: GroupSettings): TelegramBot.InlineKe keyboard.push( NEW_JACKPOT_DELAY_OPTIONS.map(minutes => ({ text: `${selected(settings.newJackpotDelayMinutes ?? 5, minutes)} ${formatNewJackpotDelay(minutes)}`, - callback_data: `group_newjackpot_delay_${minutes}`, + callback_data: `group_newjackpot_delay_${minutes}_${gid}`, })) ); } keyboard.push([{ text: `${onOff(settings.drawAnnouncements)} Draw Result Announcements`, - callback_data: 'group_toggle_announcements', + callback_data: `group_toggle_announcements_${gid}`, }]); // Add announcement delay options if announcements are enabled @@ -430,14 +474,14 @@ function getGroupSettingsKeyboard(settings: GroupSettings): TelegramBot.InlineKe keyboard.push( ANNOUNCEMENT_DELAY_OPTIONS.map(seconds => ({ text: `${selected(settings.announcementDelaySeconds || 0, seconds)} ${formatDelayOption(seconds)}`, - callback_data: `group_announce_delay_${seconds}`, + callback_data: `group_announce_delay_${seconds}_${gid}`, })) ); } keyboard.push([{ text: `${onOff(settings.reminders)} Draw Reminders`, - callback_data: 'group_toggle_reminders', + callback_data: `group_toggle_reminders_${gid}`, }]); // Add 3-tier reminder options if reminders are enabled @@ -455,39 +499,39 @@ function getGroupSettingsKeyboard(settings: GroupSettings): TelegramBot.InlineKe // Reminder 1 keyboard.push([{ text: `${onOff(r1Enabled)} Reminder 1: ${formatReminderTime(r1Time)} before`, - callback_data: 'group_toggle_reminder1', + callback_data: `group_toggle_reminder1_${gid}`, }]); if (r1Enabled) { - keyboard.push(getReminderTimeAdjustButtons(1, r1Time)); + keyboard.push(getReminderTimeAdjustButtons(1, r1Time, gid)); } // Reminder 2 keyboard.push([{ text: `${onOff(r2Enabled)} Reminder 2: ${formatReminderTime(r2Time)} before`, - callback_data: 'group_toggle_reminder2', + callback_data: `group_toggle_reminder2_${gid}`, }]); if (r2Enabled) { - keyboard.push(getReminderTimeAdjustButtons(2, r2Time)); + keyboard.push(getReminderTimeAdjustButtons(2, r2Time, gid)); } // Reminder 3 keyboard.push([{ text: `${onOff(r3Enabled)} Reminder 3: ${formatReminderTime(r3Time)} before`, - callback_data: 'group_toggle_reminder3', + callback_data: `group_toggle_reminder3_${gid}`, }]); if (r3Enabled) { - keyboard.push(getReminderTimeAdjustButtons(3, r3Time)); + keyboard.push(getReminderTimeAdjustButtons(3, r3Time, gid)); } } keyboard.push( [{ text: `${onOff(settings.ticketPurchaseAllowed)} Allow Ticket Purchases`, - callback_data: 'group_toggle_purchases', + callback_data: `group_toggle_purchases_${gid}`, }], [{ text: '🔄 Refresh', - callback_data: 'group_refresh', + callback_data: `group_refresh_${gid}`, }] ); @@ -496,28 +540,30 @@ function getGroupSettingsKeyboard(settings: GroupSettings): TelegramBot.InlineKe /** * Handle refresh callback + * groupId is extracted from callback data since settings are managed in DMs */ export async function handleGroupRefresh( bot: TelegramBot, - query: TelegramBot.CallbackQuery + query: TelegramBot.CallbackQuery, + groupId: number ): Promise { - const chatId = query.message?.chat.id; + const dmChatId = query.message?.chat.id; const messageId = query.message?.message_id; - if (!chatId || !messageId) return; + if (!dmChatId || !messageId) return; // Refresh auto-delete timer - scheduleSettingsMessageDeletion(bot, chatId, messageId); + scheduleSettingsMessageDeletion(bot, dmChatId, messageId); await bot.answerCallbackQuery(query.id, { text: 'Refreshed!' }); - const settings = await groupStateManager.getGroup(chatId); + const settings = await groupStateManager.getGroup(groupId); if (!settings) return; await bot.editMessageText( messages.groups.settingsOverview(settings), { - chat_id: chatId, + chat_id: dmChatId, message_id: messageId, parse_mode: 'Markdown', reply_markup: getGroupSettingsKeyboard(settings), diff --git a/telegram_bot/src/handlers/settings.ts b/telegram_bot/src/handlers/settings.ts index 1aff2cd..5e94119 100644 --- a/telegram_bot/src/handlers/settings.ts +++ b/telegram_bot/src/handlers/settings.ts @@ -35,7 +35,7 @@ export async function handleSettingsCommand( // Ensure notifications object exists const notifications = user.notifications || { ...DEFAULT_NOTIFICATIONS }; - const displayName = user.displayName || 'Anon'; + const displayName = stateManager.getDisplayName(user); await bot.sendMessage( chatId, @@ -83,13 +83,14 @@ export async function handleSettingsCallback( }); // Update message + const displayName = stateManager.getDisplayName(updatedUser); await bot.editMessageText( - messages.settings.overview(updatedUser.displayName || 'Anon', updatedUser.notifications), + messages.settings.overview(displayName, updatedUser.notifications), { chat_id: chatId, message_id: messageId, parse_mode: 'Markdown', - reply_markup: getSettingsKeyboard(updatedUser.displayName || 'Anon', updatedUser.notifications), + reply_markup: getSettingsKeyboard(displayName, updatedUser.notifications), } ); } @@ -145,8 +146,8 @@ export async function handleDisplayNameInput( return; } - // Clean the display name - const cleanName = text.replace(/[^\w\s\-_.]/g, '').trim() || 'Anon'; + // Clean the display name (allow @ for usernames) + const cleanName = text.replace(/[^\w\s\-_.@]/g, '').trim() || 'Anon'; await stateManager.updateDisplayName(userId, cleanName); logUserAction(userId, 'Set display name', { displayName: cleanName }); diff --git a/telegram_bot/src/index.ts b/telegram_bot/src/index.ts index f517eb5..eb1049e 100644 --- a/telegram_bot/src/index.ts +++ b/telegram_bot/src/index.ts @@ -340,57 +340,41 @@ bot.on('callback_query', async (query) => { logUserAction(query.from.id, 'Callback', { data }); try { - // Handle group settings toggles + // Handle group settings toggles (now includes group ID: group_toggle_enabled_-123456789) if (data.startsWith('group_toggle_')) { const action = data.replace('group_', ''); await handleGroupSettingsCallback(bot, query, action); return; } - // Handle group reminder time adjustment (reminder1_add_1_hours, etc.) - if (data.match(/^group_reminder\d_(add|sub)_\d+_(minutes|hours|days)$/)) { + // Handle group reminder time adjustment (reminder1_add_1_hours_-123456789, etc.) + if (data.match(/^group_reminder\d_(add|sub)_\d+_(minutes|hours|days)_-?\d+$/)) { const action = data.replace('group_', ''); await handleGroupSettingsCallback(bot, query, action); return; } - // Handle group add reminder (legacy) - if (data.startsWith('group_add_reminder_')) { + // Handle group announcement delay selection (group_announce_delay_30_-123456789) + if (data.match(/^group_announce_delay_\d+_-?\d+$/)) { const action = data.replace('group_', ''); await handleGroupSettingsCallback(bot, query, action); return; } - // Handle group remove reminder (legacy) - if (data.startsWith('group_remove_reminder_')) { + // Handle new jackpot announcement delay selection (group_newjackpot_delay_5_-123456789) + if (data.match(/^group_newjackpot_delay_\d+_-?\d+$/)) { const action = data.replace('group_', ''); await handleGroupSettingsCallback(bot, query, action); return; } - // Handle group clear reminders (legacy) - if (data === 'group_clear_reminders') { - await handleGroupSettingsCallback(bot, query, 'clear_reminders'); - return; - } - - // Handle group announcement delay selection - if (data.startsWith('group_announce_delay_')) { - const action = data.replace('group_', ''); - await handleGroupSettingsCallback(bot, query, action); - 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); + // Handle group refresh (group_refresh_-123456789) + if (data.match(/^group_refresh_-?\d+$/)) { + const groupIdMatch = data.match(/^group_refresh_(-?\d+)$/); + if (groupIdMatch) { + const groupId = parseInt(groupIdMatch[1], 10); + await handleGroupRefresh(bot, query, groupId); + } return; } diff --git a/telegram_bot/src/messages/index.ts b/telegram_bot/src/messages/index.ts index d383a2a..c1d820f 100644 --- a/telegram_bot/src/messages/index.ts +++ b/telegram_bot/src/messages/index.ts @@ -549,7 +549,7 @@ Be first to enter! Use /buyticket to buy tickets! 🍀`; `⚙️ *Your Settings* 👤 *Display Name:* ${displayName} -_(Used when announcing winners)_ +_(Shown when announcing winners. Defaults to your @username)_ *Notifications:* ${notifications.drawReminders ? '✅' : '❌'} Draw Reminders _(15 min before draws)_ @@ -563,6 +563,7 @@ Tap buttons below to change settings:`, Enter your display name (max 20 characters). This name will be shown if you win! +_Your Telegram @username is used by default._ _Send "Anon" to stay anonymous._`, nameTooLong: '❌ Display name must be 20 characters or less. Please try again:', diff --git a/telegram_bot/src/services/database.ts b/telegram_bot/src/services/database.ts index f75ff62..8a23189 100644 --- a/telegram_bot/src/services/database.ts +++ b/telegram_bot/src/services/database.ts @@ -165,6 +165,7 @@ class BotDatabase { /** * Create a new user + * Default display name is @username if available, otherwise 'Anon' */ createUser( telegramId: number, @@ -175,13 +176,15 @@ class BotDatabase { if (!this.db) throw new Error('Database not initialized'); const now = new Date().toISOString(); + // Default display name: @username if available, otherwise 'Anon' + const defaultDisplayName = username ? `@${username}` : 'Anon'; this.db.prepare(` INSERT INTO users (telegram_id, username, first_name, last_name, display_name, created_at, updated_at) - VALUES (?, ?, ?, ?, 'Anon', ?, ?) - `).run(telegramId, username || null, firstName || null, lastName || null, now, now); + VALUES (?, ?, ?, ?, ?, ?, ?) + `).run(telegramId, username || null, firstName || null, lastName || null, defaultDisplayName, now, now); - logger.info('New user created', { telegramId, username }); + logger.info('New user created', { telegramId, username, displayName: defaultDisplayName }); return this.getUser(telegramId)!; } diff --git a/telegram_bot/src/services/notificationScheduler.ts b/telegram_bot/src/services/notificationScheduler.ts index 5021334..f57efc4 100644 --- a/telegram_bot/src/services/notificationScheduler.ts +++ b/telegram_bot/src/services/notificationScheduler.ts @@ -173,7 +173,7 @@ class NotificationScheduler { if (status.result.is_winner) { const user = await stateManager.getUser(telegramId); winnerTelegramId = telegramId; - winnerDisplayName = user?.displayName || 'Anon'; + winnerDisplayName = user ? stateManager.getDisplayName(user) : 'Anon'; const winningTicket = status.tickets.find(t => t.is_winning_ticket); if (winningTicket) { @@ -396,7 +396,7 @@ class NotificationScheduler { if (status.result.is_winner) { const user = await stateManager.getUser(telegramId); winnerTelegramId = telegramId; - winnerDisplayName = user?.displayName || 'Anon'; + winnerDisplayName = user ? stateManager.getDisplayName(user) : 'Anon'; const winningTicket = status.tickets.find(t => t.is_winning_ticket); if (winningTicket) { diff --git a/telegram_bot/src/services/state.ts b/telegram_bot/src/services/state.ts index 2125e07..04b1d73 100644 --- a/telegram_bot/src/services/state.ts +++ b/telegram_bot/src/services/state.ts @@ -170,9 +170,17 @@ class StateManager { /** * Get user's display name (for announcements) + * Priority: displayName > @username > 'Anon' */ getDisplayName(user: TelegramUser): string { - return user.displayName || 'Anon'; + if (user.displayName && user.displayName !== 'Anon') { + return user.displayName; + } + // Fall back to @username if available + if (user.username) { + return `@${user.username}`; + } + return 'Anon'; } /**