diff --git a/src/services/scheduledPostsRunner.js b/src/services/scheduledPostsRunner.js index 78b268d..dba05be 100644 --- a/src/services/scheduledPostsRunner.js +++ b/src/services/scheduledPostsRunner.js @@ -212,8 +212,71 @@ async function publishToMax({ channel, text, photoUrl, article }) { if (!channel.max_channel_id || !channel.max_access_token) { 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) {