All checks were successful
Release: multi-arch Docker images / build-push (push) Successful in 11m28s
- src/services/commissionService.js: read commission wallets and shop_activated from site_settings (DB) with env fallback, 30s cache - src/handlers/adminHandlers/adminWalletsHandler.js: commission wallets from CommissionService instead of static config - src/handlers/userHandlers/userHandler.js: canUseBot blocks all users when shop not activated - src/migrations/015_shop_activation.js: seed shop_activated + commission_wallet_* into site_settings; register in runner - admin-next: /api/system/info (onion hosts), /api/commission-wallets GET/PUT (super admin), /api/activation GET/PUT (super admin); lib/commission-wallets.ts shared helper; onion badge in header; shop activation card in settings; wallet editor in wallets page - test: commissionService.test.js (10 cases: DB/env fallback, caching)
271 lines
11 KiB
JavaScript
271 lines
11 KiB
JavaScript
// userHandler.js
|
|
|
|
import config from "../../config/config.js";
|
|
import bot from "../../context/bot.js";
|
|
import UserService from "../../services/userService.js";
|
|
import WalletService from "../../services/walletService.js";
|
|
import logger from "../../utils/logger.js";
|
|
import { resetUserContext } from "../../utils/messageUtils.js";
|
|
import userStates from "../../context/userStates.js";
|
|
import chatbotService from "../../services/chatbotService.js";
|
|
import CommissionService from "../../services/commissionService.js";
|
|
import leadService from "../../services/leadService.js";
|
|
import { tForUser, LANGUAGE_NAMES, AVAILABLE_LANGUAGES } from '../../i18n/index.js';
|
|
|
|
export default class UserHandler {
|
|
static async canUseBot(msg) {
|
|
const telegramId = msg.from.id;
|
|
const user = await UserService.getUserByTelegramId(telegramId);
|
|
msg.__user = user; // Cache user for downstream handlers
|
|
const lang = user?.language || 'en';
|
|
const t = tForUser(lang);
|
|
|
|
if (!(await CommissionService.getShopActivated())) {
|
|
await bot.sendMessage(telegramId, '⛔ Shop is not activated yet. Please contact support.');
|
|
return false;
|
|
}
|
|
|
|
const keyboard = {
|
|
inline_keyboard: [
|
|
[{text: t('bot.contact_support'), url: config.SUPPORT_LINK}]
|
|
]
|
|
};
|
|
|
|
switch (user?.status) {
|
|
case 0:
|
|
return true;
|
|
case 1:
|
|
await bot.sendMessage(telegramId, t('bot.account_deleted'), {reply_markup: keyboard});
|
|
return false;
|
|
case 2:
|
|
await bot.sendMessage(telegramId, t('bot.account_blocked'), {reply_markup: keyboard});
|
|
return false;
|
|
default:
|
|
return true;
|
|
}
|
|
}
|
|
|
|
static async showProfile(msg) {
|
|
const chatId = msg.chat.id;
|
|
const telegramId = msg.from.id;
|
|
const user = await UserService.getUserByTelegramId(telegramId);
|
|
const lang = user?.language || 'en';
|
|
const t = tForUser(lang);
|
|
|
|
try {
|
|
await UserService.recalculateUserBalanceByTelegramId(telegramId);
|
|
const userStats = await UserService.getDetailedUserByTelegramId(telegramId);
|
|
|
|
if (!userStats) {
|
|
await bot.sendMessage(chatId, t('profile.not_found'));
|
|
return;
|
|
}
|
|
|
|
const activeWalletsBalance = await WalletService.getActiveWalletsBalance(userStats.id);
|
|
const archivedWalletsBalance = await WalletService.getArchivedWalletsBalance(userStats.id);
|
|
const availableBalance = userStats.bonus_balance + (userStats.total_balance || 0);
|
|
|
|
const locationText = userStats.country && userStats.city && userStats.district
|
|
? `${userStats.country}, ${userStats.city}, ${userStats.district}`
|
|
: t('profile.location_not_set');
|
|
|
|
const text = `
|
|
${t('profile.title')}
|
|
|
|
${t('profile.telegram_id')}: \`${telegramId}\`
|
|
${t('profile.location')}: ${locationText}
|
|
|
|
${t('profile.stats')}
|
|
├ ${t('profile.total_purchases')}: ${userStats.purchase_count || 0}
|
|
├ ${t('profile.total_spent')}: $${userStats.total_spent || 0}
|
|
├ ${t('profile.active_wallets')}: ${userStats.crypto_wallet_count || 0} ($${activeWalletsBalance.toFixed(2)})
|
|
├ ${t('profile.archived_wallets')}: ${userStats.archived_wallet_count || 0} ($${archivedWalletsBalance.toFixed(2)})
|
|
├ ${t('profile.bonus_balance')}: $${userStats.bonus_balance || 0}
|
|
└ ${t('profile.available_balance')}: $${availableBalance.toFixed(2)}
|
|
|
|
${t('profile.member_since')}: ${new Date(userStats.created_at).toLocaleDateString()}
|
|
`;
|
|
|
|
const keyboard = {
|
|
inline_keyboard: [
|
|
[{text: t('profile.set_location'), callback_data: 'set_location'}],
|
|
[{text: t('profile.change_language'), callback_data: 'change_language'}],
|
|
[{text: t('profile.delete_account'), callback_data: 'delete_account'}]
|
|
]
|
|
};
|
|
|
|
const result = await bot.sendMessage(chatId, text, {
|
|
parse_mode: 'Markdown',
|
|
reply_markup: keyboard
|
|
});
|
|
await userStates.set(chatId, { ...(await userStates.get(chatId) || {}), lastInlineMessageId: result.message_id });
|
|
} catch (error) {
|
|
logger.error({ err: error }, 'Error in showProfile');
|
|
await bot.sendMessage(chatId, t('profile.error_loading'));
|
|
}
|
|
}
|
|
|
|
static async handleStart(msg) {
|
|
const chatId = msg.chat.id;
|
|
const telegramId = msg.from.id;
|
|
const username = msg.chat.username;
|
|
|
|
try {
|
|
await resetUserContext(chatId);
|
|
await UserService.createUser({
|
|
telegram_id: telegramId,
|
|
username: username
|
|
});
|
|
|
|
// Фиксация лида при активации чата (/start)
|
|
const lead = await leadService.getOrCreateLead({
|
|
telegramId,
|
|
username,
|
|
name: msg.from?.first_name || null,
|
|
});
|
|
if (lead?.id) {
|
|
await leadService.logInteraction({
|
|
telegramId,
|
|
leadId: lead.id,
|
|
action: 'bot_start',
|
|
details: { username: username || null, chatType: msg.chat?.type || 'private' },
|
|
});
|
|
}
|
|
|
|
// Sleep mode: если магазин на паузе — живой ИИ-диалог вместо каталога
|
|
if (await chatbotService.isSleepMode()) {
|
|
const user = await UserService.getUserByTelegramId(telegramId);
|
|
// Если язык ещё не выбран (первый старт) — сначала выбор языка
|
|
if (!user?.language_set) {
|
|
const keyboard = {
|
|
inline_keyboard: AVAILABLE_LANGUAGES.map(code => [{
|
|
text: LANGUAGE_NAMES[code],
|
|
callback_data: `set_language_${code}`
|
|
}])
|
|
};
|
|
await bot.sendMessage(chatId, tForUser('en')('bot.language_select'), { reply_markup: keyboard });
|
|
return;
|
|
}
|
|
const lang = user?.language || 'en';
|
|
// Приветствие от ИИ-бота (живой диалог, не заглушка)
|
|
const welcome = await chatbotService.sendToChatbot({
|
|
sessionId: telegramId,
|
|
message: '/start',
|
|
telegramId,
|
|
language: lang,
|
|
username,
|
|
name: msg.from?.first_name,
|
|
});
|
|
if (welcome.reply) {
|
|
await bot.sendMessage(chatId, welcome.reply);
|
|
} else {
|
|
await bot.sendMessage(chatId, await chatbotService.getWelcomeMessage());
|
|
}
|
|
return;
|
|
}
|
|
|
|
const keyboard = {
|
|
inline_keyboard: AVAILABLE_LANGUAGES.map(code => [{
|
|
text: LANGUAGE_NAMES[code],
|
|
callback_data: `set_language_${code}`
|
|
}])
|
|
};
|
|
await bot.sendMessage(chatId, tForUser('en')('bot.language_select'), { reply_markup: keyboard });
|
|
} catch (error) {
|
|
logger.error({ err: error }, 'Error in handleStart');
|
|
const fallbackT = tForUser('en');
|
|
await bot.sendMessage(chatId, fallbackT('bot.error_generic'));
|
|
}
|
|
}
|
|
|
|
static async handleSetLanguage(callbackQuery) {
|
|
const chatId = callbackQuery.message.chat.id;
|
|
const telegramId = callbackQuery.from.id;
|
|
const lang = callbackQuery.data.replace('set_language_', '');
|
|
|
|
if (!AVAILABLE_LANGUAGES.includes(lang)) {
|
|
await bot.answerCallbackQuery(callbackQuery.id);
|
|
return;
|
|
}
|
|
|
|
try {
|
|
await UserService.setUserLanguage(telegramId, lang);
|
|
const t = tForUser(lang);
|
|
|
|
await bot.answerCallbackQuery(callbackQuery.id);
|
|
|
|
// Если магазин на паузе — после выбора языка сразу ИИ-диалог на выбранном языке
|
|
if (await chatbotService.isSleepMode()) {
|
|
try { await bot.deleteMessage(chatId, callbackQuery.message.message_id); } catch {}
|
|
const welcome = await chatbotService.sendToChatbot({
|
|
sessionId: telegramId,
|
|
message: '/start',
|
|
telegramId,
|
|
language: lang,
|
|
username: callbackQuery.from?.username,
|
|
name: callbackQuery.from?.first_name,
|
|
});
|
|
await bot.sendMessage(chatId, welcome.reply || await chatbotService.getWelcomeMessage(), {
|
|
reply_markup: { remove_keyboard: true },
|
|
});
|
|
return;
|
|
}
|
|
|
|
const keyboard = {
|
|
reply_markup: {
|
|
keyboard: [
|
|
[t('keyboard.products'), t('keyboard.profile')],
|
|
[t('keyboard.purchases'), t('keyboard.wallets')]
|
|
],
|
|
resize_keyboard: true
|
|
}
|
|
};
|
|
|
|
await bot.deleteMessage(chatId, callbackQuery.message.message_id);
|
|
await bot.sendMessage(chatId, t('bot.language_changed', { language: LANGUAGE_NAMES[lang] }), keyboard);
|
|
} catch (error) {
|
|
logger.error({ err: error }, 'Error in handleSetLanguage');
|
|
await bot.answerCallbackQuery(callbackQuery.id);
|
|
}
|
|
}
|
|
|
|
static async handleChangeLanguage(callbackQuery) {
|
|
const chatId = callbackQuery.message.chat.id;
|
|
const user = callbackQuery.message.__user || await UserService.getUserByTelegramId(callbackQuery.from.id);
|
|
const currentLang = user?.language || 'en';
|
|
|
|
const keyboard = {
|
|
inline_keyboard: AVAILABLE_LANGUAGES.map(code => [{
|
|
text: LANGUAGE_NAMES[code],
|
|
callback_data: `set_language_${code}`
|
|
}])
|
|
};
|
|
|
|
try {
|
|
await bot.deleteMessage(chatId, callbackQuery.message.message_id);
|
|
} catch {}
|
|
|
|
await bot.sendMessage(chatId, tForUser(currentLang)('bot.language_select'), { reply_markup: keyboard });
|
|
}
|
|
|
|
static async handleLanguageCommand(msg) {
|
|
const chatId = msg.chat.id;
|
|
|
|
const keyboard = {
|
|
inline_keyboard: AVAILABLE_LANGUAGES.map(code => [{
|
|
text: LANGUAGE_NAMES[code],
|
|
callback_data: `set_language_${code}`
|
|
}])
|
|
};
|
|
|
|
await bot.sendMessage(chatId, tForUser('en')('bot.language_select'), { reply_markup: keyboard });
|
|
}
|
|
|
|
static async handleBackToProfile(callbackQuery) {
|
|
await this.showProfile({
|
|
chat: {id: callbackQuery.message.chat.id},
|
|
from: {id: callbackQuery.from.id}
|
|
});
|
|
await bot.deleteMessage(callbackQuery.message.chat.id, callbackQuery.message.message_id);
|
|
}
|
|
} |