const axios = require('axios'); const config = require('../config'); const { log, logError } = require('../middleware/logger'); const User = require('../models/User'); const { normalizeUsername } = require('../services/moderationAdmin'); const OWNER_USERNAMES = new Set(config.moderationOwnerUsernames || []); const BOT_TOKEN = config.telegramBotToken; const TELEGRAM_API = BOT_TOKEN ? `https://api.telegram.org/bot${BOT_TOKEN}` : null; let isPolling = false; let offset = 0; const sendMessage = async (chatId, text, options = {}) => { if (!TELEGRAM_API) { log('warn', 'TELEGRAM_BOT_TOKEN не установлен, отправка сообщения невозможна'); return null; } try { const response = await axios.post(`${TELEGRAM_API}/sendMessage`, { chat_id: chatId, text, parse_mode: 'HTML', ...options }); return response.data; } catch (error) { logError('Ошибка отправки сообщения', error, { chatId, text: text.substring(0, 50) }); return null; } }; const sendMessageToAllUsers = async (messageText) => { if (!TELEGRAM_API) { throw new Error('TELEGRAM_BOT_TOKEN не установлен'); } try { const users = await User.find({ banned: { $ne: true } }).select('telegramId'); let sent = 0; let failed = 0; for (const user of users) { try { await sendMessage(user.telegramId, messageText); sent++; // Небольшая задержка, чтобы не превысить лимиты API await new Promise(resolve => setTimeout(resolve, 50)); } catch (error) { failed++; logError('Ошибка отправки сообщения пользователю', error, { telegramId: user.telegramId }); } } return { sent, failed, total: users.length }; } catch (error) { logError('Ошибка массовой отправки сообщений', error); throw error; } }; const requireAdmin = async (message) => { try { const telegramId = message.from?.id; if (!telegramId) return false; const user = await User.findOne({ telegramId }).select('role'); const username = normalizeUsername(message.from?.username || ''); return user && (user.role === 'admin' || OWNER_USERNAMES.has(username)); } catch (error) { logError('Ошибка проверки прав админа', error); return false; } }; const getStartMessage = () => { return `Добро пожаловать в Nakama! 📱 Nakama - социальная сеть для фурри и аниме сообщества. Основные возможности: • Создание постов с текстом и изображениями • Поиск контента через e621 и Gelbooru • Комментарии и лайки • Подписки на пользователей • Система уведомлений • Фильтры и теги Как начать: 1. Нажмите кнопку "Войти" ниже, чтобы запустить приложение 2. Создайте свой первый пост 3. Подписывайтесь на интересных пользователей Поддержка: Если возникли проблемы, напишите @NakamaReportbot Приятного использования!`; }; const handleCommand = async (message) => { const chatId = message.chat.id; const text = (message.text || '').trim(); const args = text.split(/\s+/); const command = args[0].toLowerCase(); if (command === '/start') { const startParam = message.text.split(' ')[1] || ''; // Если есть start_param (например, post_12345 или ref_ABC123) // Это обрабатывается при открытии миниаппа, здесь просто показываем инструкцию const startMessage = getStartMessage(); // Добавить кнопку для открытия миниаппа let botUsername = 'NakamaSpaceBot'; if (config.telegramBotToken) { try { const botInfo = await axios.get(`${TELEGRAM_API}/getMe`); botUsername = botInfo.data.result.username || 'NakamaSpaceBot'; } catch (error) { log('warn', 'Не удалось получить имя бота', { error: error.message }); } } // Использовать web_app с правильным URL миниаппа const miniappUrl = 'https://nakama.glpshchn.ru/'; await sendMessage(chatId, startMessage, { reply_markup: { inline_keyboard: [[ { text: '🚀 Открыть Nakama', web_app: { url: miniappUrl } } ]] } }); return; } if (command === '/broadcast') { // Проверка прав const isAdmin = await requireAdmin(message); if (!isAdmin) { await sendMessage(chatId, '❌ Команда доступна только администраторам.'); return; } // Получить текст сообщения (все после /broadcast) const messageText = args.slice(1).join(' ').trim(); if (!messageText) { await sendMessage(chatId, 'Использование: /broadcast <сообщение>\n\nПример: /broadcast Всем привет!'); return; } // Отправить подтверждение await sendMessage(chatId, '📤 Начинаю рассылку сообщения всем пользователям...'); try { const result = await sendMessageToAllUsers(messageText); await sendMessage(chatId, `✅ Рассылка завершена!\n\n` + `📊 Статистика:\n` + `• Отправлено: ${result.sent}\n` + `• Ошибок: ${result.failed}\n` + `• Всего пользователей: ${result.total}` ); } catch (error) { logError('Ошибка рассылки через команду', error); await sendMessage(chatId, `❌ Ошибка рассылки: ${error.message}`); } return; } // Игнорируем неизвестные команды }; const handleInlineQuery = async (inlineQuery) => { if (!TELEGRAM_API) return; try { const query = inlineQuery.query || ''; const queryId = inlineQuery.id; // Формат: "furry tag1 tag2" или "anime tag1 tag2" const parts = query.trim().split(/\s+/).filter(p => p.length > 0); if (parts.length === 0) { // Если нет запроса, вернуть пустой результат await axios.post(`${TELEGRAM_API}/answerInlineQuery`, { inline_query_id: queryId, results: [] }); return; } // Первое слово - источник (furry или anime) const source = parts[0].toLowerCase(); const tags = parts.slice(1); if (source !== 'furry' && source !== 'anime') { // Если первый параметр не furry или anime, вернуть пустой результат await axios.post(`${TELEGRAM_API}/answerInlineQuery`, { inline_query_id: queryId, results: [] }); return; } if (tags.length === 0) { // Если нет тегов, вернуть пустой результат await axios.post(`${TELEGRAM_API}/answerInlineQuery`, { inline_query_id: queryId, results: [] }); return; } // Объединить теги в строку для поиска const tagsQuery = tags.join(' '); let searchResults = []; if (source === 'furry') { // Поиск через e621 API try { const config = require('../config'); const E621_USER_AGENT = 'NakamaApp/1.0 (by glpshchn00 on e621; Telegram: @glpshchn00)'; const auth = Buffer.from(`${config.e621Username}:${config.e621ApiKey}`).toString('base64'); const response = await axios.get('https://e621.net/posts.json', { params: { tags: tagsQuery, limit: 50 }, headers: { 'User-Agent': E621_USER_AGENT, 'Authorization': `Basic ${auth}` }, timeout: 10000 }); let postsData = []; if (Array.isArray(response.data)) { postsData = response.data; } else if (response.data && Array.isArray(response.data.posts)) { postsData = response.data.posts; } else if (response.data && Array.isArray(response.data.data)) { postsData = response.data.data; } searchResults = postsData .filter(post => post && post.file && post.file.url) .slice(0, 50) .map(post => ({ id: post.id, url: post.file.url, preview: post.preview && post.preview.url ? post.preview.url : post.file.url, tags: post.tags && post.tags.general ? post.tags.general : [], source: 'e621' })); } catch (error) { logError('Ошибка поиска e621 для inline query', error); } } else if (source === 'anime') { // Поиск через Gelbooru API try { const config = require('../config'); const response = await axios.get('https://gelbooru.com/index.php', { params: { page: 'dapi', s: 'post', q: 'index', json: 1, tags: tagsQuery, limit: 50, api_key: config.gelbooruApiKey, user_id: config.gelbooruUserId }, headers: { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36' }, timeout: 10000 }); let postsData = []; if (Array.isArray(response.data)) { postsData = response.data; } else if (response.data && response.data.post) { postsData = Array.isArray(response.data.post) ? response.data.post : [response.data.post]; } searchResults = postsData .filter(post => post && post.file_url) .slice(0, 50) .map(post => ({ id: post.id, url: post.file_url, preview: post.preview_url || post.thumbnail_url || post.file_url, tags: post.tags ? (typeof post.tags === 'string' ? post.tags.split(' ') : post.tags) : [], source: 'gelbooru' })); } catch (error) { logError('Ошибка поиска Gelbooru для inline query', error); } } // Получить username бота let botUsername = 'NakamaSpaceBot'; try { const botInfo = await axios.get(`${TELEGRAM_API}/getMe`); botUsername = botInfo.data.result.username || 'NakamaSpaceBot'; } catch (error) { log('warn', 'Не удалось получить имя бота для inline query', { error: error.message }); } // Преобразовать результаты в InlineQueryResult const results = searchResults.map((post, index) => { const tagsStr = Array.isArray(post.tags) ? post.tags.slice(0, 10).join(' ') : ''; let caption = ''; if (tagsStr) { caption = `Tags: ${tagsStr}\n\nvia @${botUsername}`; } else { caption = `via @${botUsername}`; } return { type: 'photo', id: `${post.source}_${post.id}_${index}`, photo_url: post.url, thumb_url: post.preview || post.url, caption: caption.substring(0, 1024), parse_mode: 'HTML' }; }); await axios.post(`${TELEGRAM_API}/answerInlineQuery`, { inline_query_id: queryId, results: results, cache_time: 300 // 5 минут }); } catch (error) { logError('Ошибка обработки inline query', error); // Отправить пустой результат при ошибке try { await axios.post(`${TELEGRAM_API}/answerInlineQuery`, { inline_query_id: inlineQuery.id, results: [] }); } catch (e) { // Игнорировать ошибку отправки пустого результата } } }; const processUpdate = async (update) => { // Обработка inline query if (update.inline_query) { await handleInlineQuery(update.inline_query); return; } const message = update.message || update.edited_message; if (!message || !message.text) { return; } try { await handleCommand(message); } catch (error) { logError('Ошибка обработки команды основного бота', error, { chatId: message.chat.id, text: message.text?.substring(0, 50) }); } }; const pollUpdates = async () => { if (!TELEGRAM_API) { log('warn', 'Основной бот не запущен: TELEGRAM_BOT_TOKEN не установлен'); return; } if (isPolling) { return; } isPolling = true; log('info', 'Основной бот запущен, опрос обновлений...'); // При первом запуске получить все обновления и установить offset на последний, // чтобы не отвечать на старые команды const initializeOffset = async () => { try { const response = await axios.get(`${TELEGRAM_API}/getUpdates`, { params: { timeout: 1, allowed_updates: ['message', 'inline_query'] } }); const updates = response.data.result || []; if (updates.length > 0) { // Установить offset на последний update_id + 1, чтобы пропустить все старые обновления offset = updates[updates.length - 1].update_id + 1; log('info', `Пропущено ${updates.length} старых обновлений, offset установлен на ${offset}`); } } catch (error) { log('warn', 'Не удалось инициализировать offset, начнем с 0', { error: error.message }); } }; const poll = async () => { try { const response = await axios.get(`${TELEGRAM_API}/getUpdates`, { params: { offset, timeout: 30, allowed_updates: ['message', 'inline_query'] } }); const updates = response.data.result || []; for (const update of updates) { offset = update.update_id + 1; await processUpdate(update); } // Продолжить опрос setTimeout(poll, 1000); } catch (error) { const errorData = error.response?.data || {}; const errorCode = errorData.error_code; const errorDescription = errorData.description || error.message; // Обработка конфликта 409 - другой экземпляр бота уже опрашивает if (errorCode === 409) { // Не логируем 409 - это ожидаемая ситуация при конфликте экземпляров // Подождать дольше перед повторной попыткой setTimeout(poll, 10000); } else { logError('Ошибка опроса Telegram для основного бота', error); // Переподключиться через 5 секунд setTimeout(poll, 5000); } } }; // Сначала инициализировать offset, затем начать опрос initializeOffset().then(() => { poll(); }); }; const startMainBot = () => { if (!BOT_TOKEN) { log('warn', 'Основной бот не запущен: TELEGRAM_BOT_TOKEN не установлен'); return; } pollUpdates(); }; module.exports = { startMainBot, sendMessageToAllUsers, sendMessage };