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 axios = require('axios');
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
const FormData = require('form-data');
|
||||||
const { query } = require('../config/db');
|
const { query } = require('../config/db');
|
||||||
const settings = require('./settings');
|
const settings = require('./settings');
|
||||||
|
|
||||||
// Engine-контейнер не имеет монтированных uploads — отправляем фото в Telegram
|
// Engine-контейнер монтирует uploads volume на /var/www/zeropost-uploads
|
||||||
// по публичному URL (Telegram сам скачивает). Базу URL берём из app_settings,
|
// (см. docker-compose), поэтому отправляем фото в Telegram через multipart
|
||||||
// дефолт https://zeropost.ru. Файлы лежат на отдельном nginx и публично сервятся.
|
// 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 MAX_ATTEMPTS = 3;
|
||||||
const TG_CAPTION_LIMIT = 1024;
|
const TG_CAPTION_LIMIT = 1024;
|
||||||
const TG_MESSAGE_LIMIT = 4096;
|
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),
|
* Берёт ОДНУ 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 tgBase = await settings.get('TELEGRAM_API_BASE', 'https://api.telegram.org');
|
||||||
const reply_markup = await buildReplyMarkup(note.id);
|
const reply_markup = await buildReplyMarkup(note.id);
|
||||||
const photoUrl = await buildPhotoUrl(note.image_url);
|
const localPath = resolvePosePath(note.image_url);
|
||||||
|
|
||||||
// sendPhoto если есть URL картинки и текст влезает в caption (1024)
|
// sendPhoto через multipart (файл загружается в TG напрямую) — если есть локальный pose
|
||||||
if (photoUrl && note.content.length <= TG_CAPTION_LIMIT) {
|
// и текст влезает в caption (1024 hard-limit Telegram для sendPhoto).
|
||||||
const res = await axios.post(`${tgBase}/bot${channel.bot_token}/sendPhoto`, {
|
if (localPath && note.content.length <= TG_CAPTION_LIMIT) {
|
||||||
chat_id: channel.tg_channel_id,
|
const form = new FormData();
|
||||||
photo: photoUrl,
|
form.append('chat_id', String(channel.tg_channel_id));
|
||||||
caption: note.content,
|
form.append('caption', note.content);
|
||||||
reply_markup,
|
if (reply_markup) form.append('reply_markup', JSON.stringify(reply_markup));
|
||||||
}, { timeout: 30_000 });
|
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;
|
return res.data?.result?.message_id;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user