fix: clean chat navigation — edit messages instead of sending new ones

All callback handlers now use editOrSendCallback() to edit the existing
message in-place instead of bot.sendMessage() which creates new messages
and clutters the chat. If edit fails (message too old), the old message
is deleted and a new one sent.

Added src/utils/messageUtils.js with:
- editOrSendCallback(callbackQuery, text, options) — edit or fallback
- editOrSend(chatId, messageId, text, options) — edit or fallback
- deleteAndSend(chatId, messageId, text, options) — delete then send

Fixed handlers:
- userProductHandler: handleBuyProduct errors, handlePay validation/stock errors
- userPurchaseHandler: viewPurchase errors, handleConfirmReceived errors, handlePurchaseListPage errors
- userLocationHandler: all error paths now edit in-place
- userDeletionHandler: both error paths now edit in-place
- wallet/balanceHandler: showBalance error (text command, acceptable)
- wallet/refreshHandler: user not found and refresh errors
- wallet/topUpHandler: wallet loading error
- wallet/createHandler: invalid wallet type error
- wallet/historyHandler: both transaction history error paths
- wallet/archiveHandler: archived wallets error
This commit is contained in:
NW
2026-06-24 20:45:39 +01:00
parent 8272f36253
commit 6ce8da257a
11 changed files with 71 additions and 35 deletions

View File

@@ -4,10 +4,10 @@ import bot from "../../context/bot.js";
import UserService from "../../services/userService.js";
import userStates from "../../context/userStates.js";
import logger from '../../utils/logger.js';
import { editOrSendCallback } from '../../utils/messageUtils.js';
export default class UserDeletionHandler {
static async handleDeleteAccount(callbackQuery) {
const telegramId = callbackQuery.from.id;
const chatId = callbackQuery.message.chat.id;
try {
@@ -31,7 +31,7 @@ export default class UserDeletionHandler {
);
} catch (error) {
logger.error({ err: error }, 'Error in handleDeleteUser');
await bot.sendMessage(chatId, 'Error processing delete request. Please try again.');
await editOrSendCallback(callbackQuery, 'Error processing delete request. Please try again.');
}
}
@@ -48,7 +48,7 @@ export default class UserDeletionHandler {
);
} catch (error) {
logger.error({ err: error }, 'Error in handleConfirmDelete');
await bot.sendMessage(chatId, 'Error deleting user. Please try again.');
await editOrSendCallback(callbackQuery, 'Error deleting user. Please try again.');
}
}
}

View File

@@ -3,6 +3,7 @@ import LocationService from "../../services/locationService.js";
import bot from "../../context/bot.js";
import UserService from "../../services/userService.js";
import logger from '../../utils/logger.js';
import { editOrSendCallback } from '../../utils/messageUtils.js';
export default class UserLocationHandler {
static async handleSetLocation(callbackQuery) {
@@ -48,7 +49,7 @@ export default class UserLocationHandler {
);
} catch (error) {
logger.error({ err: error }, 'Error in handleSetLocation');
await bot.sendMessage(chatId, 'Error loading countries. Please try again.');
await editOrSendCallback(callbackQuery, 'Error loading countries. Please try again.');
}
}
@@ -80,7 +81,7 @@ export default class UserLocationHandler {
);
} catch (error) {
logger.error({ err: error }, 'Error in handleSetCountry');
await bot.sendMessage(chatId, 'Error loading cities. Please try again.');
await editOrSendCallback(callbackQuery, 'Error loading cities. Please try again.');
}
}
@@ -112,7 +113,7 @@ export default class UserLocationHandler {
);
} catch (error) {
logger.error({ err: error }, 'Error in handleSetCity');
await bot.sendMessage(chatId, 'Error loading districts. Please try again.');
await editOrSendCallback(callbackQuery, 'Error loading districts. Please try again.');
}
}
@@ -142,7 +143,7 @@ export default class UserLocationHandler {
} catch (error) {
await db.runAsync('ROLLBACK');
logger.error({ err: error }, 'Error in handleSetDistrict');
await bot.sendMessage(chatId, 'Error updating location. Please try again.');
await editOrSendCallback(callbackQuery, 'Error updating location. Please try again.');
}
}
}

