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:
@@ -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;
|
||||||
|
|||||||
@@ -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,8 +121,10 @@ export async function handleGroupSettings(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Send settings to user's private DM
|
||||||
|
try {
|
||||||
const sentMessage = await bot.sendMessage(
|
const sentMessage = await bot.sendMessage(
|
||||||
chatId,
|
userId,
|
||||||
messages.groups.settingsOverview(currentSettings),
|
messages.groups.settingsOverview(currentSettings),
|
||||||
{
|
{
|
||||||
parse_mode: 'Markdown',
|
parse_mode: 'Markdown',
|
||||||
@@ -130,7 +133,21 @@ export async function handleGroupSettings(
|
|||||||
);
|
);
|
||||||
|
|
||||||
// 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),
|
||||||
|
|||||||
@@ -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 });
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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:',
|
||||||
|
|||||||
@@ -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)!;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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';
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
Reference in New Issue
Block a user