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:
Ник (Claude)
2026-06-11 20:01:50 +03:00
parent 6e32241fe8
commit ee63172e08
+65 -2
View File
@@ -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) {