2025-11-10 20:28:24 +00:00
|
|
|
|
// Telegram Bot для отправки медиа (изображений/видео) в ЛС
|
2025-11-03 22:17:25 +00:00
|
|
|
|
const axios = require('axios');
|
2025-11-03 22:55:21 +00:00
|
|
|
|
const FormData = require('form-data');
|
2025-11-03 22:17:25 +00:00
|
|
|
|
const config = require('./config');
|
2025-11-10 20:28:24 +00:00
|
|
|
|
const path = require('path');
|
2025-11-03 22:17:25 +00:00
|
|
|
|
|
2025-11-03 22:51:17 +00:00
|
|
|
|
if (!config.telegramBotToken) {
|
|
|
|
|
|
console.warn('⚠️ TELEGRAM_BOT_TOKEN не установлен! Функция отправки фото в Telegram недоступна.');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const TELEGRAM_API = config.telegramBotToken
|
|
|
|
|
|
? `https://api.telegram.org/bot${config.telegramBotToken}`
|
|
|
|
|
|
: null;
|
2025-11-03 22:17:25 +00:00
|
|
|
|
|
2025-11-10 20:28:24 +00:00
|
|
|
|
// Декодировать 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']);
|
|
|
|
|
|
|
2025-11-03 22:55:21 +00:00
|
|
|
|
// Получить оригинальный URL из прокси URL
|
|
|
|
|
|
function getOriginalUrl(proxyUrl) {
|
2025-11-10 20:28:24 +00:00
|
|
|
|
if (!proxyUrl) {
|
2025-11-03 22:55:21 +00:00
|
|
|
|
return proxyUrl;
|
|
|
|
|
|
}
|
2025-11-10 20:28:24 +00:00
|
|
|
|
|
|
|
|
|
|
const cleanUrl = decodeHtmlEntities(proxyUrl);
|
|
|
|
|
|
|
|
|
|
|
|
if (!cleanUrl.startsWith('/api/search/proxy/')) {
|
|
|
|
|
|
return cleanUrl;
|
|
|
|
|
|
}
|
2025-11-03 22:55:21 +00:00
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
// Извлекаем encodedUrl из прокси URL
|
2025-11-10 20:28:24 +00:00
|
|
|
|
const encodedUrl = cleanUrl.replace('/api/search/proxy/', '');
|
2025-11-03 22:55:21 +00:00
|
|
|
|
// Декодируем base64
|
|
|
|
|
|
const originalUrl = Buffer.from(encodedUrl, 'base64').toString('utf-8');
|
|
|
|
|
|
return originalUrl;
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('Ошибка декодирования прокси URL:', error);
|
|
|
|
|
|
return proxyUrl;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-10 20:28:24 +00:00
|
|
|
|
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);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Отправить медиа (фото/видео) пользователю
|
2025-11-03 22:17:25 +00:00
|
|
|
|
async function sendPhotoToUser(userId, photoUrl, caption) {
|
2025-11-03 22:51:17 +00:00
|
|
|
|
if (!TELEGRAM_API) {
|
|
|
|
|
|
throw new Error('TELEGRAM_BOT_TOKEN не установлен');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-03 22:17:25 +00:00
|
|
|
|
try {
|
2025-11-03 22:55:21 +00:00
|
|
|
|
// Получаем оригинальный URL (если это прокси URL)
|
|
|
|
|
|
let finalPhotoUrl = getOriginalUrl(photoUrl);
|
|
|
|
|
|
|
|
|
|
|
|
// Если это все еще относительный URL (локальный файл), используем публичный URL
|
|
|
|
|
|
if (finalPhotoUrl.startsWith('/')) {
|
2025-11-03 22:51:17 +00:00
|
|
|
|
const baseUrl = process.env.FRONTEND_URL || process.env.API_URL || 'https://nakama.glpshchn.ru';
|
2025-11-03 22:55:21 +00:00
|
|
|
|
finalPhotoUrl = `${baseUrl}${finalPhotoUrl}`;
|
2025-11-03 22:51:17 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-03 22:55:21 +00:00
|
|
|
|
// Проверяем, является ли URL публично доступным для Telegram
|
|
|
|
|
|
// Если это оригинальный URL от e621/gelbooru, используем его напрямую
|
|
|
|
|
|
const isPublicUrl = finalPhotoUrl.includes('e621.net') ||
|
|
|
|
|
|
finalPhotoUrl.includes('gelbooru.com') ||
|
|
|
|
|
|
finalPhotoUrl.includes('nakama.glpshchn.ru');
|
2025-11-03 22:17:25 +00:00
|
|
|
|
|
2025-11-10 20:28:24 +00:00
|
|
|
|
const isVideo = looksLikeVideo(finalPhotoUrl);
|
|
|
|
|
|
|
2025-11-03 22:55:21 +00:00
|
|
|
|
if (isPublicUrl) {
|
2025-11-10 20:28:24 +00:00
|
|
|
|
const payload = {
|
2025-11-03 22:55:21 +00:00
|
|
|
|
chat_id: userId,
|
|
|
|
|
|
caption: caption || '',
|
|
|
|
|
|
parse_mode: 'HTML'
|
2025-11-10 20:28:24 +00:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
if (isVideo) {
|
|
|
|
|
|
const response = await axios.post(`${TELEGRAM_API}/sendVideo`, {
|
|
|
|
|
|
...payload,
|
|
|
|
|
|
video: finalPhotoUrl,
|
|
|
|
|
|
supports_streaming: true
|
|
|
|
|
|
});
|
|
|
|
|
|
return response.data;
|
2025-11-03 22:55:21 +00:00
|
|
|
|
}
|
2025-11-10 20:28:24 +00:00
|
|
|
|
|
|
|
|
|
|
const response = await axios.post(`${TELEGRAM_API}/sendPhoto`, {
|
|
|
|
|
|
...payload,
|
|
|
|
|
|
photo: finalPhotoUrl
|
2025-11-03 22:55:21 +00:00
|
|
|
|
});
|
|
|
|
|
|
return response.data;
|
|
|
|
|
|
}
|
2025-11-10 20:28:24 +00:00
|
|
|
|
|
|
|
|
|
|
// Если 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;
|
2025-11-03 22:17:25 +00:00
|
|
|
|
} catch (error) {
|
2025-11-10 20:28:24 +00:00
|
|
|
|
console.error('Ошибка отправки медиа:', error.response?.data || error.message);
|
2025-11-03 22:17:25 +00:00
|
|
|
|
throw error;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Отправить несколько фото группой (до 10 штук)
|
|
|
|
|
|
async function sendPhotosToUser(userId, photos) {
|
2025-11-03 22:51:17 +00:00
|
|
|
|
if (!TELEGRAM_API) {
|
|
|
|
|
|
throw new Error('TELEGRAM_BOT_TOKEN не установлен');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-03 22:17:25 +00:00
|
|
|
|
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) {
|
2025-11-03 22:55:21 +00:00
|
|
|
|
const media = [];
|
|
|
|
|
|
|
|
|
|
|
|
for (let index = 0; index < batch.length; index++) {
|
|
|
|
|
|
const photo = batch[index];
|
|
|
|
|
|
// Получаем оригинальный URL (если это прокси URL)
|
|
|
|
|
|
let photoUrl = getOriginalUrl(photo.url);
|
|
|
|
|
|
|
|
|
|
|
|
// Если это относительный URL, преобразуем в полный
|
2025-11-03 22:51:17 +00:00
|
|
|
|
if (photoUrl.startsWith('/')) {
|
2025-11-03 22:55:21 +00:00
|
|
|
|
const baseUrl = process.env.FRONTEND_URL || process.env.API_URL || 'https://nakama.glpshchn.ru';
|
2025-11-03 22:51:17 +00:00
|
|
|
|
photoUrl = `${baseUrl}${photoUrl}`;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-03 22:55:21 +00:00
|
|
|
|
// Проверяем, является ли URL публично доступным
|
|
|
|
|
|
const isPublicUrl = photoUrl.includes('e621.net') ||
|
|
|
|
|
|
photoUrl.includes('gelbooru.com') ||
|
|
|
|
|
|
photoUrl.includes('nakama.glpshchn.ru');
|
|
|
|
|
|
|
2025-11-10 20:28:24 +00:00
|
|
|
|
const isVideo = looksLikeVideo(photoUrl, photo.contentType);
|
|
|
|
|
|
|
2025-11-03 22:55:21 +00:00
|
|
|
|
if (isPublicUrl) {
|
|
|
|
|
|
// Используем публичный URL напрямую
|
|
|
|
|
|
media.push({
|
2025-11-10 20:28:24 +00:00
|
|
|
|
type: isVideo ? 'video' : 'photo',
|
2025-11-03 22:55:21 +00:00
|
|
|
|
media: photoUrl,
|
2025-11-20 21:32:48 +00:00
|
|
|
|
caption: index === 0 ? `<b>Из Nakama</b>\n${batch.length} фото` : undefined,
|
2025-11-10 20:28:24 +00:00
|
|
|
|
parse_mode: 'HTML',
|
|
|
|
|
|
...(isVideo ? { supports_streaming: true } : {})
|
2025-11-03 22:55:21 +00:00
|
|
|
|
});
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// Для непубличных URL нужно скачать изображение
|
|
|
|
|
|
// Но в sendMediaGroup нельзя смешивать URL и файлы
|
|
|
|
|
|
// Поэтому используем URL как есть (Telegram попробует загрузить)
|
|
|
|
|
|
media.push({
|
2025-11-10 20:28:24 +00:00
|
|
|
|
type: isVideo ? 'video' : 'photo',
|
2025-11-03 22:55:21 +00:00
|
|
|
|
media: photoUrl,
|
2025-11-20 21:32:48 +00:00
|
|
|
|
caption: index === 0 ? `<b>Из Nakama</b>\n${batch.length} фото` : undefined,
|
2025-11-10 20:28:24 +00:00
|
|
|
|
parse_mode: 'HTML',
|
|
|
|
|
|
...(isVideo ? { supports_streaming: true } : {})
|
2025-11-03 22:55:21 +00:00
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-11-03 22:17:25 +00:00
|
|
|
|
|
|
|
|
|
|
const response = await axios.post(`${TELEGRAM_API}/sendMediaGroup`, {
|
|
|
|
|
|
chat_id: userId,
|
|
|
|
|
|
media: media
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
results.push(response.data);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return results;
|
|
|
|
|
|
} catch (error) {
|
2025-11-10 20:28:24 +00:00
|
|
|
|
console.error('Ошибка отправки медиа группой:', error.response?.data || error.message);
|
2025-11-03 22:17:25 +00:00
|
|
|
|
throw error;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Обработать данные от Web App
|
|
|
|
|
|
async function handleWebAppData(userId, dataString) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const data = JSON.parse(dataString);
|
|
|
|
|
|
|
|
|
|
|
|
if (data.action === 'send_image') {
|
2025-11-20 21:32:48 +00:00
|
|
|
|
const caption = `<b>Из Nakama</b>\n\n${data.caption || ''}`;
|
2025-11-03 22:17:25 +00:00
|
|
|
|
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
|
|
|
|
|
|
};
|
|
|
|
|
|
|