feat(state): replace in-memory Map with SQLite-backed stateService (#59)
- Create src/services/stateService.js with get/set/delete/has API - Create migration 004_user_states.js (chat_id PK, state_data JSON, updated_at) - TTL of 24 hours — expired states auto-deleted - Cleanup job runs every hour (setInterval) - Replace src/context/userStates.js Map with async stateService proxy - Add await to all 45 userStates.get/set/delete/has calls across 13 files - Add initStates() call in index.js startup sequence - All state survives bot restarts now 18 files changed, 172 insertions, 46 deletions
This commit is contained in:
@@ -1,2 +1,11 @@
|
|||||||
const userStates = new Map();
|
import { get, set, del, has, initStates } from '../services/stateService.js';
|
||||||
|
|
||||||
|
const userStates = {
|
||||||
|
get: (chatId) => get(chatId),
|
||||||
|
set: (chatId, value) => set(chatId, value),
|
||||||
|
delete: (chatId) => del(chatId),
|
||||||
|
has: (chatId) => has(chatId),
|
||||||
|
initStates,
|
||||||
|
};
|
||||||
|
|
||||||
export default userStates;
|
export default userStates;
|
||||||
@@ -84,7 +84,7 @@ export default class AdminDumpHandler {
|
|||||||
|
|
||||||
const chatId = callbackQuery.message.chat.id;
|
const chatId = callbackQuery.message.chat.id;
|
||||||
|
|
||||||
userStates.set(chatId, { action: 'upload_database_dump' });
|
await userStates.set(chatId, { action: 'upload_database_dump' });
|
||||||
|
|
||||||
await bot.editMessageText(
|
await bot.editMessageText(
|
||||||
'Please upload database dump',
|
'Please upload database dump',
|
||||||
@@ -121,7 +121,7 @@ export default class AdminDumpHandler {
|
|||||||
static async handleDumpImport(msg) {
|
static async handleDumpImport(msg) {
|
||||||
const chatId = msg.chat.id;
|
const chatId = msg.chat.id;
|
||||||
|
|
||||||
const state = userStates.get(chatId);
|
const state = await userStates.get(chatId);
|
||||||
|
|
||||||
if (!state || state.action !== 'upload_database_dump') {
|
if (!state || state.action !== 'upload_database_dump') {
|
||||||
return false;
|
return false;
|
||||||
@@ -145,7 +145,7 @@ export default class AdminDumpHandler {
|
|||||||
|
|
||||||
const statistics = await this.getDumpStatistic();
|
const statistics = await this.getDumpStatistic();
|
||||||
await bot.sendMessage(chatId, JSON.stringify(statistics, null, 2));
|
await bot.sendMessage(chatId, JSON.stringify(statistics, null, 2));
|
||||||
userStates.delete(chatId);
|
await userStates.delete(chatId);
|
||||||
} else {
|
} else {
|
||||||
await bot.sendMessage(chatId, 'Please upload a valid .zip file.');
|
await bot.sendMessage(chatId, 'Please upload a valid .zip file.');
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ export default class AdminLocationHandler {
|
|||||||
|
|
||||||
const chatId = callbackQuery.message.chat.id;
|
const chatId = callbackQuery.message.chat.id;
|
||||||
|
|
||||||
userStates.set(chatId, { action: 'add_location' });
|
await userStates.set(chatId, { action: 'add_location' });
|
||||||
|
|
||||||
await bot.editMessageText(
|
await bot.editMessageText(
|
||||||
'Please enter the location in the following format:\nCountry|City|District',
|
'Please enter the location in the following format:\nCountry|City|District',
|
||||||
@@ -30,7 +30,7 @@ export default class AdminLocationHandler {
|
|||||||
|
|
||||||
static async handleLocationInput(msg) {
|
static async handleLocationInput(msg) {
|
||||||
const chatId = msg.chat.id;
|
const chatId = msg.chat.id;
|
||||||
const state = userStates.get(chatId);
|
const state = await userStates.get(chatId);
|
||||||
|
|
||||||
if (!state || state.action !== 'add_location') {
|
if (!state || state.action !== 'add_location') {
|
||||||
return false;
|
return false;
|
||||||
@@ -87,7 +87,7 @@ export default class AdminLocationHandler {
|
|||||||
throw new Error('Failed to insert location');
|
throw new Error('Failed to insert location');
|
||||||
}
|
}
|
||||||
|
|
||||||
userStates.delete(chatId);
|
await userStates.delete(chatId);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
await db.runAsync('ROLLBACK');
|
await db.runAsync('ROLLBACK');
|
||||||
|
|
||||||
@@ -166,7 +166,7 @@ export default class AdminLocationHandler {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
userStates.delete(chatId);
|
await userStates.delete(chatId);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const locations = await db.allAsync(`
|
const locations = await db.allAsync(`
|
||||||
@@ -362,6 +362,6 @@ export default class AdminLocationHandler {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
userStates.delete(chatId);
|
await userStates.delete(chatId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -449,7 +449,7 @@ export default class AdminUserHandler {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
userStates.set(chatId, { action: "edit_bonus_balance", telegram_id: telegramId });
|
await userStates.set(chatId, { action: "edit_bonus_balance", telegram_id: telegramId });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error({ err: error }, 'Error in handleEditUserBalance');
|
logger.error({ err: error }, 'Error in handleEditUserBalance');
|
||||||
await bot.sendMessage(chatId, 'Error loading user wallets. Please try again.');
|
await bot.sendMessage(chatId, 'Error loading user wallets. Please try again.');
|
||||||
@@ -462,7 +462,7 @@ export default class AdminUserHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const chatId = msg.chat.id;
|
const chatId = msg.chat.id;
|
||||||
const state = userStates.get(chatId);
|
const state = await userStates.get(chatId);
|
||||||
|
|
||||||
if (!state || state.action !== 'edit_bonus_balance') {
|
if (!state || state.action !== 'edit_bonus_balance') {
|
||||||
return false;
|
return false;
|
||||||
@@ -482,6 +482,6 @@ export default class AdminUserHandler {
|
|||||||
await bot.sendMessage(chatId, 'Something went wrong');
|
await bot.sendMessage(chatId, 'Something went wrong');
|
||||||
}
|
}
|
||||||
|
|
||||||
userStates.delete(chatId);
|
await userStates.delete(chatId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -10,7 +10,7 @@ export default class CategoryAddHandler {
|
|||||||
|
|
||||||
static async handleCategoryInput(msg) {
|
static async handleCategoryInput(msg) {
|
||||||
const chatId = msg.chat.id;
|
const chatId = msg.chat.id;
|
||||||
const state = userStates.get(chatId);
|
const state = await userStates.get(chatId);
|
||||||
|
|
||||||
if (!state || !state.action?.startsWith('add_category_')) {
|
if (!state || !state.action?.startsWith('add_category_')) {
|
||||||
return false;
|
return false;
|
||||||
@@ -51,7 +51,7 @@ export default class CategoryAddHandler {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
userStates.delete(chatId);
|
await userStates.delete(chatId);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error.code === 'SQLITE_CONSTRAINT') {
|
if (error.code === 'SQLITE_CONSTRAINT') {
|
||||||
await bot.sendMessage(chatId, 'This category already exists in this location.');
|
await bot.sendMessage(chatId, 'This category already exists in this location.');
|
||||||
@@ -72,7 +72,7 @@ export default class CategoryAddHandler {
|
|||||||
const chatId = callbackQuery.message.chat.id;
|
const chatId = callbackQuery.message.chat.id;
|
||||||
const locationId = callbackQuery.data.replace('add_category_', '');
|
const locationId = callbackQuery.data.replace('add_category_', '');
|
||||||
|
|
||||||
userStates.set(chatId, {action: `add_category_${locationId}`});
|
await userStates.set(chatId, {action: `add_category_${locationId}`});
|
||||||
|
|
||||||
const location = await LocationService.getLocationById(locationId);
|
const location = await LocationService.getLocationById(locationId);
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ export default class CategoryEditHandler {
|
|||||||
|
|
||||||
static async handleCategoryUpdate(msg) {
|
static async handleCategoryUpdate(msg) {
|
||||||
const chatId = msg.chat.id;
|
const chatId = msg.chat.id;
|
||||||
const state = userStates.get(chatId);
|
const state = await userStates.get(chatId);
|
||||||
|
|
||||||
if (!state || !state.action?.startsWith('edit_category_')) {
|
if (!state || !state.action?.startsWith('edit_category_')) {
|
||||||
return false;
|
return false;
|
||||||
@@ -48,7 +48,7 @@ export default class CategoryEditHandler {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
userStates.delete(chatId);
|
await userStates.delete(chatId);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error({ err: error }, 'Error updating category');
|
logger.error({ err: error }, 'Error updating category');
|
||||||
await bot.sendMessage(chatId, 'Ошибка обновления категории. Пожалуйста, попробуйте снова.');
|
await bot.sendMessage(chatId, 'Ошибка обновления категории. Пожалуйста, попробуйте снова.');
|
||||||
@@ -65,7 +65,7 @@ export default class CategoryEditHandler {
|
|||||||
const chatId = callbackQuery.message.chat.id;
|
const chatId = callbackQuery.message.chat.id;
|
||||||
const [locationId, categoryId] = callbackQuery.data.replace('edit_category_', '').split('_');
|
const [locationId, categoryId] = callbackQuery.data.replace('edit_category_', '').split('_');
|
||||||
|
|
||||||
userStates.set(chatId, { action: `edit_category_${locationId}_${categoryId}` });
|
await userStates.set(chatId, { action: `edit_category_${locationId}_${categoryId}` });
|
||||||
|
|
||||||
await bot.editMessageText(
|
await bot.editMessageText(
|
||||||
'Пожалуйста, введите новое название категории:',
|
'Пожалуйста, введите новое название категории:',
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ export default class CreateHandler {
|
|||||||
const jsonExample = JSON.stringify(sampleProducts, null, 2);
|
const jsonExample = JSON.stringify(sampleProducts, null, 2);
|
||||||
const message = `To add product, send a JSON file with product in the following format:\n\n<pre>${jsonExample}</pre>\n\nProduct must have all the fields shown above.\n\nYou can either:\n1. Send the JSON as text\n2. Upload a .json file`;
|
const message = `To add product, send a JSON file with product in the following format:\n\n<pre>${jsonExample}</pre>\n\nProduct must have all the fields shown above.\n\nYou can either:\n1. Send the JSON as text\n2. Upload a .json file`;
|
||||||
|
|
||||||
userStates.set(chatId, {
|
await userStates.set(chatId, {
|
||||||
action: 'import_products',
|
action: 'import_products',
|
||||||
locationId,
|
locationId,
|
||||||
categoryId
|
categoryId
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ export default class DistrictHandler {
|
|||||||
const messageId = callbackQuery.message.message_id;
|
const messageId = callbackQuery.message.message_id;
|
||||||
const [country, city, district] = callbackQuery.data.replace('prod_district_', '').split('_');
|
const [country, city, district] = callbackQuery.data.replace('prod_district_', '').split('_');
|
||||||
|
|
||||||
userStates.delete(chatId);
|
await userStates.delete(chatId);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const location = await LocationService.getLocation(country, city, district);
|
const location = await LocationService.getLocation(country, city, district);
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import logger from '../../../utils/logger.js';
|
|||||||
export default class EditImportHandler {
|
export default class EditImportHandler {
|
||||||
static async handleProductEditImport(msg) {
|
static async handleProductEditImport(msg) {
|
||||||
const chatId = msg.chat.id;
|
const chatId = msg.chat.id;
|
||||||
const state = userStates.get(chatId);
|
const state = await userStates.get(chatId);
|
||||||
|
|
||||||
if (!state || state.action !== 'edit_product') {
|
if (!state || state.action !== 'edit_product') {
|
||||||
return false;
|
return false;
|
||||||
@@ -84,7 +84,7 @@ export default class EditImportHandler {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
userStates.delete(chatId);
|
await userStates.delete(chatId);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error({ err: error }, 'Error importing products');
|
logger.error({ err: error }, 'Error importing products');
|
||||||
await bot.sendMessage(chatId, 'Error importing products. Please check the data and try again.');
|
await bot.sendMessage(chatId, 'Error importing products. Please check the data and try again.');
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ export default class EditStartHandler {
|
|||||||
const jsonExample = JSON.stringify(sampleProduct, null, 2);
|
const jsonExample = JSON.stringify(sampleProduct, null, 2);
|
||||||
const message = `To edit product, send a JSON file with product data:\n\n<pre>${jsonExample}</pre>\n\nYou can either:\n1. Send the JSON as text\n2. Upload a .json file`;
|
const message = `To edit product, send a JSON file with product data:\n\n<pre>${jsonExample}</pre>\n\nYou can either:\n1. Send the JSON as text\n2. Upload a .json file`;
|
||||||
|
|
||||||
userStates.set(chatId, {
|
await userStates.set(chatId, {
|
||||||
action: 'edit_product',
|
action: 'edit_product',
|
||||||
locationId,
|
locationId,
|
||||||
categoryId,
|
categoryId,
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ export default class ImportHandler {
|
|||||||
|
|
||||||
static async handleProductImport(msg) {
|
static async handleProductImport(msg) {
|
||||||
const chatId = msg.chat.id;
|
const chatId = msg.chat.id;
|
||||||
const state = userStates.get(chatId);
|
const state = await userStates.get(chatId);
|
||||||
if (!state || state.action !== 'import_products') return false;
|
if (!state || state.action !== 'import_products') return false;
|
||||||
if (!isAdmin(msg.from.id)) {
|
if (!isAdmin(msg.from.id)) {
|
||||||
await bot.sendMessage(chatId, 'Unauthorized access.');
|
await bot.sendMessage(chatId, 'Unauthorized access.');
|
||||||
@@ -46,7 +46,7 @@ export default class ImportHandler {
|
|||||||
inline_keyboard: [[{ text: '« Back to Products', callback_data: `prod_category_${state.locationId}_${state.categoryId}` }]]
|
inline_keyboard: [[{ text: '« Back to Products', callback_data: `prod_category_${state.locationId}_${state.categoryId}` }]]
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
userStates.delete(chatId);
|
await userStates.delete(chatId);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error({ err: error }, 'Error importing products');
|
logger.error({ err: error }, 'Error importing products');
|
||||||
await bot.sendMessage(chatId, 'Error importing products. Please check the data and try again.');
|
await bot.sendMessage(chatId, 'Error importing products. Please check the data and try again.');
|
||||||
|
|||||||
@@ -76,7 +76,7 @@ export default class ViewHandler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
userStates.set(chatId, {
|
await userStates.set(chatId, {
|
||||||
msgToDelete: [photoMessage.message_id, hiddenPhotoMessage.message_id]
|
msgToDelete: [photoMessage.message_id, hiddenPhotoMessage.message_id]
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -148,7 +148,7 @@ export default class UserProductHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Сохраняем текстовое представление локации в состоянии пользователя
|
// Сохраняем текстовое представление локации в состоянии пользователя
|
||||||
userStates.set(chatId, {
|
await userStates.set(chatId, {
|
||||||
location: `${country}_${city}_${district}`
|
location: `${country}_${city}_${district}`
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -189,7 +189,7 @@ export default class UserProductHandler {
|
|||||||
await bot.deleteMessage(chatId, messageId);
|
await bot.deleteMessage(chatId, messageId);
|
||||||
|
|
||||||
// Получаем состояние пользователя
|
// Получаем состояние пользователя
|
||||||
const state = userStates.get(chatId);
|
const state = await userStates.get(chatId);
|
||||||
|
|
||||||
// Удаляем сообщение с фотографией, если оно существует
|
// Удаляем сообщение с фотографией, если оно существует
|
||||||
if (state && state.photoMessageId) {
|
if (state && state.photoMessageId) {
|
||||||
@@ -243,7 +243,7 @@ export default class UserProductHandler {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Сохраняем состояние пользователя
|
// Сохраняем состояние пользователя
|
||||||
userStates.set(chatId, {
|
await userStates.set(chatId, {
|
||||||
...state,
|
...state,
|
||||||
action: 'viewing_category',
|
action: 'viewing_category',
|
||||||
categoryId,
|
categoryId,
|
||||||
@@ -332,7 +332,7 @@ export default class UserProductHandler {
|
|||||||
await bot.deleteMessage(chatId, messageId);
|
await bot.deleteMessage(chatId, messageId);
|
||||||
|
|
||||||
// Получаем состояние пользователя
|
// Получаем состояние пользователя
|
||||||
const state = userStates.get(chatId);
|
const state = await userStates.get(chatId);
|
||||||
|
|
||||||
// Удаляем сообщение с фотографией, если оно существует
|
// Удаляем сообщение с фотографией, если оно существует
|
||||||
if (state?.photoMessageId) {
|
if (state?.photoMessageId) {
|
||||||
@@ -390,7 +390,7 @@ export default class UserProductHandler {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Сохраняем ID сообщения с фотографией и ID сообщения с товаром в состояние пользователя
|
// Сохраняем ID сообщения с фотографией и ID сообщения с товаром в состояние пользователя
|
||||||
userStates.set(chatId, {
|
await userStates.set(chatId, {
|
||||||
action: 'buying_product',
|
action: 'buying_product',
|
||||||
productId,
|
productId,
|
||||||
quantity: 1,
|
quantity: 1,
|
||||||
@@ -408,7 +408,7 @@ export default class UserProductHandler {
|
|||||||
const chatId = callbackQuery.message.chat.id;
|
const chatId = callbackQuery.message.chat.id;
|
||||||
const messageId = callbackQuery.message.message_id;
|
const messageId = callbackQuery.message.message_id;
|
||||||
const productId = callbackQuery.data.replace('increase_quantity_', '');
|
const productId = callbackQuery.data.replace('increase_quantity_', '');
|
||||||
const state = userStates.get(chatId);
|
const state = await userStates.get(chatId);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const product = await ProductService.getProductById(productId);
|
const product = await ProductService.getProductById(productId);
|
||||||
@@ -428,7 +428,7 @@ export default class UserProductHandler {
|
|||||||
const newQuantity = Math.min(currentQuantity + 1, product.quantity_in_stock);
|
const newQuantity = Math.min(currentQuantity + 1, product.quantity_in_stock);
|
||||||
|
|
||||||
// Update state
|
// Update state
|
||||||
userStates.set(chatId, {
|
await userStates.set(chatId, {
|
||||||
...state,
|
...state,
|
||||||
quantity: newQuantity
|
quantity: newQuantity
|
||||||
});
|
});
|
||||||
@@ -468,7 +468,7 @@ export default class UserProductHandler {
|
|||||||
const chatId = callbackQuery.message.chat.id;
|
const chatId = callbackQuery.message.chat.id;
|
||||||
const messageId = callbackQuery.message.message_id;
|
const messageId = callbackQuery.message.message_id;
|
||||||
const productId = callbackQuery.data.replace('decrease_quantity_', '');
|
const productId = callbackQuery.data.replace('decrease_quantity_', '');
|
||||||
const state = userStates.get(chatId);
|
const state = await userStates.get(chatId);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const product = await ProductService.getProductById(productId)
|
const product = await ProductService.getProductById(productId)
|
||||||
@@ -488,7 +488,7 @@ export default class UserProductHandler {
|
|||||||
const newQuantity = Math.max(currentQuantity - 1, 1);
|
const newQuantity = Math.max(currentQuantity - 1, 1);
|
||||||
|
|
||||||
// Update state
|
// Update state
|
||||||
userStates.set(chatId, {
|
await userStates.set(chatId, {
|
||||||
...state,
|
...state,
|
||||||
quantity: newQuantity
|
quantity: newQuantity
|
||||||
});
|
});
|
||||||
@@ -528,7 +528,7 @@ export default class UserProductHandler {
|
|||||||
const chatId = callbackQuery.message.chat.id;
|
const chatId = callbackQuery.message.chat.id;
|
||||||
const telegramId = callbackQuery.from.id;
|
const telegramId = callbackQuery.from.id;
|
||||||
const productId = callbackQuery.data.replace('buy_product_', '');
|
const productId = callbackQuery.data.replace('buy_product_', '');
|
||||||
const state = userStates.get(chatId);
|
const state = await userStates.get(chatId);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const user = await UserService.getUserByTelegramId(telegramId);
|
const user = await UserService.getUserByTelegramId(telegramId);
|
||||||
@@ -622,7 +622,7 @@ export default class UserProductHandler {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Сохранение ID сообщения с фотографией в состояние пользователя
|
// Сохранение ID сообщения с фотографией в состояние пользователя
|
||||||
userStates.set(chatId, {
|
await userStates.set(chatId, {
|
||||||
...state,
|
...state,
|
||||||
photoMessageId: state?.photoMessageId || null,
|
photoMessageId: state?.photoMessageId || null,
|
||||||
purchaseMessageId: purchaseMessage.message_id
|
purchaseMessageId: purchaseMessage.message_id
|
||||||
@@ -637,7 +637,7 @@ export default class UserProductHandler {
|
|||||||
const chatId = callbackQuery.message.chat.id;
|
const chatId = callbackQuery.message.chat.id;
|
||||||
const telegramId = callbackQuery.from.id;
|
const telegramId = callbackQuery.from.id;
|
||||||
const [walletType, productId, quantity] = callbackQuery.data.replace('pay_with_', '').split('_');
|
const [walletType, productId, quantity] = callbackQuery.data.replace('pay_with_', '').split('_');
|
||||||
const state = userStates.get(chatId);
|
const state = await userStates.get(chatId);
|
||||||
|
|
||||||
if (!Validators.isValidWalletType(walletType)) {
|
if (!Validators.isValidWalletType(walletType)) {
|
||||||
await bot.sendMessage(chatId, 'Invalid wallet type.');
|
await bot.sendMessage(chatId, 'Invalid wallet type.');
|
||||||
@@ -670,7 +670,7 @@ export default class UserProductHandler {
|
|||||||
const balance = user.total_balance + user.bonus_balance;
|
const balance = user.total_balance + user.bonus_balance;
|
||||||
|
|
||||||
if (totalPrice > balance) {
|
if (totalPrice > balance) {
|
||||||
userStates.delete(chatId);
|
await userStates.delete(chatId);
|
||||||
await bot.editMessageText(`Not enough money`, {
|
await bot.editMessageText(`Not enough money`, {
|
||||||
chat_id: chatId,
|
chat_id: chatId,
|
||||||
message_id: callbackQuery.message.message_id,
|
message_id: callbackQuery.message.message_id,
|
||||||
@@ -738,7 +738,7 @@ export default class UserProductHandler {
|
|||||||
await bot.deleteMessage(chatId, callbackQuery.message.message_id);
|
await bot.deleteMessage(chatId, callbackQuery.message.message_id);
|
||||||
|
|
||||||
// Сохраняем ID сообщения с Hidden Photo в состояние пользователя
|
// Сохраняем ID сообщения с Hidden Photo в состояние пользователя
|
||||||
userStates.set(chatId, {
|
await userStates.set(chatId, {
|
||||||
action: 'viewing_purchase',
|
action: 'viewing_purchase',
|
||||||
purchaseId,
|
purchaseId,
|
||||||
hiddenPhotoMessageId: hiddenPhotoMessage ? hiddenPhotoMessage.message_id : null
|
hiddenPhotoMessageId: hiddenPhotoMessage ? hiddenPhotoMessage.message_id : null
|
||||||
|
|||||||
@@ -95,7 +95,7 @@ export default class UserPurchaseHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Удаляем сообщение с Hidden Photo, если оно существует
|
// Удаляем сообщение с Hidden Photo, если оно существует
|
||||||
const state = userStates.get(chatId);
|
const state = await userStates.get(chatId);
|
||||||
if (state?.hiddenPhotoMessageId) {
|
if (state?.hiddenPhotoMessageId) {
|
||||||
try {
|
try {
|
||||||
await bot.deleteMessage(chatId, state.hiddenPhotoMessageId);
|
await bot.deleteMessage(chatId, state.hiddenPhotoMessageId);
|
||||||
@@ -114,7 +114,7 @@ export default class UserPurchaseHandler {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Удаляем состояние пользователя
|
// Удаляем состояние пользователя
|
||||||
userStates.delete(chatId);
|
await userStates.delete(chatId);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.error({ err: e }, 'Error in handlePurchaseListPage');
|
logger.error({ err: e }, 'Error in handlePurchaseListPage');
|
||||||
await bot.sendMessage(chatId, 'Error loading purchase history. Please try again.');
|
await bot.sendMessage(chatId, 'Error loading purchase history. Please try again.');
|
||||||
@@ -168,7 +168,7 @@ export default class UserPurchaseHandler {
|
|||||||
const category = await CategoryService.getCategoryById(product.category_id);
|
const category = await CategoryService.getCategoryById(product.category_id);
|
||||||
|
|
||||||
// Удаляем старое сообщение с Hidden Photo, если оно существует
|
// Удаляем старое сообщение с Hidden Photo, если оно существует
|
||||||
const state = userStates.get(chatId);
|
const state = await userStates.get(chatId);
|
||||||
if (state?.hiddenPhotoMessageId) {
|
if (state?.hiddenPhotoMessageId) {
|
||||||
try {
|
try {
|
||||||
await bot.deleteMessage(chatId, state.hiddenPhotoMessageId);
|
await bot.deleteMessage(chatId, state.hiddenPhotoMessageId);
|
||||||
@@ -219,7 +219,7 @@ export default class UserPurchaseHandler {
|
|||||||
await bot.deleteMessage(chatId, callbackQuery.message.message_id);
|
await bot.deleteMessage(chatId, callbackQuery.message.message_id);
|
||||||
|
|
||||||
// Сохраняем ID сообщения с Hidden Photo в состояние пользователя
|
// Сохраняем ID сообщения с Hidden Photo в состояние пользователя
|
||||||
userStates.set(chatId, {
|
await userStates.set(chatId, {
|
||||||
action: 'viewing_purchase',
|
action: 'viewing_purchase',
|
||||||
purchaseId,
|
purchaseId,
|
||||||
hiddenPhotoMessageId: hiddenPhotoMessage ? hiddenPhotoMessage.message_id : null
|
hiddenPhotoMessageId: hiddenPhotoMessage ? hiddenPhotoMessage.message_id : null
|
||||||
@@ -279,7 +279,7 @@ export default class UserPurchaseHandler {
|
|||||||
await bot.deleteMessage(chatId, messageId);
|
await bot.deleteMessage(chatId, messageId);
|
||||||
|
|
||||||
// Удаляем Hidden Photo, если оно существует
|
// Удаляем Hidden Photo, если оно существует
|
||||||
const state = userStates.get(chatId);
|
const state = await userStates.get(chatId);
|
||||||
if (state?.hiddenPhotoMessageId) {
|
if (state?.hiddenPhotoMessageId) {
|
||||||
try {
|
try {
|
||||||
await bot.deleteMessage(chatId, state.hiddenPhotoMessageId);
|
await bot.deleteMessage(chatId, state.hiddenPhotoMessageId);
|
||||||
@@ -289,7 +289,7 @@ export default class UserPurchaseHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Удаляем состояние пользователя
|
// Удаляем состояние пользователя
|
||||||
userStates.delete(chatId);
|
await userStates.delete(chatId);
|
||||||
|
|
||||||
// Открываем список покупок для пользователя
|
// Открываем список покупок для пользователя
|
||||||
await this.showPurchases({ chat: { id: chatId }, from: { id: callbackQuery.from.id } });
|
await this.showPurchases({ chat: { id: chatId }, from: { id: callbackQuery.from.id } });
|
||||||
|
|||||||
@@ -8,8 +8,11 @@ import adminHandler from './handlers/adminHandlers/adminHandler.js';
|
|||||||
import callbackRouter from './router/callbackRouter.js';
|
import callbackRouter from './router/callbackRouter.js';
|
||||||
import messageRouter from './router/messageRouter.js';
|
import messageRouter from './router/messageRouter.js';
|
||||||
|
|
||||||
|
import { initStates } from './services/stateService.js';
|
||||||
|
|
||||||
await runMigrations();
|
await runMigrations();
|
||||||
await cleanUpInvalidForeignKeys();
|
await cleanUpInvalidForeignKeys();
|
||||||
|
await initStates();
|
||||||
|
|
||||||
const logDebug = (action, functionName) => {
|
const logDebug = (action, functionName) => {
|
||||||
logger.debug({ action, functionName }, 'Button Press');
|
logger.debug({ action, functionName }, 'Button Press');
|
||||||
|
|||||||
10
src/migrations/004_user_states.js
Normal file
10
src/migrations/004_user_states.js
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
export default async function migration004(db) {
|
||||||
|
await db.runAsync(`
|
||||||
|
CREATE TABLE IF NOT EXISTS user_states (
|
||||||
|
chat_id TEXT PRIMARY KEY,
|
||||||
|
state_data TEXT NOT NULL,
|
||||||
|
updated_at INTEGER NOT NULL
|
||||||
|
)
|
||||||
|
`);
|
||||||
|
console.log('Migration 004: user_states table created');
|
||||||
|
}
|
||||||
@@ -38,6 +38,7 @@ export async function runMigrations() {
|
|||||||
(await import('./001_initial_schema.js')).default,
|
(await import('./001_initial_schema.js')).default,
|
||||||
(await import('./002_add_columns.js')).default,
|
(await import('./002_add_columns.js')).default,
|
||||||
(await import('./003_add_indexes.js')).default,
|
(await import('./003_add_indexes.js')).default,
|
||||||
|
(await import('./004_user_states.js')).default,
|
||||||
];
|
];
|
||||||
|
|
||||||
for (let i = currentVersion; i < migrations.length; i++) {
|
for (let i = currentVersion; i < migrations.length; i++) {
|
||||||
|
|||||||
103
src/services/stateService.js
Normal file
103
src/services/stateService.js
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
import db from '../config/database.js';
|
||||||
|
import logger from '../utils/logger.js';
|
||||||
|
|
||||||
|
const TTL_MS = 24 * 60 * 60 * 1000; // 24 hours
|
||||||
|
const CLEANUP_INTERVAL_MS = 60 * 60 * 1000; // 1 hour
|
||||||
|
|
||||||
|
let initialized = false;
|
||||||
|
|
||||||
|
export async function initStates() {
|
||||||
|
if (initialized) return;
|
||||||
|
|
||||||
|
await db.runAsync(`
|
||||||
|
CREATE TABLE IF NOT EXISTS user_states (
|
||||||
|
chat_id TEXT PRIMARY KEY,
|
||||||
|
state_data TEXT NOT NULL,
|
||||||
|
updated_at INTEGER NOT NULL
|
||||||
|
)
|
||||||
|
`);
|
||||||
|
|
||||||
|
initialized = true;
|
||||||
|
logger.info('user_states table initialized');
|
||||||
|
|
||||||
|
setInterval(cleanExpired, CLEANUP_INTERVAL_MS);
|
||||||
|
cleanExpired();
|
||||||
|
}
|
||||||
|
|
||||||
|
function serialize(value) {
|
||||||
|
if (value === undefined) return null;
|
||||||
|
return JSON.stringify(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
function deserialize(json) {
|
||||||
|
if (!json) return undefined;
|
||||||
|
try {
|
||||||
|
return JSON.parse(json);
|
||||||
|
} catch {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function get(chatId) {
|
||||||
|
const row = await db.getAsync(
|
||||||
|
'SELECT state_data, updated_at FROM user_states WHERE chat_id = ?',
|
||||||
|
[String(chatId)]
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!row) return undefined;
|
||||||
|
|
||||||
|
if (Date.now() - row.updated_at > TTL_MS) {
|
||||||
|
await db.runAsync('DELETE FROM user_states WHERE chat_id = ?', [String(chatId)]);
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return deserialize(row.state_data);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function set(chatId, value) {
|
||||||
|
const data = serialize(value);
|
||||||
|
const now = Date.now();
|
||||||
|
|
||||||
|
await db.runAsync(
|
||||||
|
`INSERT INTO user_states (chat_id, state_data, updated_at)
|
||||||
|
VALUES (?, ?, ?)
|
||||||
|
ON CONFLICT(chat_id) DO UPDATE SET state_data = ?, updated_at = ?`,
|
||||||
|
[String(chatId), data, now, data, now]
|
||||||
|
);
|
||||||
|
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function del(chatId) {
|
||||||
|
await db.runAsync('DELETE FROM user_states WHERE chat_id = ?', [String(chatId)]);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function has(chatId) {
|
||||||
|
const row = await db.getAsync(
|
||||||
|
'SELECT updated_at FROM user_states WHERE chat_id = ?',
|
||||||
|
[String(chatId)]
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!row) return false;
|
||||||
|
|
||||||
|
if (Date.now() - row.updated_at > TTL_MS) {
|
||||||
|
await db.runAsync('DELETE FROM user_states WHERE chat_id = ?', [String(chatId)]);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function cleanExpired() {
|
||||||
|
const cutoff = Date.now() - TTL_MS;
|
||||||
|
const result = await db.runAsync(
|
||||||
|
'DELETE FROM user_states WHERE updated_at < ?',
|
||||||
|
[cutoff]
|
||||||
|
);
|
||||||
|
if (result.changes > 0) {
|
||||||
|
logger.info({ expiredCount: result.changes }, 'Cleaned expired user states');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const userStates = { get, set, delete: del, has, initStates, cleanExpired };
|
||||||
|
export default userStates;
|
||||||
Reference in New Issue
Block a user