// Одноразовый скрипт: генерирует обложку для welcome-поста и отправляет в TG. // Запуск: node scripts/send-welcome-post.js // // Шлём файл через multipart напрямую — иначе TG прокси (CF Worker) не вытягивает // файлы с zeropost.ru и валится с "Bad Request: failed to get HTTP URL content". const fs = require('fs'); const path = require('path'); const axios = require('axios'); const FormData = require('form-data'); const ROOT = '/var/www/zeropost-engine'; process.chdir(ROOT); require('dotenv').config({ path: path.join(ROOT, '.env') }); const config = require(path.join(ROOT, 'src/config')); const settings = require(path.join(ROOT, 'src/services/settings')); const { query } = require(path.join(ROOT, 'src/config/db')); const UPLOADS_DIR = process.env.UPLOADS_DIR || '/var/www/zeropost-uploads'; let sharp = null; try { sharp = require('sharp'); } catch {} const WELCOME_PROMPT = `Abstract editorial cover illustration for a technology blog about AI, cybersecurity, automation and development. Style: flat geometric shapes, smooth flowing curves, layered planes, vector-clean lines. Modern editorial illustration. Color palette: emerald green (#10b981, #34d399) as primary brand color, soft teal accents, warm off-white background (#fafaf9), subtle dark navy accents. Sophisticated and clean. Mood: clean, modern, calm, intellectual, welcoming — Stripe Press / Linear / Anthropic brand aesthetic. Composition: balanced asymmetry with four implied zones representing four content pillars, flowing diagonal forms, generous negative space, sense of intellectual depth and curiosity. Wide 16:9 format. Strictly: no text, no letters, no logos, no human faces, no robots, no brains, no glowing nodes, no circuit boards, no clocks, no screens.`; async function generateCover() { console.log('[welcome] generating cover via /v1/responses + image_generation...'); const model = process.env.AI_MODEL_IMAGE_VIA_RESPONSES || 'gpt-5.2'; const wrappedInput = `Use the image_generation tool to create the following illustration. Do not write any text response, only call the tool.\n\n${WELCOME_PROMPT}`; const res = await axios.post( `${config.ai.baseUrl}/responses`, { model, input: wrappedInput, tools: [{ type: 'image_generation' }], tool_choice: { type: 'image_generation' }, }, { headers: { Authorization: `Bearer ${config.ai.imageApiKey}` }, timeout: 300_000, } ); const output = res.data?.output || []; const imgCall = output.find(o => o.type === 'image_generation_call'); if (!imgCall || !imgCall.result) { throw new Error('No image returned from /responses'); } const bytes = Buffer.from(imgCall.result, 'base64'); console.log(`[welcome] got ${(bytes.length / 1024).toFixed(0)}KB image`); const ts = Date.now(); const ext = imgCall.output_format || 'png'; const origName = `welcome-${ts}.${ext}`; const origPath = path.join(UPLOADS_DIR, origName); fs.writeFileSync(origPath, bytes); let finalLocal = origPath; if (sharp) { try { const webpName = `welcome-${ts}.webp`; const webpPath = path.join(UPLOADS_DIR, webpName); await sharp(bytes) .resize(1600, null, { withoutEnlargement: true }) .webp({ quality: 86 }) .toFile(webpPath); const size = fs.statSync(webpPath).size; console.log(`[welcome] optimized → ${webpName} (${(size / 1024).toFixed(0)}KB)`); finalLocal = webpPath; } catch (e) { console.warn(`[welcome] sharp skipped: ${e.message}`); } } return { localPath: finalLocal }; } const WELCOME_TEXT = `👋 Это *ZeroPost* — блог про ИИ, кибербезопасность, автоматизацию и разработку. Эксперимент: контент пишет ИИ, а человек только держит курс. Получается технологический блог без воды и хайпа — разборы инструментов, рабочие промпты, реальные кейсы. В канале — анонсы новых материалов с сайта. Каждая заметка отрабатывает один практический вопрос: «как сделать X в инструменте Y и не пожалеть». 🌐 Все материалы: [zeropost.ru](https://zeropost.ru) 📌 Закрепите этот пост, чтобы не потерять`; async function sendToTelegram(localPath) { const { rows: chs } = await query(`SELECT * FROM channels WHERE id=1`); if (!chs.length) throw new Error('Channel 1 not found'); const channel = chs[0]; const tgBase = await settings.get('TELEGRAM_API_BASE', 'https://api.telegram.org'); const reply_markup = { inline_keyboard: [[{ text: '🌐 Открыть сайт', url: 'https://zeropost.ru' }]], }; console.log(`[welcome] uploading ${localPath} to TG via multipart...`); const form = new FormData(); form.append('chat_id', String(channel.tg_channel_id)); form.append('caption', WELCOME_TEXT.slice(0, 1024)); form.append('parse_mode', 'Markdown'); form.append('reply_markup', JSON.stringify(reply_markup)); form.append('photo', fs.createReadStream(localPath)); const res = await axios.post(`${tgBase}/bot${channel.bot_token}/sendPhoto`, form, { headers: form.getHeaders(), timeout: 60000, maxContentLength: Infinity, maxBodyLength: Infinity, }); const messageId = res.data?.result?.message_id; console.log(`[welcome] sent, message_id=${messageId}`); await query( `INSERT INTO posts (channel_id, content, status, published_at, tg_message_id) VALUES ($1,$2,'published',NOW(),$3)`, [channel.id, WELCOME_TEXT, messageId] ); return messageId; } (async () => { try { const { localPath } = await generateCover(); console.log(`[welcome] cover ready at ${localPath}`); const messageId = await sendToTelegram(localPath); console.log(`[welcome] DONE. TG message_id=${messageId}. Длина caption=${WELCOME_TEXT.length}`); process.exit(0); } catch (err) { console.error('[welcome] FAILED:', err.response?.data || err.message); process.exit(1); } })();