diff --git a/src/services/scheduledPostsRunner.js b/src/services/scheduledPostsRunner.js index 64a77c0..86a3c8d 100644 --- a/src/services/scheduledPostsRunner.js +++ b/src/services/scheduledPostsRunner.js @@ -127,7 +127,13 @@ async function publishToVK({ channel, text, photoUrl, article }) { if (!channel.vk_group_id || !channel.vk_access_token) { throw new Error('VK не настроен'); } - // VK не поддерживает кнопки в постах — добавляем ссылку в конец текста, если её там ещё нет + + const groupId = String(channel.vk_group_id).replace(/^-/, ''); + const ownerId = '-' + groupId; + const token = channel.vk_access_token; + const v = '5.199'; + + // Добавляем ссылку на статью в конец текста let finalText = text; if (article) { const url = articleUrl(article); @@ -136,15 +142,69 @@ async function publishToVK({ channel, text, photoUrl, article }) { finalText = `${finalText}\n\n${buttonText}\n${url}`; } } + + // 2-step upload картинки + let attachments = ''; + if (photoUrl) { + try { + // Шаг 1: получаем upload URL + const uploadServerRes = await axios.get('https://api.vk.com/method/photos.getWallUploadServer', { + params: { group_id: groupId, access_token: token, v }, + timeout: 10_000, + }); + if (uploadServerRes.data?.error) throw new Error(`VK getWallUploadServer: ${uploadServerRes.data.error.error_msg}`); + const uploadUrl = uploadServerRes.data.response.upload_url; + + // Шаг 2: загружаем файл + const localPath = resolveLocalPhoto(photoUrl); + let fileBuffer, fileName; + if (localPath) { + fileBuffer = fs.readFileSync(localPath); + fileName = path.basename(localPath); + } else { + // Внешний URL — скачиваем + 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('photo', fileBuffer, { filename: fileName, contentType: 'image/jpeg' }); + const uploadRes = await axios.post(uploadUrl, form, { + headers: form.getHeaders(), + timeout: 60_000, + }); + const { server, photo: photoData, hash } = uploadRes.data; + if (!photoData) throw new Error('VK upload: пустой ответ'); + + // Шаг 3: сохраняем фото + const saveRes = await axios.get('https://api.vk.com/method/photos.saveWallPhoto', { + params: { group_id: groupId, server, photo: photoData, hash, access_token: token, v }, + timeout: 10_000, + }); + if (saveRes.data?.error) throw new Error(`VK saveWallPhoto: ${saveRes.data.error.error_msg}`); + const saved = saveRes.data.response?.[0]; + if (!saved) throw new Error('VK saveWallPhoto: нет данных'); + + attachments = `photo${saved.owner_id}_${saved.id}`; + } catch (photoErr) { + console.warn(`[VK] photo upload failed, posting without photo: ${photoErr.message}`); + // Публикуем без картинки если загрузка упала + } + } + + // Шаг 4: публикуем пост const params = new URLSearchParams({ - owner_id: '-' + String(channel.vk_group_id).replace(/^-/, ''), + owner_id: ownerId, from_group: '1', message: finalText, - access_token: channel.vk_access_token, - v: '5.199', + access_token: token, + v, }); - const res = await axios.post('https://api.vk.com/method/wall.post', params, { timeout: 15000 }); - if (res.data?.error) throw new Error(`VK: ${res.data.error.error_msg}`); + if (attachments) params.set('attachments', attachments); + + const res = await axios.post('https://api.vk.com/method/wall.post', params, { timeout: 15_000 }); + if (res.data?.error) throw new Error(`VK wall.post: ${res.data.error.error_msg}`); return res.data?.response?.post_id; }