Add SQLite database for Telegram bot user/group settings
- Replace Redis/in-memory storage with SQLite for persistence - Add database.ts service with tables for users, groups, purchases, participants - Update state.ts and groupState.ts to use SQLite backend - Fix buyer_name to use display name instead of Telegram ID - Remove legacy reminder array handlers (now using 3-slot system) - Add better-sqlite3 dependency, remove ioredis - Update env.example with BOT_DATABASE_PATH option - Add data/ directory to .gitignore for database files
This commit is contained in:
@@ -2,6 +2,19 @@ import TelegramBot from 'node-telegram-bot-api';
|
||||
import { groupStateManager } from '../services/groupState';
|
||||
import { logger, logUserAction } from '../services/logger';
|
||||
import { messages } from '../messages';
|
||||
import {
|
||||
GroupSettings,
|
||||
REMINDER_PRESETS,
|
||||
ANNOUNCEMENT_DELAY_OPTIONS,
|
||||
DEFAULT_GROUP_REMINDER_SLOTS,
|
||||
ReminderTime,
|
||||
formatReminderTime,
|
||||
reminderTimeToMinutes
|
||||
} from '../types/groups';
|
||||
|
||||
// Track settings messages for auto-deletion
|
||||
const settingsMessageTimeouts: Map<string, NodeJS.Timeout> = new Map();
|
||||
const SETTINGS_MESSAGE_TTL = 2 * 60 * 1000; // 2 minutes
|
||||
|
||||
/**
|
||||
* Check if a user is an admin in a group
|
||||
@@ -106,7 +119,7 @@ export async function handleGroupSettings(
|
||||
return;
|
||||
}
|
||||
|
||||
await bot.sendMessage(
|
||||
const sentMessage = await bot.sendMessage(
|
||||
chatId,
|
||||
messages.groups.settingsOverview(currentSettings),
|
||||
{
|
||||
@@ -114,12 +127,45 @@ export async function handleGroupSettings(
|
||||
reply_markup: getGroupSettingsKeyboard(currentSettings),
|
||||
}
|
||||
);
|
||||
|
||||
// Schedule auto-delete after 2 minutes
|
||||
scheduleSettingsMessageDeletion(bot, chatId, sentMessage.message_id);
|
||||
} catch (error) {
|
||||
logger.error('Error in handleGroupSettings', { error, chatId });
|
||||
await bot.sendMessage(chatId, messages.errors.generic);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Schedule deletion of settings message after 2 minutes
|
||||
*/
|
||||
function scheduleSettingsMessageDeletion(
|
||||
bot: TelegramBot,
|
||||
chatId: number,
|
||||
messageId: number
|
||||
): void {
|
||||
const key = `${chatId}:${messageId}`;
|
||||
|
||||
// Clear any existing timeout for this message
|
||||
const existingTimeout = settingsMessageTimeouts.get(key);
|
||||
if (existingTimeout) {
|
||||
clearTimeout(existingTimeout);
|
||||
}
|
||||
|
||||
// Schedule new deletion
|
||||
const timeout = setTimeout(async () => {
|
||||
try {
|
||||
await bot.deleteMessage(chatId, messageId);
|
||||
logger.debug('Auto-deleted settings message', { chatId, messageId });
|
||||
} catch (error) {
|
||||
// Ignore errors (message might already be deleted)
|
||||
}
|
||||
settingsMessageTimeouts.delete(key);
|
||||
}, SETTINGS_MESSAGE_TTL);
|
||||
|
||||
settingsMessageTimeouts.set(key, timeout);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle group settings toggle callback
|
||||
*/
|
||||
@@ -144,51 +190,132 @@ export async function handleGroupSettingsCallback(
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
let setting: 'enabled' | 'drawAnnouncements' | 'reminders' | 'ticketPurchaseAllowed';
|
||||
|
||||
switch (action) {
|
||||
case 'toggle_enabled':
|
||||
setting = 'enabled';
|
||||
break;
|
||||
case 'toggle_announcements':
|
||||
setting = 'drawAnnouncements';
|
||||
break;
|
||||
case 'toggle_reminders':
|
||||
setting = 'reminders';
|
||||
break;
|
||||
case 'toggle_purchases':
|
||||
setting = 'ticketPurchaseAllowed';
|
||||
break;
|
||||
default:
|
||||
await bot.answerCallbackQuery(query.id);
|
||||
return;
|
||||
}
|
||||
// Refresh auto-delete timer on any interaction
|
||||
scheduleSettingsMessageDeletion(bot, chatId, messageId);
|
||||
|
||||
try {
|
||||
const currentSettings = await groupStateManager.getGroup(chatId);
|
||||
if (!currentSettings) {
|
||||
await bot.answerCallbackQuery(query.id, { text: 'Group not found' });
|
||||
return;
|
||||
}
|
||||
|
||||
const newValue = !currentSettings[setting];
|
||||
const updatedSettings = await groupStateManager.updateSetting(chatId, setting, newValue);
|
||||
let updatedSettings: GroupSettings | null = null;
|
||||
|
||||
// Handle toggle actions
|
||||
if (action.startsWith('toggle_')) {
|
||||
let setting: 'enabled' | 'drawAnnouncements' | 'reminders' | 'ticketPurchaseAllowed' | 'newJackpotAnnouncement' | 'reminder1Enabled' | 'reminder2Enabled' | 'reminder3Enabled';
|
||||
|
||||
switch (action) {
|
||||
case 'toggle_enabled':
|
||||
setting = 'enabled';
|
||||
break;
|
||||
case 'toggle_announcements':
|
||||
setting = 'drawAnnouncements';
|
||||
break;
|
||||
case 'toggle_reminders':
|
||||
setting = 'reminders';
|
||||
break;
|
||||
case 'toggle_purchases':
|
||||
setting = 'ticketPurchaseAllowed';
|
||||
break;
|
||||
case 'toggle_newjackpot':
|
||||
setting = 'newJackpotAnnouncement';
|
||||
break;
|
||||
case 'toggle_reminder1':
|
||||
setting = 'reminder1Enabled';
|
||||
break;
|
||||
case 'toggle_reminder2':
|
||||
setting = 'reminder2Enabled';
|
||||
break;
|
||||
case 'toggle_reminder3':
|
||||
setting = 'reminder3Enabled';
|
||||
break;
|
||||
default:
|
||||
await bot.answerCallbackQuery(query.id);
|
||||
return;
|
||||
}
|
||||
|
||||
const currentValue = currentSettings[setting] !== false; // Default true for new settings
|
||||
const newValue = !currentValue;
|
||||
updatedSettings = await groupStateManager.updateSetting(chatId, setting, newValue);
|
||||
|
||||
if (updatedSettings) {
|
||||
logUserAction(userId, 'Updated group setting', {
|
||||
groupId: chatId,
|
||||
setting,
|
||||
newValue,
|
||||
});
|
||||
await bot.answerCallbackQuery(query.id, {
|
||||
text: `${setting} ${newValue ? 'enabled' : 'disabled'}`,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Legacy handlers removed - now using 3-slot reminder system with toggle_reminder1/2/3 and time adjustments
|
||||
|
||||
// Handle announcement delay selection
|
||||
if (action.startsWith('announce_delay_')) {
|
||||
const seconds = parseInt(action.replace('announce_delay_', ''), 10);
|
||||
if (!isNaN(seconds)) {
|
||||
updatedSettings = await groupStateManager.updateAnnouncementDelay(chatId, seconds);
|
||||
if (updatedSettings) {
|
||||
logUserAction(userId, 'Updated announcement delay', { groupId: chatId, seconds });
|
||||
await bot.answerCallbackQuery(query.id, {
|
||||
text: seconds === 0 ? 'Announce immediately' : `Announce ${seconds}s after draw`
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Handle reminder time adjustments (reminder1_add_1_hours, reminder2_sub_1_days, etc.)
|
||||
const reminderTimeMatch = action.match(/^reminder(\d)_(add|sub)_(\d+)_(minutes|hours|days)$/);
|
||||
if (reminderTimeMatch) {
|
||||
const slot = parseInt(reminderTimeMatch[1], 10) as 1 | 2 | 3;
|
||||
const operation = reminderTimeMatch[2] as 'add' | 'sub';
|
||||
const amount = parseInt(reminderTimeMatch[3], 10);
|
||||
const unit = reminderTimeMatch[4] as 'minutes' | 'hours' | 'days';
|
||||
|
||||
// Get current time for this slot
|
||||
const currentTimeKey = `reminder${slot}Time` as 'reminder1Time' | 'reminder2Time' | 'reminder3Time';
|
||||
const defaultTimes: Record<string, ReminderTime> = {
|
||||
reminder1Time: { value: 1, unit: 'hours' },
|
||||
reminder2Time: { value: 1, unit: 'days' },
|
||||
reminder3Time: { value: 6, unit: 'days' },
|
||||
};
|
||||
const currentTime = currentSettings[currentTimeKey] || defaultTimes[currentTimeKey];
|
||||
|
||||
// Convert to minutes for calculation
|
||||
const currentMinutes = reminderTimeToMinutes(currentTime);
|
||||
const adjustMinutes = unit === 'minutes' ? amount : unit === 'hours' ? amount * 60 : amount * 60 * 24;
|
||||
const newMinutes = operation === 'add'
|
||||
? currentMinutes + adjustMinutes
|
||||
: Math.max(1, currentMinutes - adjustMinutes); // Minimum 1 minute
|
||||
|
||||
// Convert back to best unit
|
||||
let newTime: ReminderTime;
|
||||
if (newMinutes >= 1440 && newMinutes % 1440 === 0) {
|
||||
newTime = { value: newMinutes / 1440, unit: 'days' };
|
||||
} else if (newMinutes >= 60 && newMinutes % 60 === 0) {
|
||||
newTime = { value: newMinutes / 60, unit: 'hours' };
|
||||
} else {
|
||||
newTime = { value: newMinutes, unit: 'minutes' };
|
||||
}
|
||||
|
||||
updatedSettings = await groupStateManager.updateReminderTime(chatId, slot, newTime);
|
||||
if (updatedSettings) {
|
||||
logUserAction(userId, 'Updated reminder time', { groupId: chatId, slot, newTime });
|
||||
await bot.answerCallbackQuery(query.id, {
|
||||
text: `Reminder ${slot}: ${formatReminderTime(newTime)} before draw`
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (!updatedSettings) {
|
||||
await bot.answerCallbackQuery(query.id, { text: 'Failed to update' });
|
||||
return;
|
||||
}
|
||||
|
||||
logUserAction(userId, 'Updated group setting', {
|
||||
groupId: chatId,
|
||||
setting,
|
||||
newValue,
|
||||
});
|
||||
|
||||
await bot.answerCallbackQuery(query.id, {
|
||||
text: `${setting} ${newValue ? 'enabled' : 'disabled'}`,
|
||||
});
|
||||
|
||||
// Update the message with new settings
|
||||
await bot.editMessageText(
|
||||
messages.groups.settingsOverview(updatedSettings),
|
||||
@@ -205,41 +332,132 @@ export async function handleGroupSettingsCallback(
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Format delay option for display
|
||||
*/
|
||||
function formatDelayOption(seconds: number): string {
|
||||
if (seconds === 0) return 'Instant';
|
||||
if (seconds >= 60) {
|
||||
const minutes = seconds / 60;
|
||||
return minutes === 1 ? '1 min' : `${minutes} min`;
|
||||
}
|
||||
return `${seconds}s`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get time adjustment buttons for a reminder slot
|
||||
*/
|
||||
function getReminderTimeAdjustButtons(slot: number, currentTime: ReminderTime): TelegramBot.InlineKeyboardButton[] {
|
||||
return [
|
||||
{ text: '−1m', callback_data: `group_reminder${slot}_sub_1_minutes` },
|
||||
{ text: '+1m', callback_data: `group_reminder${slot}_add_1_minutes` },
|
||||
{ text: '−1h', callback_data: `group_reminder${slot}_sub_1_hours` },
|
||||
{ text: '+1h', callback_data: `group_reminder${slot}_add_1_hours` },
|
||||
{ text: '−1d', callback_data: `group_reminder${slot}_sub_1_days` },
|
||||
{ text: '+1d', callback_data: `group_reminder${slot}_add_1_days` },
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a reminder time is already set
|
||||
*/
|
||||
function hasReminder(settings: GroupSettings, rt: ReminderTime): boolean {
|
||||
if (!settings.reminderTimes) return false;
|
||||
return settings.reminderTimes.some(
|
||||
r => r.value === rt.value && r.unit === rt.unit
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate inline keyboard for group settings
|
||||
*/
|
||||
function getGroupSettingsKeyboard(settings: {
|
||||
enabled: boolean;
|
||||
drawAnnouncements: boolean;
|
||||
reminders: boolean;
|
||||
ticketPurchaseAllowed: boolean;
|
||||
}): TelegramBot.InlineKeyboardMarkup {
|
||||
const onOff = (val: boolean) => val ? '✅' : '❌';
|
||||
function getGroupSettingsKeyboard(settings: GroupSettings): TelegramBot.InlineKeyboardMarkup {
|
||||
const onOff = (val: boolean | undefined) => val !== false ? '✅' : '❌';
|
||||
const selected = (current: number, option: number) => current === option ? '●' : '○';
|
||||
|
||||
return {
|
||||
inline_keyboard: [
|
||||
[{
|
||||
text: `${onOff(settings.enabled)} Bot Enabled`,
|
||||
callback_data: 'group_toggle_enabled',
|
||||
}],
|
||||
[{
|
||||
text: `${onOff(settings.drawAnnouncements)} Draw Announcements`,
|
||||
callback_data: 'group_toggle_announcements',
|
||||
}],
|
||||
[{
|
||||
text: `${onOff(settings.reminders)} Draw Reminders`,
|
||||
callback_data: 'group_toggle_reminders',
|
||||
}],
|
||||
[{
|
||||
text: `${onOff(settings.ticketPurchaseAllowed)} Allow Ticket Purchases`,
|
||||
callback_data: 'group_toggle_purchases',
|
||||
}],
|
||||
[{
|
||||
text: '🔄 Refresh',
|
||||
callback_data: 'group_refresh',
|
||||
}],
|
||||
],
|
||||
};
|
||||
const keyboard: TelegramBot.InlineKeyboardButton[][] = [
|
||||
[{
|
||||
text: `${onOff(settings.enabled)} Bot Enabled`,
|
||||
callback_data: 'group_toggle_enabled',
|
||||
}],
|
||||
[{
|
||||
text: `${onOff(settings.newJackpotAnnouncement)} New Jackpot Announcement`,
|
||||
callback_data: 'group_toggle_newjackpot',
|
||||
}],
|
||||
[{
|
||||
text: `${onOff(settings.drawAnnouncements)} Draw Result Announcements`,
|
||||
callback_data: 'group_toggle_announcements',
|
||||
}],
|
||||
];
|
||||
|
||||
// Add announcement delay options if announcements are enabled
|
||||
if (settings.drawAnnouncements) {
|
||||
keyboard.push(
|
||||
ANNOUNCEMENT_DELAY_OPTIONS.map(seconds => ({
|
||||
text: `${selected(settings.announcementDelaySeconds || 0, seconds)} ${formatDelayOption(seconds)}`,
|
||||
callback_data: `group_announce_delay_${seconds}`,
|
||||
}))
|
||||
);
|
||||
}
|
||||
|
||||
keyboard.push([{
|
||||
text: `${onOff(settings.reminders)} Draw Reminders`,
|
||||
callback_data: 'group_toggle_reminders',
|
||||
}]);
|
||||
|
||||
// Add 3-tier reminder options if reminders are enabled
|
||||
if (settings.reminders) {
|
||||
// Get default values with fallback for migration
|
||||
const r1Enabled = settings.reminder1Enabled !== false;
|
||||
const r2Enabled = settings.reminder2Enabled === true;
|
||||
const r3Enabled = settings.reminder3Enabled === true;
|
||||
|
||||
// Get times with defaults
|
||||
const r1Time = settings.reminder1Time || { value: 1, unit: 'hours' as const };
|
||||
const r2Time = settings.reminder2Time || { value: 1, unit: 'days' as const };
|
||||
const r3Time = settings.reminder3Time || { value: 6, unit: 'days' as const };
|
||||
|
||||
// Reminder 1
|
||||
keyboard.push([{
|
||||
text: `${onOff(r1Enabled)} Reminder 1: ${formatReminderTime(r1Time)} before`,
|
||||
callback_data: 'group_toggle_reminder1',
|
||||
}]);
|
||||
if (r1Enabled) {
|
||||
keyboard.push(getReminderTimeAdjustButtons(1, r1Time));
|
||||
}
|
||||
|
||||
// Reminder 2
|
||||
keyboard.push([{
|
||||
text: `${onOff(r2Enabled)} Reminder 2: ${formatReminderTime(r2Time)} before`,
|
||||
callback_data: 'group_toggle_reminder2',
|
||||
}]);
|
||||
if (r2Enabled) {
|
||||
keyboard.push(getReminderTimeAdjustButtons(2, r2Time));
|
||||
}
|
||||
|
||||
// Reminder 3
|
||||
keyboard.push([{
|
||||
text: `${onOff(r3Enabled)} Reminder 3: ${formatReminderTime(r3Time)} before`,
|
||||
callback_data: 'group_toggle_reminder3',
|
||||
}]);
|
||||
if (r3Enabled) {
|
||||
keyboard.push(getReminderTimeAdjustButtons(3, r3Time));
|
||||
}
|
||||
}
|
||||
|
||||
keyboard.push(
|
||||
[{
|
||||
text: `${onOff(settings.ticketPurchaseAllowed)} Allow Ticket Purchases`,
|
||||
callback_data: 'group_toggle_purchases',
|
||||
}],
|
||||
[{
|
||||
text: '🔄 Refresh',
|
||||
callback_data: 'group_refresh',
|
||||
}]
|
||||
);
|
||||
|
||||
return { inline_keyboard: keyboard };
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -254,6 +472,9 @@ export async function handleGroupRefresh(
|
||||
|
||||
if (!chatId || !messageId) return;
|
||||
|
||||
// Refresh auto-delete timer
|
||||
scheduleSettingsMessageDeletion(bot, chatId, messageId);
|
||||
|
||||
await bot.answerCallbackQuery(query.id, { text: 'Refreshed!' });
|
||||
|
||||
const settings = await groupStateManager.getGroup(chatId);
|
||||
|
||||
Reference in New Issue
Block a user