fix(zero): back to multipart photo upload (engine now has uploads volume)
The URL-mode I added earlier (Telegram fetches photo from our public URL) hit 'wrong type of the web page content' from Telegram servers — likely because traefik / Cloudflare serves something non-image to bot User-Agent, or the worker proxy mangles things on the way back. Either way it stopped working. Today we added the persistent /var/www/zeropost-uploads volume to the engine container, so the file is now directly accessible inside engine. Switching back to multipart upload via fs.createReadStream — same pattern as scheduledPostsRunner uses for article covers (and which works fine). ZERO_PUBLIC_BASE_URL setting is no longer needed but harmless if left.
This commit is contained in:
@@ -16,26 +16,39 @@
|
||||
*/
|
||||
|
||||
const axios = require('axios');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const FormData = require('form-data');
|
||||
const { query } = require('../config/db');
|
||||
const settings = require('./settings');
|
||||
|
||||
// Engine-контейнер не имеет монтированных uploads — отправляем фото в Telegram
|
||||
// по публичному URL (Telegram сам скачивает). Базу URL берём из app_settings,
|
||||
// дефолт https://zeropost.ru. Файлы лежат на отдельном nginx и публично сервятся.
|
||||
// Engine-контейнер монтирует uploads volume на /var/www/zeropost-uploads
|
||||
// (см. docker-compose), поэтому отправляем фото в Telegram через multipart
|
||||
// upload (file stream) — это надёжнее URL-режима, особенно когда Telegram
|
||||
// почему-то не может скачать наш публичный URL ("wrong type of web page content").
|
||||
// Тот же подход используется в scheduledPostsRunner для обложек статей.
|
||||
const UPLOADS_DIR = process.env.UPLOADS_DIR || '/var/www/zeropost-uploads';
|
||||
|
||||
/**
|
||||
* Открывает локальный путь для /uploads/* image_url. Возвращает абсолютный путь
|
||||
* или null если файла нет / image_url абсолютный URL / некорректный путь.
|
||||
*/
|
||||
function resolvePosePath(imageUrl) {
|
||||
if (!imageUrl) return null;
|
||||
if (/^https?:\/\//i.test(imageUrl)) return null; // абсолютный URL — multipart не годится
|
||||
let pathname = imageUrl;
|
||||
try { pathname = new URL(imageUrl, 'http://x').pathname; } catch { /* relative */ }
|
||||
if (!pathname.startsWith('/uploads/')) return null;
|
||||
const filename = pathname.replace(/^\/uploads\//, '');
|
||||
if (filename.includes('..') || filename.includes('/')) return null;
|
||||
const local = path.join(UPLOADS_DIR, filename);
|
||||
return fs.existsSync(local) ? local : null;
|
||||
}
|
||||
const MAX_ATTEMPTS = 3;
|
||||
const TG_CAPTION_LIMIT = 1024;
|
||||
const TG_MESSAGE_LIMIT = 4096;
|
||||
|
||||
/**
|
||||
* Превращает relative image_url (/uploads/zero-X.webp) в полный публичный URL
|
||||
* который Telegram сможет скачать. Если image_url уже абсолютный — возвращает как есть.
|
||||
*/
|
||||
async function buildPhotoUrl(imageUrl) {
|
||||
if (!imageUrl) return null;
|
||||
if (/^https?:\/\//i.test(imageUrl)) return imageUrl;
|
||||
const base = await settings.get('ZERO_PUBLIC_BASE_URL', 'https://zeropost.ru');
|
||||
return `${base.replace(/\/$/, '')}${imageUrl.startsWith('/') ? '' : '/'}${imageUrl}`;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Берёт ОДНУ approved-заметку готовую к публикации (scheduled_at <= NOW),
|
||||
@@ -83,16 +96,22 @@ async function sendToTelegram(note, channel) {
|
||||
|
||||
const tgBase = await settings.get('TELEGRAM_API_BASE', 'https://api.telegram.org');
|
||||
const reply_markup = await buildReplyMarkup(note.id);
|
||||
const photoUrl = await buildPhotoUrl(note.image_url);
|
||||
const localPath = resolvePosePath(note.image_url);
|
||||
|
||||
// sendPhoto если есть URL картинки и текст влезает в caption (1024)
|
||||
if (photoUrl && note.content.length <= TG_CAPTION_LIMIT) {
|
||||
const res = await axios.post(`${tgBase}/bot${channel.bot_token}/sendPhoto`, {
|
||||
chat_id: channel.tg_channel_id,
|
||||
photo: photoUrl,
|
||||
caption: note.content,
|
||||
reply_markup,
|
||||
}, { timeout: 30_000 });
|
||||
// sendPhoto через multipart (файл загружается в TG напрямую) — если есть локальный pose
|
||||
// и текст влезает в caption (1024 hard-limit Telegram для sendPhoto).
|
||||
if (localPath && note.content.length <= TG_CAPTION_LIMIT) {
|
||||
const form = new FormData();
|
||||
form.append('chat_id', String(channel.tg_channel_id));
|
||||
form.append('caption', note.content);
|
||||
if (reply_markup) form.append('reply_markup', JSON.stringify(reply_markup));
|
||||
form.append('photo', fs.createReadStream(localPath));
|
||||
const res = await axios.post(`${tgBase}/bot${channel.bot_token}/sendPhoto`, form, {
|
||||
headers: form.getHeaders(),
|
||||
timeout: 60_000,
|
||||
maxContentLength: Infinity,
|
||||
maxBodyLength: Infinity,
|
||||
});
|
||||
return res.data?.result?.message_id;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user