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 { PendingPurchaseData, AwaitingPaymentData } from '../types';
import { messages } from '../messages'; 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 * Handle /buy command or "Buy Tickets" button
*/ */
@@ -293,11 +300,11 @@ export async function handlePurchaseConfirmation(
logUserAction(userId, 'Confirmed purchase', { tickets: pendingData.ticketCount }); 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( const purchaseResult = await apiClient.buyTickets(
pendingData.ticketCount, pendingData.ticketCount,
user.lightningAddress, user.lightningAddress,
user.displayName || 'Anon' stateManager.getDisplayName(user)
); );
logPaymentEvent(userId, purchaseResult.ticket_purchase_id, 'created', { logPaymentEvent(userId, purchaseResult.ticket_purchase_id, 'created', {
@@ -352,7 +359,8 @@ export async function handlePurchaseConfirmation(
} catch (error) { } catch (error) {
logger.error('Error in handlePurchaseConfirmation', { error, userId }); logger.error('Error in handlePurchaseConfirmation', { error, userId });
await bot.sendMessage(chatId, messages.errors.invoiceCreationFailed, { 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); await stateManager.clearUserStateData(userId);
} }
@@ -413,7 +421,8 @@ async function pollPaymentStatus(
await bot.sendMessage(chatId, messages.buy.invoiceExpired, { await bot.sendMessage(chatId, messages.buy.invoiceExpired, {
parse_mode: 'Markdown', 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); await stateManager.clearUserStateData(userId);
return; return;
@@ -467,7 +476,8 @@ async function pollPaymentStatus(
await bot.sendMessage(chatId, messages.buy.invoiceExpiredShort, { await bot.sendMessage(chatId, messages.buy.invoiceExpiredShort, {
parse_mode: 'Markdown', 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); await stateManager.clearUserStateData(userId);
return; return;

View File

@@ -77,6 +77,7 @@ export async function handleBotRemovedFromGroup(
/** /**
* Handle /settings command (group admin only) * Handle /settings command (group admin only)
* Opens settings in private DM, not in the group
*/ */
export async function handleGroupSettings( export async function handleGroupSettings(
bot: TelegramBot, bot: TelegramBot,
@@ -120,17 +121,33 @@ export async function handleGroupSettings(
return; return;
} }
const sentMessage = await bot.sendMessage( // Send settings to user's private DM
chatId, try {
messages.groups.settingsOverview(currentSettings), const sentMessage = await bot.sendMessage(
{ userId,
parse_mode: 'Markdown', messages.groups.settingsOverview(currentSettings),
reply_markup: getGroupSettingsKeyboard(currentSettings), {
} parse_mode: 'Markdown',
); reply_markup: getGroupSettingsKeyboard(currentSettings),
}
);
// Schedule auto-delete after 2 minutes // Schedule auto-delete after 2 minutes
scheduleSettingsMessageDeletion(bot, chatId, sentMessage.message_id); 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) { } catch (error) {
logger.error('Error in handleGroupSettings', { error, chatId }); logger.error('Error in handleGroupSettings', { error, chatId });
await bot.sendMessage(chatId, messages.errors.generic); await bot.sendMessage(chatId, messages.errors.generic);
@@ -167,22 +184,45 @@ function scheduleSettingsMessageDeletion(
settingsMessageTimeouts.set(key, timeout); 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 * Handle group settings toggle callback
* Settings can now be managed from DMs, so group ID is included in callback data
*/ */
export async function handleGroupSettingsCallback( export async function handleGroupSettingsCallback(
bot: TelegramBot, bot: TelegramBot,
query: TelegramBot.CallbackQuery, query: TelegramBot.CallbackQuery,
action: string action: string
): Promise<void> { ): Promise<void> {
const chatId = query.message?.chat.id; const dmChatId = query.message?.chat.id;
const userId = query.from.id; const userId = query.from.id;
const messageId = query.message?.message_id; const messageId = query.message?.message_id;
if (!chatId || !messageId) return; if (!dmChatId || !messageId) return;
// Check if user is admin // Parse the action to extract the group ID
const isAdmin = await isGroupAdmin(bot, chatId, userId); 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) { if (!isAdmin) {
await bot.answerCallbackQuery(query.id, { await bot.answerCallbackQuery(query.id, {
text: messages.groups.adminOnly, text: messages.groups.adminOnly,
@@ -191,11 +231,11 @@ export async function handleGroupSettingsCallback(
return; return;
} }
// Refresh auto-delete timer on any interaction // Refresh auto-delete timer on any interaction (in the DM)
scheduleSettingsMessageDeletion(bot, chatId, messageId); scheduleSettingsMessageDeletion(bot, dmChatId, messageId);
try { try {
const currentSettings = await groupStateManager.getGroup(chatId); const currentSettings = await groupStateManager.getGroup(groupId);
if (!currentSettings) { if (!currentSettings) {
await bot.answerCallbackQuery(query.id, { text: 'Group not found' }); await bot.answerCallbackQuery(query.id, { text: 'Group not found' });
return; return;
@@ -204,10 +244,10 @@ export async function handleGroupSettingsCallback(
let updatedSettings: GroupSettings | null = null; let updatedSettings: GroupSettings | null = null;
// Handle toggle actions // Handle toggle actions
if (action.startsWith('toggle_')) { if (settingAction.startsWith('toggle_')) {
let setting: 'enabled' | 'drawAnnouncements' | 'reminders' | 'ticketPurchaseAllowed' | 'newJackpotAnnouncement' | 'reminder1Enabled' | 'reminder2Enabled' | 'reminder3Enabled'; let setting: 'enabled' | 'drawAnnouncements' | 'reminders' | 'ticketPurchaseAllowed' | 'newJackpotAnnouncement' | 'reminder1Enabled' | 'reminder2Enabled' | 'reminder3Enabled';
switch (action) { switch (settingAction) {
case 'toggle_enabled': case 'toggle_enabled':
setting = 'enabled'; setting = 'enabled';
break; break;
@@ -239,11 +279,11 @@ export async function handleGroupSettingsCallback(
const currentValue = currentSettings[setting] !== false; // Default true for new settings const currentValue = currentSettings[setting] !== false; // Default true for new settings
const newValue = !currentValue; const newValue = !currentValue;
updatedSettings = await groupStateManager.updateSetting(chatId, setting, newValue); updatedSettings = await groupStateManager.updateSetting(groupId, setting, newValue);
if (updatedSettings) { if (updatedSettings) {
logUserAction(userId, 'Updated group setting', { logUserAction(userId, 'Updated group setting', {
groupId: chatId, groupId,
setting, setting,
newValue, 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 // Legacy handlers removed - now using 3-slot reminder system with toggle_reminder1/2/3 and time adjustments
// Handle announcement delay selection // Handle announcement delay selection (announce_delay_30 -> seconds=30)
if (action.startsWith('announce_delay_')) { const announceDelayMatch = settingAction.match(/^announce_delay_(\d+)$/);
const seconds = parseInt(action.replace('announce_delay_', ''), 10); if (announceDelayMatch) {
const seconds = parseInt(announceDelayMatch[1], 10);
if (!isNaN(seconds)) { if (!isNaN(seconds)) {
updatedSettings = await groupStateManager.updateAnnouncementDelay(chatId, seconds); updatedSettings = await groupStateManager.updateAnnouncementDelay(groupId, seconds);
if (updatedSettings) { if (updatedSettings) {
logUserAction(userId, 'Updated announcement delay', { groupId: chatId, seconds }); logUserAction(userId, 'Updated announcement delay', { groupId, seconds });
await bot.answerCallbackQuery(query.id, { await bot.answerCallbackQuery(query.id, {
text: seconds === 0 ? 'Announce immediately' : `Announce ${seconds}s after draw` text: seconds === 0 ? 'Announce immediately' : `Announce ${seconds}s after draw`
}); });
@@ -269,13 +310,14 @@ export async function handleGroupSettingsCallback(
} }
} }
// Handle new jackpot delay selection // Handle new jackpot delay selection (newjackpot_delay_5 -> minutes=5)
if (action.startsWith('newjackpot_delay_')) { const newJackpotDelayMatch = settingAction.match(/^newjackpot_delay_(\d+)$/);
const minutes = parseInt(action.replace('newjackpot_delay_', ''), 10); if (newJackpotDelayMatch) {
const minutes = parseInt(newJackpotDelayMatch[1], 10);
if (!isNaN(minutes)) { if (!isNaN(minutes)) {
updatedSettings = await groupStateManager.updateNewJackpotDelay(chatId, minutes); updatedSettings = await groupStateManager.updateNewJackpotDelay(groupId, minutes);
if (updatedSettings) { if (updatedSettings) {
logUserAction(userId, 'Updated new jackpot delay', { groupId: chatId, minutes }); logUserAction(userId, 'Updated new jackpot delay', { groupId, minutes });
await bot.answerCallbackQuery(query.id, { await bot.answerCallbackQuery(query.id, {
text: minutes === 0 ? 'Announce immediately' : `Announce ${minutes} min after new jackpot` 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.) // 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) { if (reminderTimeMatch) {
const slot = parseInt(reminderTimeMatch[1], 10) as 1 | 2 | 3; const slot = parseInt(reminderTimeMatch[1], 10) as 1 | 2 | 3;
const operation = reminderTimeMatch[2] as 'add' | 'sub'; const operation = reminderTimeMatch[2] as 'add' | 'sub';
@@ -317,9 +359,9 @@ export async function handleGroupSettingsCallback(
newTime = { value: newMinutes, unit: 'minutes' }; newTime = { value: newMinutes, unit: 'minutes' };
} }
updatedSettings = await groupStateManager.updateReminderTime(chatId, slot, newTime); updatedSettings = await groupStateManager.updateReminderTime(groupId, slot, newTime);
if (updatedSettings) { if (updatedSettings) {
logUserAction(userId, 'Updated reminder time', { groupId: chatId, slot, newTime }); logUserAction(userId, 'Updated reminder time', { groupId, slot, newTime });
await bot.answerCallbackQuery(query.id, { await bot.answerCallbackQuery(query.id, {
text: `Reminder ${slot}: ${formatReminderTime(newTime)} before draw` text: `Reminder ${slot}: ${formatReminderTime(newTime)} before draw`
}); });
@@ -331,18 +373,18 @@ export async function handleGroupSettingsCallback(
return; return;
} }
// Update the message with new settings // Update the message with new settings (in the DM)
await bot.editMessageText( await bot.editMessageText(
messages.groups.settingsOverview(updatedSettings), messages.groups.settingsOverview(updatedSettings),
{ {
chat_id: chatId, chat_id: dmChatId,
message_id: messageId, message_id: messageId,
parse_mode: 'Markdown', parse_mode: 'Markdown',
reply_markup: getGroupSettingsKeyboard(updatedSettings), reply_markup: getGroupSettingsKeyboard(updatedSettings),
} }
); );
} catch (error) { } 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' }); 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 * 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 [ return [
{ text: '1m', callback_data: `group_reminder${slot}_sub_1_minutes` }, { text: '1m', callback_data: `group_reminder${slot}_sub_1_minutes_${groupId}` },
{ text: '+1m', callback_data: `group_reminder${slot}_add_1_minutes` }, { text: '+1m', callback_data: `group_reminder${slot}_add_1_minutes_${groupId}` },
{ text: '1h', callback_data: `group_reminder${slot}_sub_1_hours` }, { text: '1h', callback_data: `group_reminder${slot}_sub_1_hours_${groupId}` },
{ text: '+1h', callback_data: `group_reminder${slot}_add_1_hours` }, { text: '+1h', callback_data: `group_reminder${slot}_add_1_hours_${groupId}` },
{ text: '1d', callback_data: `group_reminder${slot}_sub_1_days` }, { text: '1d', callback_data: `group_reminder${slot}_sub_1_days_${groupId}` },
{ text: '+1d', callback_data: `group_reminder${slot}_add_1_days` }, { 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 * 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 { function getGroupSettingsKeyboard(settings: GroupSettings): TelegramBot.InlineKeyboardMarkup {
const onOff = (val: boolean | undefined) => val !== false ? '✅' : '❌'; const onOff = (val: boolean | undefined) => val !== false ? '✅' : '❌';
const selected = (current: number, option: number) => current === option ? '●' : '○'; const selected = (current: number, option: number) => current === option ? '●' : '○';
const gid = settings.groupId; // Include group ID in all callbacks
const keyboard: TelegramBot.InlineKeyboardButton[][] = [ const keyboard: TelegramBot.InlineKeyboardButton[][] = [
[{ [{
text: `${onOff(settings.enabled)} Bot Enabled`, text: `${onOff(settings.enabled)} Bot Enabled`,
callback_data: 'group_toggle_enabled', callback_data: `group_toggle_enabled_${gid}`,
}], }],
[{ [{
text: `${onOff(settings.newJackpotAnnouncement)} New Jackpot Announcement`, 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( keyboard.push(
NEW_JACKPOT_DELAY_OPTIONS.map(minutes => ({ NEW_JACKPOT_DELAY_OPTIONS.map(minutes => ({
text: `${selected(settings.newJackpotDelayMinutes ?? 5, minutes)} ${formatNewJackpotDelay(minutes)}`, text: `${selected(settings.newJackpotDelayMinutes ?? 5, minutes)} ${formatNewJackpotDelay(minutes)}`,
callback_data: `group_newjackpot_delay_${minutes}`, callback_data: `group_newjackpot_delay_${minutes}_${gid}`,
})) }))
); );
} }
keyboard.push([{ keyboard.push([{
text: `${onOff(settings.drawAnnouncements)} Draw Result Announcements`, 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 // Add announcement delay options if announcements are enabled
@@ -430,14 +474,14 @@ function getGroupSettingsKeyboard(settings: GroupSettings): TelegramBot.InlineKe
keyboard.push( keyboard.push(
ANNOUNCEMENT_DELAY_OPTIONS.map(seconds => ({ ANNOUNCEMENT_DELAY_OPTIONS.map(seconds => ({
text: `${selected(settings.announcementDelaySeconds || 0, seconds)} ${formatDelayOption(seconds)}`, text: `${selected(settings.announcementDelaySeconds || 0, seconds)} ${formatDelayOption(seconds)}`,
callback_data: `group_announce_delay_${seconds}`, callback_data: `group_announce_delay_${seconds}_${gid}`,
})) }))
); );
} }
keyboard.push([{ keyboard.push([{
text: `${onOff(settings.reminders)} Draw Reminders`, 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 // Add 3-tier reminder options if reminders are enabled
@@ -455,39 +499,39 @@ function getGroupSettingsKeyboard(settings: GroupSettings): TelegramBot.InlineKe
// Reminder 1 // Reminder 1
keyboard.push([{ keyboard.push([{
text: `${onOff(r1Enabled)} Reminder 1: ${formatReminderTime(r1Time)} before`, text: `${onOff(r1Enabled)} Reminder 1: ${formatReminderTime(r1Time)} before`,
callback_data: 'group_toggle_reminder1', callback_data: `group_toggle_reminder1_${gid}`,
}]); }]);
if (r1Enabled) { if (r1Enabled) {
keyboard.push(getReminderTimeAdjustButtons(1, r1Time)); keyboard.push(getReminderTimeAdjustButtons(1, r1Time, gid));
} }
// Reminder 2 // Reminder 2
keyboard.push([{ keyboard.push([{
text: `${onOff(r2Enabled)} Reminder 2: ${formatReminderTime(r2Time)} before`, text: `${onOff(r2Enabled)} Reminder 2: ${formatReminderTime(r2Time)} before`,
callback_data: 'group_toggle_reminder2', callback_data: `group_toggle_reminder2_${gid}`,
}]); }]);
if (r2Enabled) { if (r2Enabled) {
keyboard.push(getReminderTimeAdjustButtons(2, r2Time)); keyboard.push(getReminderTimeAdjustButtons(2, r2Time, gid));
} }
// Reminder 3 // Reminder 3
keyboard.push([{ keyboard.push([{
text: `${onOff(r3Enabled)} Reminder 3: ${formatReminderTime(r3Time)} before`, text: `${onOff(r3Enabled)} Reminder 3: ${formatReminderTime(r3Time)} before`,
callback_data: 'group_toggle_reminder3', callback_data: `group_toggle_reminder3_${gid}`,
}]); }]);
if (r3Enabled) { if (r3Enabled) {
keyboard.push(getReminderTimeAdjustButtons(3, r3Time)); keyboard.push(getReminderTimeAdjustButtons(3, r3Time, gid));
} }
} }
keyboard.push( keyboard.push(
[{ [{
text: `${onOff(settings.ticketPurchaseAllowed)} Allow Ticket Purchases`, text: `${onOff(settings.ticketPurchaseAllowed)} Allow Ticket Purchases`,
callback_data: 'group_toggle_purchases', callback_data: `group_toggle_purchases_${gid}`,
}], }],
[{ [{
text: '🔄 Refresh', text: '🔄 Refresh',
callback_data: 'group_refresh', callback_data: `group_refresh_${gid}`,
}] }]
); );
@@ -496,28 +540,30 @@ function getGroupSettingsKeyboard(settings: GroupSettings): TelegramBot.InlineKe
/** /**
* Handle refresh callback * Handle refresh callback
* groupId is extracted from callback data since settings are managed in DMs
*/ */
export async function handleGroupRefresh( export async function handleGroupRefresh(
bot: TelegramBot, bot: TelegramBot,
query: TelegramBot.CallbackQuery query: TelegramBot.CallbackQuery,
groupId: number
): Promise<void> { ): Promise<void> {
const chatId = query.message?.chat.id; const dmChatId = query.message?.chat.id;
const messageId = query.message?.message_id; const messageId = query.message?.message_id;
if (!chatId || !messageId) return; if (!dmChatId || !messageId) return;
// Refresh auto-delete timer // Refresh auto-delete timer
scheduleSettingsMessageDeletion(bot, chatId, messageId); scheduleSettingsMessageDeletion(bot, dmChatId, messageId);
await bot.answerCallbackQuery(query.id, { text: 'Refreshed!' }); await bot.answerCallbackQuery(query.id, { text: 'Refreshed!' });
const settings = await groupStateManager.getGroup(chatId); const settings = await groupStateManager.getGroup(groupId);
if (!settings) return; if (!settings) return;
await bot.editMessageText( await bot.editMessageText(
messages.groups.settingsOverview(settings), messages.groups.settingsOverview(settings),
{ {
chat_id: chatId, chat_id: dmChatId,
message_id: messageId, message_id: messageId,
parse_mode: 'Markdown', parse_mode: 'Markdown',
reply_markup: getGroupSettingsKeyboard(settings), reply_markup: getGroupSettingsKeyboard(settings),

View File

@@ -35,7 +35,7 @@ export async function handleSettingsCommand(
// Ensure notifications object exists // Ensure notifications object exists
const notifications = user.notifications || { ...DEFAULT_NOTIFICATIONS }; const notifications = user.notifications || { ...DEFAULT_NOTIFICATIONS };
const displayName = user.displayName || 'Anon'; const displayName = stateManager.getDisplayName(user);
await bot.sendMessage( await bot.sendMessage(
chatId, chatId,
@@ -83,13 +83,14 @@ export async function handleSettingsCallback(
}); });
// Update message // Update message
const displayName = stateManager.getDisplayName(updatedUser);
await bot.editMessageText( await bot.editMessageText(
messages.settings.overview(updatedUser.displayName || 'Anon', updatedUser.notifications), messages.settings.overview(displayName, updatedUser.notifications),
{ {
chat_id: chatId, chat_id: chatId,
message_id: messageId, message_id: messageId,
parse_mode: 'Markdown', 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; return;
} }
// Clean the display name // Clean the display name (allow @ for usernames)
const cleanName = text.replace(/[^\w\s\-_.]/g, '').trim() || 'Anon'; const cleanName = text.replace(/[^\w\s\-_.@]/g, '').trim() || 'Anon';
await stateManager.updateDisplayName(userId, cleanName); await stateManager.updateDisplayName(userId, cleanName);
logUserAction(userId, 'Set display name', { displayName: 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 }); logUserAction(query.from.id, 'Callback', { data });
try { try {
// Handle group settings toggles // Handle group settings toggles (now includes group ID: group_toggle_enabled_-123456789)
if (data.startsWith('group_toggle_')) { if (data.startsWith('group_toggle_')) {
const action = data.replace('group_', ''); const action = data.replace('group_', '');
await handleGroupSettingsCallback(bot, query, action); await handleGroupSettingsCallback(bot, query, action);
return; return;
} }
// Handle group reminder time adjustment (reminder1_add_1_hours, etc.) // Handle group reminder time adjustment (reminder1_add_1_hours_-123456789, etc.)
if (data.match(/^group_reminder\d_(add|sub)_\d+_(minutes|hours|days)$/)) { if (data.match(/^group_reminder\d_(add|sub)_\d+_(minutes|hours|days)_-?\d+$/)) {
const action = data.replace('group_', ''); const action = data.replace('group_', '');
await handleGroupSettingsCallback(bot, query, action); await handleGroupSettingsCallback(bot, query, action);
return; return;
} }
// Handle group add reminder (legacy) // Handle group announcement delay selection (group_announce_delay_30_-123456789)
if (data.startsWith('group_add_reminder_')) { if (data.match(/^group_announce_delay_\d+_-?\d+$/)) {
const action = data.replace('group_', ''); const action = data.replace('group_', '');
await handleGroupSettingsCallback(bot, query, action); await handleGroupSettingsCallback(bot, query, action);
return; return;
} }
// Handle group remove reminder (legacy) // Handle new jackpot announcement delay selection (group_newjackpot_delay_5_-123456789)
if (data.startsWith('group_remove_reminder_')) { if (data.match(/^group_newjackpot_delay_\d+_-?\d+$/)) {
const action = data.replace('group_', ''); const action = data.replace('group_', '');
await handleGroupSettingsCallback(bot, query, action); await handleGroupSettingsCallback(bot, query, action);
return; return;
} }
// Handle group clear reminders (legacy) // Handle group refresh (group_refresh_-123456789)
if (data === 'group_clear_reminders') { if (data.match(/^group_refresh_-?\d+$/)) {
await handleGroupSettingsCallback(bot, query, 'clear_reminders'); const groupIdMatch = data.match(/^group_refresh_(-?\d+)$/);
return; if (groupIdMatch) {
} const groupId = parseInt(groupIdMatch[1], 10);
await handleGroupRefresh(bot, query, groupId);
// 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);
return; return;
} }

View File

@@ -549,7 +549,7 @@ Be first to enter! Use /buyticket to buy tickets! 🍀`;
`⚙️ *Your Settings* `⚙️ *Your Settings*
👤 *Display Name:* ${displayName} 👤 *Display Name:* ${displayName}
_(Used when announcing winners)_ _(Shown when announcing winners. Defaults to your @username)_
*Notifications:* *Notifications:*
${notifications.drawReminders ? '✅' : '❌'} Draw Reminders _(15 min before draws)_ ${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). Enter your display name (max 20 characters).
This name will be shown if you win! This name will be shown if you win!
_Your Telegram @username is used by default._
_Send "Anon" to stay anonymous._`, _Send "Anon" to stay anonymous._`,
nameTooLong: '❌ Display name must be 20 characters or less. Please try again:', nameTooLong: '❌ Display name must be 20 characters or less. Please try again:',

View File

@@ -165,6 +165,7 @@ class BotDatabase {
/** /**
* Create a new user * Create a new user
* Default display name is @username if available, otherwise 'Anon'
*/ */
createUser( createUser(
telegramId: number, telegramId: number,
@@ -175,13 +176,15 @@ class BotDatabase {
if (!this.db) throw new Error('Database not initialized'); if (!this.db) throw new Error('Database not initialized');
const now = new Date().toISOString(); const now = new Date().toISOString();
// Default display name: @username if available, otherwise 'Anon'
const defaultDisplayName = username ? `@${username}` : 'Anon';
this.db.prepare(` this.db.prepare(`
INSERT INTO users (telegram_id, username, first_name, last_name, display_name, created_at, updated_at) INSERT INTO users (telegram_id, username, first_name, last_name, display_name, created_at, updated_at)
VALUES (?, ?, ?, ?, 'Anon', ?, ?) VALUES (?, ?, ?, ?, ?, ?, ?)
`).run(telegramId, username || null, firstName || null, lastName || null, now, now); `).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)!; return this.getUser(telegramId)!;
} }

View File

@@ -173,7 +173,7 @@ class NotificationScheduler {
if (status.result.is_winner) { if (status.result.is_winner) {
const user = await stateManager.getUser(telegramId); const user = await stateManager.getUser(telegramId);
winnerTelegramId = telegramId; winnerTelegramId = telegramId;
winnerDisplayName = user?.displayName || 'Anon'; winnerDisplayName = user ? stateManager.getDisplayName(user) : 'Anon';
const winningTicket = status.tickets.find(t => t.is_winning_ticket); const winningTicket = status.tickets.find(t => t.is_winning_ticket);
if (winningTicket) { if (winningTicket) {
@@ -396,7 +396,7 @@ class NotificationScheduler {
if (status.result.is_winner) { if (status.result.is_winner) {
const user = await stateManager.getUser(telegramId); const user = await stateManager.getUser(telegramId);
winnerTelegramId = telegramId; winnerTelegramId = telegramId;
winnerDisplayName = user?.displayName || 'Anon'; winnerDisplayName = user ? stateManager.getDisplayName(user) : 'Anon';
const winningTicket = status.tickets.find(t => t.is_winning_ticket); const winningTicket = status.tickets.find(t => t.is_winning_ticket);
if (winningTicket) { if (winningTicket) {

View File

@@ -170,9 +170,17 @@ class StateManager {
/** /**
* Get user's display name (for announcements) * Get user's display name (for announcements)
* Priority: displayName > @username > 'Anon'
*/ */
getDisplayName(user: TelegramUser): string { 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';
} }
/** /**