forked from admin/zeropost-engine
feat: MAX publisher — platform-api.max.ru with image upload
publishToMax():
- API: platform-api.max.ru (новый домен, Authorization header)
- Text: POST /messages?chat_id={id} с {text, attachments}
- Photo: POST /uploads?type=image → presigned URL → multipart upload → {token}
Attach: {type:'image', payload:{token}} в теле сообщения
- Graceful fallback: при ошибке фото — пост без картинки
- Убрана заглушка 'не реализована'
This commit is contained in:
@@ -212,8 +212,71 @@ async function publishToMax({ channel, text, photoUrl, article }) {
|
|||||||
if (!channel.max_channel_id || !channel.max_access_token) {
|
if (!channel.max_channel_id || !channel.max_access_token) {
|
||||||
throw new Error('MAX не настроен');
|
throw new Error('MAX не настроен');
|
||||||
}
|
}
|
||||||
// Заглушка — точный endpoint MAX заполним когда подключим живой канал
|
|
||||||
throw new Error('MAX публикация не реализована');
|
const BASE = 'https://platform-api.max.ru';
|
||||||
|
const token = channel.max_access_token;
|
||||||
|
const chatId = channel.max_channel_id;
|
||||||
|
const headers = { Authorization: token, 'Content-Type': 'application/json' };
|
||||||
|
|
||||||
|
// Добавляем ссылку на статью в конец текста
|
||||||
|
let finalText = text;
|
||||||
|
if (article) {
|
||||||
|
const url = articleUrl(article);
|
||||||
|
if (!finalText.includes(url)) {
|
||||||
|
const buttonText = channel.auto_publish_button_text || DEFAULT_BUTTON_TEXT;
|
||||||
|
finalText = `${finalText}\n\n${buttonText}\n${url}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const body = { text: finalText, attachments: [] };
|
||||||
|
|
||||||
|
// Загрузка фото через 2-step upload
|
||||||
|
if (photoUrl) {
|
||||||
|
try {
|
||||||
|
// Шаг 1: получаем presigned URL для загрузки
|
||||||
|
const uploadUrlRes = await axios.post(`${BASE}/uploads?type=image`, null, {
|
||||||
|
headers: { Authorization: token },
|
||||||
|
timeout: 10_000,
|
||||||
|
});
|
||||||
|
const uploadUrl = uploadUrlRes.data?.url;
|
||||||
|
if (!uploadUrl) throw new Error('MAX: нет upload URL');
|
||||||
|
|
||||||
|
// Шаг 2: загружаем файл (multipart)
|
||||||
|
const localPath = resolveLocalPhoto(photoUrl);
|
||||||
|
let fileBuffer, fileName;
|
||||||
|
if (localPath) {
|
||||||
|
fileBuffer = fs.readFileSync(localPath);
|
||||||
|
fileName = path.basename(localPath);
|
||||||
|
} else {
|
||||||
|
const dl = await axios.get(photoUrl, { responseType: 'arraybuffer', timeout: 30_000 });
|
||||||
|
fileBuffer = Buffer.from(dl.data);
|
||||||
|
fileName = 'photo.jpg';
|
||||||
|
}
|
||||||
|
|
||||||
|
const form = new FormData();
|
||||||
|
form.append('file', fileBuffer, { filename: fileName, contentType: 'image/jpeg' });
|
||||||
|
const uploadRes = await axios.post(uploadUrl, form, {
|
||||||
|
headers: form.getHeaders(),
|
||||||
|
timeout: 60_000,
|
||||||
|
});
|
||||||
|
|
||||||
|
const token_img = uploadRes.data?.token;
|
||||||
|
if (token_img) {
|
||||||
|
body.attachments.push({ type: 'image', payload: { token: token_img } });
|
||||||
|
}
|
||||||
|
} catch (photoErr) {
|
||||||
|
console.warn(`[MAX] photo upload failed, posting without photo: ${photoErr.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!body.attachments.length) delete body.attachments;
|
||||||
|
|
||||||
|
const res = await axios.post(`${BASE}/messages?chat_id=${chatId}`, body, {
|
||||||
|
headers,
|
||||||
|
timeout: 15_000,
|
||||||
|
});
|
||||||
|
if (res.data?.error) throw new Error(`MAX: ${res.data.error.message || res.data.error}`);
|
||||||
|
return res.data?.message?.body?.mid;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function publishOne(scheduledPost) {
|
async function publishOne(scheduledPost) {
|
||||||
|
|||||||
Reference in New Issue
Block a user