// Telegram Bot для отправки медиа (изображений/видео) в ЛС const axios = require('axios'); const FormData = require('form-data'); const config = require('./config'); const path = require('path'); if (!config.telegramBotToken) { console.warn('⚠️ TELEGRAM_BOT_TOKEN не установлен! Функция отправки фото в Telegram недоступна.'); } const TELEGRAM_API = config.telegramBotToken ? `https://api.telegram.org/bot${config.telegramBotToken}` : null; // Декодировать HTML entities (например, /) function decodeHtmlEntities(str = '') { if (!str || typeof str !== 'string') { return str; } return str .replace(/&/g, '&') .replace(/</g, '<') .replace(/>/g, '>') .replace(/"/g, '"') .replace(/'/g, "'") .replace(///g, '/') .replace(///g, '/'); } const VIDEO_EXTENSIONS = new Set(['.mp4', '.webm', '.mov', '.m4v', '.avi', '.mkv']); // Получить оригинальный URL из прокси URL function getOriginalUrl(proxyUrl) { if (!proxyUrl) { return proxyUrl; } const cleanUrl = decodeHtmlEntities(proxyUrl); if (!cleanUrl.startsWith('/api/search/proxy/')) { return cleanUrl; } try { // Извлекаем encodedUrl из прокси URL const encodedUrl = cleanUrl.replace('/api/search/proxy/', ''); // Декодируем base64 const originalUrl = Buffer.from(encodedUrl, 'base64').toString('utf-8'); return originalUrl; } catch (error) { console.error('Ошибка декодирования прокси URL:', error); return proxyUrl; } } function looksLikeVideo(url = '', contentType = '') { if (contentType && contentType.toLowerCase().startsWith('video/')) { return true; } let candidate = url; try { const parsed = new URL(url); candidate = parsed.pathname; } catch (e) { // ignore } const ext = path.extname((candidate || '').split('?')[0]).toLowerCase(); return VIDEO_EXTENSIONS.has(ext); } // Отправить медиа (фото/видео) пользователю async function sendPhotoToUser(userId, photoUrl, caption) { if (!TELEGRAM_API) { throw new Error('TELEGRAM_BOT_TOKEN не установлен'); } try { // Получаем оригинальный URL (если это прокси URL) let finalPhotoUrl = getOriginalUrl(photoUrl); // Если это все еще относительный URL (локальный файл), используем публичный URL if (finalPhotoUrl.startsWith('/')) { const baseUrl = process.env.FRONTEND_URL || process.env.API_URL || 'https://nakama.glpshchn.ru'; finalPhotoUrl = `${baseUrl}${finalPhotoUrl}`; } // Проверяем, является ли URL публично доступным для Telegram // Если это оригинальный URL от e621/gelbooru, используем его напрямую const isPublicUrl = finalPhotoUrl.includes('e621.net') || finalPhotoUrl.includes('gelbooru.com') || finalPhotoUrl.includes('nakama.glpshchn.ru'); const isVideo = looksLikeVideo(finalPhotoUrl); if (isPublicUrl) { const payload = { chat_id: userId, caption: caption || '', parse_mode: 'HTML' }; if (isVideo) { const response = await axios.post(`${TELEGRAM_API}/sendVideo`, { ...payload, video: finalPhotoUrl, supports_streaming: true }); return response.data; } const response = await axios.post(`${TELEGRAM_API}/sendPhoto`, { ...payload, photo: finalPhotoUrl }); return response.data; } // Если URL не публичный, скачиваем файл и отправляем как multipart const fileResponse = await axios.get(finalPhotoUrl, { responseType: 'stream', timeout: 30000 }); const contentType = fileResponse.headers['content-type'] || ''; const inferredVideo = isVideo || looksLikeVideo(finalPhotoUrl, contentType); const form = new FormData(); form.append('chat_id', userId); const endpoint = inferredVideo ? 'sendVideo' : 'sendPhoto'; const fieldName = inferredVideo ? 'video' : 'photo'; const defaultExt = inferredVideo ? '.mp4' : '.jpg'; let filename = `file${defaultExt}`; try { const parsed = new URL(finalPhotoUrl); const ext = path.extname(parsed.pathname || ''); if (ext) { filename = `file${ext}`; } } catch (e) { const ext = path.extname(finalPhotoUrl); if (ext) { filename = `file${ext}`; } } form.append(fieldName, fileResponse.data, { filename, contentType: contentType || (inferredVideo ? 'video/mp4' : 'image/jpeg') }); if (caption) { form.append('caption', caption); } form.append('parse_mode', 'HTML'); if (inferredVideo) { form.append('supports_streaming', 'true'); } const response = await axios.post(`${TELEGRAM_API}/${endpoint}`, form, { headers: form.getHeaders() }); return response.data; } catch (error) { console.error('Ошибка отправки медиа:', error.response?.data || error.message); throw error; } } // Отправить несколько фото группой (до 10 штук) async function sendPhotosToUser(userId, photos) { if (!TELEGRAM_API) { throw new Error('TELEGRAM_BOT_TOKEN не установлен'); } try { // Telegram поддерживает до 10 фото в одной группе const batches = []; for (let i = 0; i < photos.length; i += 10) { batches.push(photos.slice(i, i + 10)); } const results = []; for (const batch of batches) { const media = []; for (let index = 0; index < batch.length; index++) { const photo = batch[index]; // Получаем оригинальный URL (если это прокси URL) let photoUrl = getOriginalUrl(photo.url); // Если это относительный URL, преобразуем в полный if (photoUrl.startsWith('/')) { const baseUrl = process.env.FRONTEND_URL || process.env.API_URL || 'https://nakama.glpshchn.ru'; photoUrl = `${baseUrl}${photoUrl}`; } // Проверяем, является ли URL публично доступным const isPublicUrl = photoUrl.includes('e621.net') || photoUrl.includes('gelbooru.com') || photoUrl.includes('nakama.glpshchn.ru'); const isVideo = looksLikeVideo(photoUrl, photo.contentType); if (isPublicUrl) { // Используем публичный URL напрямую media.push({ type: isVideo ? 'video' : 'photo', media: photoUrl, caption: index === 0 ? `Из NakamaHost\n${batch.length} фото` : undefined, parse_mode: 'HTML', ...(isVideo ? { supports_streaming: true } : {}) }); } else { // Для непубличных URL нужно скачать изображение // Но в sendMediaGroup нельзя смешивать URL и файлы // Поэтому используем URL как есть (Telegram попробует загрузить) media.push({ type: isVideo ? 'video' : 'photo', media: photoUrl, caption: index === 0 ? `Из NakamaHost\n${batch.length} фото` : undefined, parse_mode: 'HTML', ...(isVideo ? { supports_streaming: true } : {}) }); } } const response = await axios.post(`${TELEGRAM_API}/sendMediaGroup`, { chat_id: userId, media: media }); results.push(response.data); } return results; } catch (error) { console.error('Ошибка отправки медиа группой:', error.response?.data || error.message); throw error; } } // Обработать данные от Web App async function handleWebAppData(userId, dataString) { try { const data = JSON.parse(dataString); if (data.action === 'send_image') { const caption = `Из NakamaHost\n\n${data.caption || ''}`; await sendPhotoToUser(userId, data.url, caption); return { success: true, message: 'Изображение отправлено!' }; } return { success: false, message: 'Неизвестное действие' }; } catch (error) { console.error('Ошибка обработки данных:', error); return { success: false, message: error.message }; } } module.exports = { sendPhotoToUser, sendPhotosToUser, handleWebAppData };