Update files
This commit is contained in:
parent
ce159a3fcd
commit
b244fdcf13
157
backend/bot.js
157
backend/bot.js
|
|
@ -1,7 +1,8 @@
|
||||||
// Telegram Bot для отправки изображений в ЛС
|
// Telegram Bot для отправки медиа (изображений/видео) в ЛС
|
||||||
const axios = require('axios');
|
const axios = require('axios');
|
||||||
const FormData = require('form-data');
|
const FormData = require('form-data');
|
||||||
const config = require('./config');
|
const config = require('./config');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
if (!config.telegramBotToken) {
|
if (!config.telegramBotToken) {
|
||||||
console.warn('⚠️ TELEGRAM_BOT_TOKEN не установлен! Функция отправки фото в Telegram недоступна.');
|
console.warn('⚠️ TELEGRAM_BOT_TOKEN не установлен! Функция отправки фото в Telegram недоступна.');
|
||||||
|
|
@ -11,15 +12,39 @@ const TELEGRAM_API = config.telegramBotToken
|
||||||
? `https://api.telegram.org/bot${config.telegramBotToken}`
|
? `https://api.telegram.org/bot${config.telegramBotToken}`
|
||||||
: null;
|
: 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
|
// Получить оригинальный URL из прокси URL
|
||||||
function getOriginalUrl(proxyUrl) {
|
function getOriginalUrl(proxyUrl) {
|
||||||
if (!proxyUrl || !proxyUrl.startsWith('/api/search/proxy/')) {
|
if (!proxyUrl) {
|
||||||
return proxyUrl;
|
return proxyUrl;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const cleanUrl = decodeHtmlEntities(proxyUrl);
|
||||||
|
|
||||||
|
if (!cleanUrl.startsWith('/api/search/proxy/')) {
|
||||||
|
return cleanUrl;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Извлекаем encodedUrl из прокси URL
|
// Извлекаем encodedUrl из прокси URL
|
||||||
const encodedUrl = proxyUrl.replace('/api/search/proxy/', '');
|
const encodedUrl = cleanUrl.replace('/api/search/proxy/', '');
|
||||||
// Декодируем base64
|
// Декодируем base64
|
||||||
const originalUrl = Buffer.from(encodedUrl, 'base64').toString('utf-8');
|
const originalUrl = Buffer.from(encodedUrl, 'base64').toString('utf-8');
|
||||||
return originalUrl;
|
return originalUrl;
|
||||||
|
|
@ -29,7 +54,24 @@ function getOriginalUrl(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) {
|
async function sendPhotoToUser(userId, photoUrl, caption) {
|
||||||
if (!TELEGRAM_API) {
|
if (!TELEGRAM_API) {
|
||||||
throw new Error('TELEGRAM_BOT_TOKEN не установлен');
|
throw new Error('TELEGRAM_BOT_TOKEN не установлен');
|
||||||
|
|
@ -51,42 +93,81 @@ async function sendPhotoToUser(userId, photoUrl, caption) {
|
||||||
finalPhotoUrl.includes('gelbooru.com') ||
|
finalPhotoUrl.includes('gelbooru.com') ||
|
||||||
finalPhotoUrl.includes('nakama.glpshchn.ru');
|
finalPhotoUrl.includes('nakama.glpshchn.ru');
|
||||||
|
|
||||||
|
const isVideo = looksLikeVideo(finalPhotoUrl);
|
||||||
|
|
||||||
if (isPublicUrl) {
|
if (isPublicUrl) {
|
||||||
// Используем публичный URL напрямую
|
const payload = {
|
||||||
const response = await axios.post(`${TELEGRAM_API}/sendPhoto`, {
|
|
||||||
chat_id: userId,
|
chat_id: userId,
|
||||||
photo: finalPhotoUrl,
|
|
||||||
caption: caption || '',
|
caption: caption || '',
|
||||||
parse_mode: 'HTML'
|
parse_mode: 'HTML'
|
||||||
});
|
};
|
||||||
|
|
||||||
return response.data;
|
if (isVideo) {
|
||||||
} else {
|
const response = await axios.post(`${TELEGRAM_API}/sendVideo`, {
|
||||||
// Если URL не публичный, скачиваем изображение и отправляем как файл
|
...payload,
|
||||||
const imageResponse = await axios.get(finalPhotoUrl, {
|
video: finalPhotoUrl,
|
||||||
responseType: 'stream',
|
supports_streaming: true
|
||||||
timeout: 30000
|
});
|
||||||
});
|
return response.data;
|
||||||
|
|
||||||
const form = new FormData();
|
|
||||||
form.append('chat_id', userId);
|
|
||||||
form.append('photo', imageResponse.data, {
|
|
||||||
filename: 'image.jpg',
|
|
||||||
contentType: imageResponse.headers['content-type'] || 'image/jpeg'
|
|
||||||
});
|
|
||||||
if (caption) {
|
|
||||||
form.append('caption', caption);
|
|
||||||
}
|
}
|
||||||
form.append('parse_mode', 'HTML');
|
|
||||||
|
const response = await axios.post(`${TELEGRAM_API}/sendPhoto`, {
|
||||||
const response = await axios.post(`${TELEGRAM_API}/sendPhoto`, form, {
|
...payload,
|
||||||
headers: form.getHeaders()
|
photo: finalPhotoUrl
|
||||||
});
|
});
|
||||||
|
|
||||||
return response.data;
|
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) {
|
} catch (error) {
|
||||||
console.error('Ошибка отправки фото:', error.response?.data || error.message);
|
console.error('Ошибка отправки медиа:', error.response?.data || error.message);
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -125,23 +206,27 @@ async function sendPhotosToUser(userId, photos) {
|
||||||
photoUrl.includes('gelbooru.com') ||
|
photoUrl.includes('gelbooru.com') ||
|
||||||
photoUrl.includes('nakama.glpshchn.ru');
|
photoUrl.includes('nakama.glpshchn.ru');
|
||||||
|
|
||||||
|
const isVideo = looksLikeVideo(photoUrl, photo.contentType);
|
||||||
|
|
||||||
if (isPublicUrl) {
|
if (isPublicUrl) {
|
||||||
// Используем публичный URL напрямую
|
// Используем публичный URL напрямую
|
||||||
media.push({
|
media.push({
|
||||||
type: 'photo',
|
type: isVideo ? 'video' : 'photo',
|
||||||
media: photoUrl,
|
media: photoUrl,
|
||||||
caption: index === 0 ? `<b>Из NakamaHost</b>\n${batch.length} фото` : undefined,
|
caption: index === 0 ? `<b>Из NakamaHost</b>\n${batch.length} фото` : undefined,
|
||||||
parse_mode: 'HTML'
|
parse_mode: 'HTML',
|
||||||
|
...(isVideo ? { supports_streaming: true } : {})
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
// Для непубличных URL нужно скачать изображение
|
// Для непубличных URL нужно скачать изображение
|
||||||
// Но в sendMediaGroup нельзя смешивать URL и файлы
|
// Но в sendMediaGroup нельзя смешивать URL и файлы
|
||||||
// Поэтому используем URL как есть (Telegram попробует загрузить)
|
// Поэтому используем URL как есть (Telegram попробует загрузить)
|
||||||
media.push({
|
media.push({
|
||||||
type: 'photo',
|
type: isVideo ? 'video' : 'photo',
|
||||||
media: photoUrl,
|
media: photoUrl,
|
||||||
caption: index === 0 ? `<b>Из NakamaHost</b>\n${batch.length} фото` : undefined,
|
caption: index === 0 ? `<b>Из NakamaHost</b>\n${batch.length} фото` : undefined,
|
||||||
parse_mode: 'HTML'
|
parse_mode: 'HTML',
|
||||||
|
...(isVideo ? { supports_streaming: true } : {})
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -156,7 +241,7 @@ async function sendPhotosToUser(userId, photos) {
|
||||||
|
|
||||||
return results;
|
return results;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Ошибка отправки фото группой:', error.response?.data || error.message);
|
console.error('Ошибка отправки медиа группой:', error.response?.data || error.message);
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -361,18 +361,22 @@ const sendChannelMediaGroup = async (files, caption) => {
|
||||||
const chatId = config.moderationChannelUsername || '@reichenbfurry';
|
const chatId = config.moderationChannelUsername || '@reichenbfurry';
|
||||||
|
|
||||||
const form = new FormData();
|
const form = new FormData();
|
||||||
const media = files.map((file, index) => ({
|
const media = files.map((file, index) => {
|
||||||
type: 'photo',
|
const isVideo = file.mimetype && file.mimetype.startsWith('video/');
|
||||||
media: `attach://file${index}`,
|
return {
|
||||||
...(index === 0 ? { caption: `${caption}${ERROR_SUPPORT_SUFFIX}`, parse_mode: 'HTML' } : {})
|
type: isVideo ? 'video' : 'photo',
|
||||||
}));
|
media: `attach://file${index}`,
|
||||||
|
...(index === 0 ? { caption: `${caption}${ERROR_SUPPORT_SUFFIX}`, parse_mode: 'HTML' } : {}),
|
||||||
|
...(isVideo ? { supports_streaming: true } : {})
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
form.append('chat_id', chatId);
|
form.append('chat_id', chatId);
|
||||||
form.append('media', JSON.stringify(media));
|
form.append('media', JSON.stringify(media));
|
||||||
|
|
||||||
files.forEach((file, index) => {
|
files.forEach((file, index) => {
|
||||||
form.append(`file${index}`, fs.createReadStream(file.path), {
|
form.append(`file${index}`, fs.createReadStream(file.path), {
|
||||||
filename: file.filename || `image${index}.jpg`
|
filename: file.originalname || file.filename || `media${index}`
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -568,8 +568,8 @@ export default function App() {
|
||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
<label>
|
<label>
|
||||||
Изображения (до 10)
|
Медиа (до 10, фото или видео)
|
||||||
<input type="file" accept="image/*" multiple onChange={handleFileChange} />
|
<input type="file" accept="image/*,video/*" multiple onChange={handleFileChange} />
|
||||||
</label>
|
</label>
|
||||||
{publishState.files.length > 0 && (
|
{publishState.files.length > 0 && (
|
||||||
<div className="file-list">
|
<div className="file-list">
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue