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