- Portfolio CRUD: добавление, редактирование, удаление, переключение публикации - Services CRUD: полное управление услугами с возможностью активации/деактивации - Banner system: новая модель Banner с CRUD операциями и аналитикой кликов - Telegram integration: расширенные настройки бота, обнаружение чатов, отправка сообщений - Media management: улучшенная загрузка файлов с оптимизацией изображений и превью - UI improvements: обновлённые админ-панели с rich-text редактором и drag&drop загрузкой - Database: добавлена таблица banners с полями для баннеров и аналитики
374 lines
13 KiB
JavaScript
374 lines
13 KiB
JavaScript
const axios = require('axios');
|
||
|
||
class TelegramService {
|
||
constructor() {
|
||
this.botToken = process.env.TELEGRAM_BOT_TOKEN;
|
||
this.chatId = process.env.TELEGRAM_CHAT_ID;
|
||
this.baseUrl = `https://api.telegram.org/bot${this.botToken}`;
|
||
this.isEnabled = !!(this.botToken && this.chatId);
|
||
this.chats = new Map(); // Store chat information
|
||
this.botInfo = null; // Store bot information
|
||
}
|
||
|
||
// Update bot token and reinitialize
|
||
updateBotToken(newToken) {
|
||
this.botToken = newToken;
|
||
this.baseUrl = `https://api.telegram.org/bot${this.botToken}`;
|
||
this.isEnabled = !!(this.botToken && this.chatId);
|
||
this.botInfo = null; // Reset bot info
|
||
return this.testConnection();
|
||
}
|
||
|
||
// Update default chat ID
|
||
updateChatId(newChatId) {
|
||
this.chatId = newChatId;
|
||
this.isEnabled = !!(this.botToken && this.chatId);
|
||
}
|
||
|
||
// Get bot information
|
||
async getBotInfo() {
|
||
if (this.botInfo) return { success: true, bot: this.botInfo };
|
||
|
||
if (!this.botToken) {
|
||
return { success: false, message: 'Bot token not configured' };
|
||
}
|
||
|
||
try {
|
||
const response = await axios.get(`${this.baseUrl}/getMe`);
|
||
this.botInfo = response.data.result;
|
||
return { success: true, bot: this.botInfo };
|
||
} catch (error) {
|
||
console.error('Get bot info error:', error.response?.data || error.message);
|
||
return { success: false, error: error.message };
|
||
}
|
||
}
|
||
|
||
// Get updates (for getting chat IDs and managing chats)
|
||
async getUpdates() {
|
||
if (!this.botToken) {
|
||
return { success: false, message: 'Bot token not configured' };
|
||
}
|
||
|
||
try {
|
||
const response = await axios.get(`${this.baseUrl}/getUpdates`);
|
||
const updates = response.data.result;
|
||
|
||
// Extract unique chats
|
||
const chats = new Map();
|
||
updates.forEach(update => {
|
||
if (update.message) {
|
||
const chat = update.message.chat;
|
||
chats.set(chat.id, {
|
||
id: chat.id,
|
||
type: chat.type,
|
||
title: chat.title || `${chat.first_name || ''} ${chat.last_name || ''}`.trim(),
|
||
username: chat.username || null,
|
||
first_name: chat.first_name || null,
|
||
last_name: chat.last_name || null,
|
||
description: chat.description || null,
|
||
invite_link: chat.invite_link || null,
|
||
pinned_message: chat.pinned_message || null,
|
||
permissions: chat.permissions || null,
|
||
slow_mode_delay: chat.slow_mode_delay || null
|
||
});
|
||
}
|
||
});
|
||
|
||
// Update internal chat storage
|
||
chats.forEach((chat, id) => {
|
||
this.chats.set(id, chat);
|
||
});
|
||
|
||
return {
|
||
success: true,
|
||
updates,
|
||
chats: Array.from(chats.values()),
|
||
totalUpdates: updates.length
|
||
};
|
||
} catch (error) {
|
||
console.error('Get updates error:', error.response?.data || error.message);
|
||
return { success: false, error: error.message };
|
||
}
|
||
}
|
||
|
||
// Get chat information
|
||
async getChat(chatId) {
|
||
if (!this.botToken) {
|
||
return { success: false, message: 'Bot token not configured' };
|
||
}
|
||
|
||
try {
|
||
const response = await axios.get(`${this.baseUrl}/getChat`, {
|
||
params: { chat_id: chatId }
|
||
});
|
||
const chat = response.data.result;
|
||
|
||
// Store in local cache
|
||
this.chats.set(chatId, chat);
|
||
|
||
return { success: true, chat };
|
||
} catch (error) {
|
||
console.error('Get chat error:', error.response?.data || error.message);
|
||
return { success: false, error: error.message };
|
||
}
|
||
}
|
||
|
||
// Get chat administrators
|
||
async getChatAdministrators(chatId) {
|
||
if (!this.botToken) {
|
||
return { success: false, message: 'Bot token not configured' };
|
||
}
|
||
|
||
try {
|
||
const response = await axios.get(`${this.baseUrl}/getChatAdministrators`, {
|
||
params: { chat_id: chatId }
|
||
});
|
||
return { success: true, administrators: response.data.result };
|
||
} catch (error) {
|
||
console.error('Get chat administrators error:', error.response?.data || error.message);
|
||
return { success: false, error: error.message };
|
||
}
|
||
}
|
||
|
||
// Get available chats (cached)
|
||
getAvailableChats() {
|
||
return Array.from(this.chats.values()).map(chat => ({
|
||
id: chat.id,
|
||
title: chat.title || chat.username || `${chat.first_name || ''} ${chat.last_name || ''}`.trim(),
|
||
type: chat.type,
|
||
username: chat.username
|
||
}));
|
||
}
|
||
|
||
async sendMessage(text, options = {}) {
|
||
const chatId = options.chat_id || this.chatId;
|
||
|
||
if (!this.botToken) {
|
||
console.warn('Telegram bot token is not configured');
|
||
return { success: false, message: 'Telegram bot token not configured' };
|
||
}
|
||
|
||
if (!chatId) {
|
||
console.warn('Telegram chat ID is not specified');
|
||
return { success: false, message: 'Telegram chat ID not specified' };
|
||
}
|
||
|
||
try {
|
||
const response = await axios.post(`${this.baseUrl}/sendMessage`, {
|
||
chat_id: chatId,
|
||
text: text,
|
||
parse_mode: 'HTML',
|
||
disable_web_page_preview: false,
|
||
disable_notification: false,
|
||
...options
|
||
});
|
||
|
||
return { success: true, data: response.data };
|
||
} catch (error) {
|
||
console.error('Telegram send message error:', error.response?.data || error.message);
|
||
return { success: false, error: error.message };
|
||
}
|
||
}
|
||
|
||
// Send message to multiple chats
|
||
async sendBroadcastMessage(text, chatIds = [], options = {}) {
|
||
if (!this.botToken) {
|
||
return { success: false, message: 'Telegram bot token not configured' };
|
||
}
|
||
|
||
if (!chatIds || chatIds.length === 0) {
|
||
return { success: false, message: 'No chat IDs specified' };
|
||
}
|
||
|
||
const results = [];
|
||
const errors = [];
|
||
|
||
for (const chatId of chatIds) {
|
||
try {
|
||
const result = await this.sendMessage(text, {
|
||
...options,
|
||
chat_id: chatId
|
||
});
|
||
|
||
if (result.success) {
|
||
results.push({ chatId, success: true, messageId: result.data.result.message_id });
|
||
} else {
|
||
errors.push({ chatId, error: result.error || result.message });
|
||
}
|
||
|
||
// Add delay between messages to avoid rate limiting
|
||
await new Promise(resolve => setTimeout(resolve, 100));
|
||
} catch (error) {
|
||
errors.push({ chatId, error: error.message });
|
||
}
|
||
}
|
||
|
||
return {
|
||
success: errors.length === 0,
|
||
results,
|
||
errors,
|
||
totalSent: results.length,
|
||
totalFailed: errors.length
|
||
};
|
||
}
|
||
|
||
// Send custom message with advanced options
|
||
async sendCustomMessage({
|
||
text,
|
||
chatIds = [],
|
||
parseMode = 'HTML',
|
||
disableWebPagePreview = false,
|
||
disableNotification = false,
|
||
replyMarkup = null
|
||
}) {
|
||
const targetChats = chatIds.length > 0 ? chatIds : [this.chatId];
|
||
|
||
return await this.sendBroadcastMessage(text, targetChats, {
|
||
parse_mode: parseMode,
|
||
disable_web_page_preview: disableWebPagePreview,
|
||
disable_notification: disableNotification,
|
||
reply_markup: replyMarkup
|
||
});
|
||
}
|
||
|
||
async sendContactNotification(contact) {
|
||
const message = this.formatContactMessage(contact);
|
||
return await this.sendMessage(message);
|
||
}
|
||
|
||
async sendNewContactAlert(contact) {
|
||
const message = `🔔 <b>Новый запрос с сайта!</b>\n\n` +
|
||
`👤 <b>Клиент:</b> ${contact.name}\n` +
|
||
`📧 <b>Email:</b> ${contact.email}\n` +
|
||
`📱 <b>Телефон:</b> ${contact.phone || 'Не указан'}\n` +
|
||
`💼 <b>Услуга:</b> ${contact.serviceInterest || 'Общий запрос'}\n` +
|
||
`💰 <b>Бюджет:</b> ${contact.budget || 'Не указан'}\n` +
|
||
`⏱️ <b>Сроки:</b> ${contact.timeline || 'Не указаны'}\n\n` +
|
||
`💬 <b>Сообщение:</b>\n${contact.message}\n\n` +
|
||
`🕐 <b>Время:</b> ${new Date(contact.createdAt).toLocaleString('ru-RU')}\n\n` +
|
||
`🔗 <a href="${process.env.BASE_URL || 'http://localhost:3000'}/admin/contacts/${contact.id}">Открыть в админ-панели</a>`;
|
||
|
||
return await this.sendMessage(message);
|
||
}
|
||
|
||
async sendPortfolioNotification(portfolio) {
|
||
const message = `📁 <b>Новый проект добавлен в портфолио</b>\n\n` +
|
||
`🏷️ <b>Название:</b> ${portfolio.title}\n` +
|
||
`📂 <b>Категория:</b> ${portfolio.category}\n` +
|
||
`👤 <b>Клиент:</b> ${portfolio.clientName || 'Не указан'}\n` +
|
||
`🌐 <b>URL:</b> ${portfolio.projectUrl || 'Не указан'}\n` +
|
||
`⭐ <b>Рекомендуемый:</b> ${portfolio.featured ? 'Да' : 'Нет'}\n` +
|
||
`📅 <b>Время:</b> ${new Date(portfolio.createdAt).toLocaleString('ru-RU')}\n\n` +
|
||
`🔗 <a href="${process.env.BASE_URL || 'http://localhost:3000'}/portfolio/${portfolio.id}">Посмотреть проект</a>`;
|
||
|
||
return await this.sendMessage(message);
|
||
}
|
||
|
||
async sendServiceNotification(service) {
|
||
const message = `⚙️ <b>Новая услуга добавлена</b>\n\n` +
|
||
`🏷️ <b>Название:</b> ${service.name}\n` +
|
||
`📂 <b>Категория:</b> ${service.category}\n` +
|
||
`💰 <b>Стоимость:</b> ${service.pricing?.basePrice ? `от $${service.pricing.basePrice}` : 'По запросу'}\n` +
|
||
`⏱️ <b>Время выполнения:</b> ${service.estimatedTime || 'Не указано'}\n` +
|
||
`⭐ <b>Рекомендуемая:</b> ${service.featured ? 'Да' : 'Нет'}\n` +
|
||
`📅 <b>Время:</b> ${new Date(service.createdAt).toLocaleString('ru-RU')}\n\n` +
|
||
`🔗 <a href="${process.env.BASE_URL || 'http://localhost:3000'}/services">Посмотреть услуги</a>`;
|
||
|
||
return await this.sendMessage(message);
|
||
}
|
||
|
||
async sendCalculatorQuote(calculatorData) {
|
||
const totalCost = calculatorData.services?.reduce((sum, service) => sum + (service.price || 0), 0) || 0;
|
||
|
||
const message = `💰 <b>Новый расчет стоимости</b>\n\n` +
|
||
`👤 <b>Клиент:</b> ${calculatorData.name || 'Не указан'}\n` +
|
||
`📧 <b>Email:</b> ${calculatorData.email || 'Не указан'}\n` +
|
||
`📱 <b>Телефон:</b> ${calculatorData.phone || 'Не указан'}\n\n` +
|
||
`🛠️ <b>Выбранные услуги:</b>\n${this.formatServices(calculatorData.services)}\n` +
|
||
`💵 <b>Общая стоимость:</b> $${totalCost}\n\n` +
|
||
`📅 <b>Время:</b> ${new Date().toLocaleString('ru-RU')}`;
|
||
|
||
return await this.sendMessage(message);
|
||
}
|
||
|
||
formatContactMessage(contact) {
|
||
return `📞 <b>Уведомление о контакте</b>\n\n` +
|
||
`👤 <b>Клиент:</b> ${contact.name}\n` +
|
||
`📧 <b>Email:</b> ${contact.email}\n` +
|
||
`📱 <b>Телефон:</b> ${contact.phone || 'Не указан'}\n` +
|
||
`💼 <b>Услуга:</b> ${contact.serviceInterest || 'Общий запрос'}\n` +
|
||
`📊 <b>Статус:</b> ${this.getStatusText(contact.status)}\n` +
|
||
`⚡ <b>Приоритет:</b> ${this.getPriorityText(contact.priority)}\n\n` +
|
||
`💬 <b>Сообщение:</b>\n${contact.message}\n\n` +
|
||
`🔗 <a href="${process.env.BASE_URL || 'http://localhost:3000'}/admin/contacts/${contact.id}">Открыть в админ-панели</a>`;
|
||
}
|
||
|
||
formatServices(services) {
|
||
if (!services || services.length === 0) return 'Не выбрано';
|
||
|
||
return services.map(service =>
|
||
`• ${service.name} - $${service.price || 0}`
|
||
).join('\n');
|
||
}
|
||
|
||
getStatusText(status) {
|
||
const statusMap = {
|
||
'new': '🆕 Новое',
|
||
'in_progress': '⏳ В работе',
|
||
'completed': '✅ Завершено'
|
||
};
|
||
return statusMap[status] || status;
|
||
}
|
||
|
||
getPriorityText(priority) {
|
||
const priorityMap = {
|
||
'low': '🟢 Низкий',
|
||
'medium': '🟡 Средний',
|
||
'high': '🔴 Высокий'
|
||
};
|
||
return priorityMap[priority] || priority;
|
||
}
|
||
|
||
// Test connection and update bot info
|
||
async testConnection() {
|
||
if (!this.botToken) {
|
||
return { success: false, message: 'Telegram bot token not configured' };
|
||
}
|
||
|
||
try {
|
||
const response = await axios.get(`${this.baseUrl}/getMe`);
|
||
this.botInfo = response.data.result;
|
||
|
||
// Also get updates to discover available chats
|
||
await this.getUpdates();
|
||
|
||
return {
|
||
success: true,
|
||
bot: this.botInfo,
|
||
availableChats: this.getAvailableChats()
|
||
};
|
||
} catch (error) {
|
||
console.error('Telegram connection test error:', error.response?.data || error.message);
|
||
return { success: false, error: error.message };
|
||
}
|
||
}
|
||
|
||
// Webhook setup (for future use)
|
||
async setWebhook(webhookUrl) {
|
||
if (!this.isEnabled) {
|
||
return { success: false, message: 'Telegram bot not configured' };
|
||
}
|
||
|
||
try {
|
||
const response = await axios.post(`${this.baseUrl}/setWebhook`, {
|
||
url: webhookUrl
|
||
});
|
||
return { success: true, data: response.data };
|
||
} catch (error) {
|
||
console.error('Telegram webhook setup error:', error.response?.data || error.message);
|
||
return { success: false, error: error.message };
|
||
}
|
||
}
|
||
}
|
||
|
||
module.exports = new TelegramService(); |