nakama/backend/bot.js

273 lines
8.9 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 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(/&lt;/g, '<')
.replace(/&gt;/g, '>')
.replace(/&quot;/g, '"')
.replace(/&#39;/g, "'")
.replace(/&#x2F;/g, '/')
.replace(/&#47;/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 ? `<b>Из Nakama</b>\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 ? `<b>Из Nakama</b>\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 = `<b>Из Nakama</b>\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
};