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()
This commit is contained in:
Michilis
2025-12-12 15:28:05 +00:00
parent 959268e7c1
commit 00f09236a3
8 changed files with 164 additions and 111 deletions

View File

@@ -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;

View File

@@ -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<void> {
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<void> {
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),

View File

@@ -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 });

View File

@@ -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;
}

View File

@@ -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:',

View File

@@ -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)!;
}

View File

@@ -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) {

View File

@@ -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';
}
/**