View File

@@ -8,6 +8,7 @@ import CategoryService from "../../services/categoryService.js";
import UserService from "../../services/userService.js";
import PurchaseService from '../../services/purchaseService.js';
import Validators from '../../utils/validators.js';
import { editOrSendCallback, deleteAndSend } from '../../utils/messageUtils.js';
import fs from 'fs';
import path from 'path';
@@ -572,8 +573,7 @@ export default class UserProductHandler {
// Проверка баланса пользователя
if (userBalance <= 0) {
await bot.sendMessage(
chatId,
await editOrSendCallback(callbackQuery,
`❌ Insufficient balance. Your current balance is $${userBalance}. You need $${totalPrice} to complete this purchase.`,
{
reply_markup: {
@@ -587,8 +587,7 @@ export default class UserProductHandler {
}
if (userBalance < totalPrice) {
await bot.sendMessage(
chatId,
await editOrSendCallback(callbackQuery,
`❌ Insufficient balance. Your current balance is $${userBalance}. You need $${totalPrice} to complete this purchase.`,
{
reply_markup: {
@@ -610,8 +609,7 @@ export default class UserProductHandler {
`, [user.id]);
if (cryptoWallets.length === 0) {
await bot.sendMessage(
chatId,
await editOrSendCallback(callbackQuery,
'You need to add a crypto wallet first to make purchases.',
{
reply_markup: {
@@ -652,7 +650,7 @@ export default class UserProductHandler {
});
} catch (error) {
logger.error({ err: error }, 'Error in handleBuyProduct');
await bot.sendMessage(chatId, 'Error processing purchase. Please try again.');
await editOrSendCallback(callbackQuery, 'Error processing purchase. Please try again.');
}
}
@@ -663,16 +661,16 @@ export default class UserProductHandler {
const state = await userStates.get(chatId);
if (!Validators.isValidWalletType(walletType)) {
await bot.sendMessage(chatId, 'Invalid wallet type.');
await editOrSendCallback(callbackQuery, 'Invalid wallet type.');
return;
}
if (!Validators.isValidNumericId(Number(productId))) {
await bot.sendMessage(chatId, 'Invalid product.');
await editOrSendCallback(callbackQuery, 'Invalid product.');
return;
}
const qty = Number(quantity);
if (!Number.isFinite(qty) || qty <= 0) {
await bot.sendMessage(chatId, 'Invalid quantity.');
await editOrSendCallback(callbackQuery, 'Invalid quantity.');
return;
}
@@ -703,7 +701,7 @@ export default class UserProductHandler {
// Проверка наличия товара
if (product.quantity_in_stock < quantity) {
await bot.sendMessage(chatId, `❌ Not enough items in stock. Only ${product.quantity_in_stock} available.`);
await editOrSendCallback(callbackQuery, `❌ Not enough items in stock. Only ${product.quantity_in_stock} available.`);
return;
}
@@ -764,7 +762,7 @@ export default class UserProductHandler {
});
} catch (error) {
logger.error({ err: error }, 'Error in handlePay');
await bot.sendMessage(chatId, 'Error processing purchase. Please try again.');
await editOrSendCallback(callbackQuery, 'Error processing purchase. Please try again.');
}
}
}

View File

@@ -15,6 +15,7 @@ import CategoryService from "../../services/categoryService.js";
import WalletService from "../../services/walletService.js";
import userStates from "../../context/userStates.js";
import Validators from '../../utils/validators.js';
import { editOrSendCallback, deleteAndSend } from '../../utils/messageUtils.js';
const UPLOADS_DIR = path.join(process.cwd(), 'uploads');
const FALLBACK_PHOTO = path.join(process.cwd(), 'corrupt-photo.jpg');
@@ -144,7 +145,7 @@ export default class UserPurchaseHandler {
await userStates.delete(chatId);
} catch (e) {
logger.error({ err: e }, 'Error in handlePurchaseListPage');
await bot.sendMessage(chatId, 'Error loading purchase history. Please try again.');
await editOrSendCallback(callbackQuery, 'Error loading purchase history. Please try again.');
}
}
@@ -177,14 +178,13 @@ export default class UserPurchaseHandler {
// Получаем данные покупки
const purchase = await PurchaseService.getPurchaseById(purchaseId);
if (!purchase) {
await bot.sendMessage(chatId, "No such purchase");
await editOrSendCallback(callbackQuery, "No such purchase");
return;
}
// Получаем данные товара по product_id
const product = await ProductService.getProductById(purchase.product_id);
if (!product) {
await bot.sendMessage(chatId, "No such product");
await editOrSendCallback(callbackQuery, "No such product");
return;
}
@@ -249,7 +249,7 @@ export default class UserPurchaseHandler {
});
} catch (error) {
logger.error({ err: error }, 'Error in viewPurchase');
await bot.sendMessage(chatId, 'Error loading purchase details. Please try again.');
await editOrSendCallback(callbackQuery, 'Error loading purchase details. Please try again.');
}
}
@@ -262,14 +262,14 @@ export default class UserPurchaseHandler {
// Получаем данные покупки
const purchase = await PurchaseService.getPurchaseById(purchaseId);
if (!purchase) {
await bot.sendMessage(chatId, "Purchase not found.");
await editOrSendCallback(callbackQuery, "Purchase not found.");
return;
}
// Получаем данные пользователя по user_id из покупки
const user = await UserService.getUserByUserId(purchase.user_id);
if (!user) {
await bot.sendMessage(chatId, "User not found for this purchase.");
await editOrSendCallback(callbackQuery, 'User not found.');
return;
}
@@ -318,7 +318,7 @@ export default class UserPurchaseHandler {
await this.showPurchases({ chat: { id: chatId }, from: { id: callbackQuery.from.id } });
} catch (error) {
logger.error({ err: error }, 'Error in handleConfirmReceived');
await bot.sendMessage(chatId, 'Error confirming receipt. Please try again.');
await editOrSendCallback(callbackQuery, 'Error confirming receipt. Please try again.');
}
}
}

View File

@@ -3,6 +3,7 @@ import WalletUtils from '../../../utils/walletUtils.js';
import UserService from '../../../services/userService.js';
import bot from '../../../context/bot.js';
import logger from '../../../utils/logger.js';
import { editOrSendCallback } from '../../../utils/messageUtils.js';
export default class ArchiveHandler {
static async handleViewArchivedWallets(callbackQuery) {
@@ -82,7 +83,7 @@ export default class ArchiveHandler {
});
} catch (error) {
logger.error({ err: error }, 'Error in handleViewArchivedWallets');
await bot.sendMessage(chatId, 'Error loading archived wallets. Please try again.');
await editOrSendCallback(callbackQuery, 'Error loading archived wallets. Please try again.');
}
}
}

View File

@@ -4,6 +4,7 @@ import UserService from '../../../services/userService.js';
import WalletService from '../../../services/walletService.js';
import bot from '../../../context/bot.js';
import logger from '../../../utils/logger.js';
import { editOrSendCallback } from '../../../utils/messageUtils.js';
export default class BalanceHandler {
static async showBalance(msg) {

View File

@@ -5,6 +5,7 @@ import bot from '../../../context/bot.js';
import UserService from '../../../services/userService.js';
import logger from '../../../utils/logger.js';
import WalletHelpers from './helpers.js';
import { editOrSendCallback } from '../../../utils/messageUtils.js';
export default class CreateHandler {
static async handleAddWallet(callbackQuery) {
@@ -35,7 +36,7 @@ export default class CreateHandler {
const walletType = callbackQuery.data.replace('generate_wallet_', '').replace('_', ' ');
if (!Validators.isValidWalletType(walletType)) {
await bot.sendMessage(chatId, 'Invalid wallet type.');
await editOrSendCallback(callbackQuery, 'Invalid wallet type.');
return;
}

View File

@@ -2,6 +2,7 @@ import db from '../../../config/database.js';
import UserService from '../../../services/userService.js';
import bot from '../../../context/bot.js';
import logger from '../../../utils/logger.js';
import { editOrSendCallback } from '../../../utils/messageUtils.js';
export default class HistoryHandler {
static async handleTransactionHistory(callbackQuery, page = 0) {
@@ -11,7 +12,7 @@ export default class HistoryHandler {
try {
const user = await UserService.getUserByTelegramId(telegramId.toString());
if (!user) {
await bot.sendMessage(chatId, 'Profile not found. Please use /start to create one.');
await editOrSendCallback(callbackQuery, 'Profile not found. Please use /start to create one.');
return;
}
@@ -63,7 +64,7 @@ export default class HistoryHandler {
});
} catch (error) {
logger.error({ err: error }, 'Error in handleTransactionHistory');
await bot.sendMessage(chatId, 'Error loading transaction history. Please try again.');
await editOrSendCallback(callbackQuery, 'Error loading transaction history. Please try again.');
}
}
@@ -103,7 +104,7 @@ export default class HistoryHandler {
});
} catch (error) {
logger.error({ err: error }, 'Error in handleWalletHistory');
await bot.sendMessage(chatId, 'Error loading transaction history. Please try again.');
await editOrSendCallback(callbackQuery, 'Error loading transaction history. Please try again.');
}
}
}

View File

@@ -3,6 +3,7 @@ import WalletUtils from '../../../utils/walletUtils.js';
import UserService from '../../../services/userService.js';
import bot from '../../../context/bot.js';
import logger from '../../../utils/logger.js';
import { editOrSendCallback } from '../../../utils/messageUtils.js';
export default class RefreshHandler {
static async handleRefreshBalance(callbackQuery) {
@@ -14,7 +15,7 @@ export default class RefreshHandler {
const user = await UserService.getUserByTelegramId(callbackQuery.from.id.toString());
if (!user) {
await bot.sendMessage(chatId, 'Profile not found. Please use /start to create one.');
await editOrSendCallback(callbackQuery, 'Profile not found. Please use /start to create one.');
return;
}
@@ -69,7 +70,7 @@ export default class RefreshHandler {
} catch (error) {
logger.error({ err: error }, 'Error in handleRefreshBalance');
await bot.answerCallbackQuery(callbackQuery.id, { text: '❌ Error refreshing balances.' });
await bot.sendMessage(chatId, '❌ Error refreshing balances. Please try again.', {
await editOrSendCallback(callbackQuery, '❌ Error refreshing balances. Please try again.', {
reply_markup: { inline_keyboard: [[{ text: '« Back', callback_data: 'back_to_balance' }]] }
});
}

View File

@@ -3,6 +3,7 @@ import WalletUtils from '../../../utils/walletUtils.js';
import UserService from '../../../services/userService.js';
import bot from '../../../context/bot.js';
import logger from '../../../utils/logger.js';
import { editOrSendCallback } from '../../../utils/messageUtils.js';
export default class TopUpHandler {
static async handleTopUpWallet(callbackQuery) {
@@ -56,7 +57,7 @@ export default class TopUpHandler {
});
} catch (error) {
logger.error({ err: error }, 'Error in handleTopUpWallet');
await bot.sendMessage(chatId, 'Error loading wallets. Please try again.');
await editOrSendCallback(callbackQuery, 'Error loading wallets. Please try again.');
}
}
}

31
src/utils/messageUtils.js Normal file
View File

@@ -0,0 +1,31 @@
import bot from '../context/bot.js';
export async function editOrSend(chatId, messageId, text, options = {}) {
if (messageId) {
try {
const result = await bot.editMessageText(text, {
chat_id: chatId,
message_id: messageId,
...options,
});
return result;
} catch (e) {
// message too old or already edited — delete and send new
try { await bot.deleteMessage(chatId, messageId); } catch (_) {}
}
}
return bot.sendMessage(chatId, text, options);
}
export async function editOrSendCallback(callbackQuery, text, options = {}) {
const chatId = callbackQuery.message.chat.id;
const messageId = callbackQuery.message.message_id;
return editOrSend(chatId, messageId, text, options);
}
export async function deleteAndSend(chatId, messageId, text, options = {}) {
if (messageId) {
try { await bot.deleteMessage(chatId, messageId); } catch (_) {}
}
return bot.sendMessage(chatId, text, options);
}