Update files

This commit is contained in:
glpshchn 2025-11-10 23:28:24 +03:00
parent ce159a3fcd
commit b244fdcf13
3 changed files with 133 additions and 44 deletions

View File

@ -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(/&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 // Получить оригинальный 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;
} }
} }

View File

@ -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}`
}); });
}); });

View File

@ -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">