diff --git a/src/routes/channels.js b/src/routes/channels.js index cb65c1e..d5544c2 100644 --- a/src/routes/channels.js +++ b/src/routes/channels.js @@ -1,6 +1,7 @@ const express = require('express'); const router = express.Router(); const channelsSvc = require('../services/channels'); +const { query } = require('../config/db'); const getUserId = (req) => { const id = req.headers['x-user-id']; @@ -8,7 +9,144 @@ const getUserId = (req) => { return parseInt(id); }; -// GET /api/channels — список каналов пользователя +// ── Admin routes (системные каналы zeropost, без user_id) ───────────────────── + +// GET /api/channels/admin — список системных каналов +router.get('/admin', async (req, res) => { + try { + const { rows } = await query( + `SELECT c.*, + to_jsonb(s.*) - 'channel_id' - 'updated_at' AS style, + to_jsonb(sch.*) - 'channel_id' - 'updated_at' AS schedule + FROM channels c + LEFT JOIN channel_style s ON s.channel_id = c.id + LEFT JOIN channel_schedule sch ON sch.channel_id = c.id + WHERE c.is_system = true + ORDER BY c.created_at ASC` + ); + res.json(rows); + } catch (err) { res.status(500).json({ error: err.message }); } +}); + +// POST /api/channels/admin — создать системный канал +router.post('/admin', async (req, res) => { + try { + const { + name, platform = 'telegram', + tg_channel_id, tg_username, bot_token, + vk_group_id, vk_access_token, + max_channel_id, max_access_token, + niche, audience, goal = 'educational', + } = req.body; + if (!name) return res.status(400).json({ error: 'name is required' }); + const { rows } = await query( + `INSERT INTO channels + (user_id, name, platform, tg_channel_id, tg_username, bot_token, + vk_group_id, vk_access_token, max_channel_id, max_access_token, + niche, audience, goal, is_system) + VALUES (0,$1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,true) + RETURNING *`, + [name, platform, tg_channel_id||null, tg_username||null, bot_token||null, + vk_group_id||null, vk_access_token||null, max_channel_id||null, max_access_token||null, + niche||null, audience||null, goal] + ); + // Создаём style и schedule по умолчанию + await query(`INSERT INTO channel_style (channel_id) VALUES ($1) ON CONFLICT DO NOTHING`, [rows[0].id]); + await query(`INSERT INTO channel_schedule (channel_id) VALUES ($1) ON CONFLICT DO NOTHING`, [rows[0].id]); + res.json(rows[0]); + } catch (err) { res.status(500).json({ error: err.message }); } +}); + +// PATCH /api/channels/admin/:id — обновить системный канал +router.patch('/admin/:id', async (req, res) => { + try { + const allowed = ['name','platform','tg_channel_id','tg_username','bot_token', + 'vk_group_id','vk_access_token','max_channel_id','max_access_token', + 'niche','audience','goal','is_active']; + const fields = []; const vals = []; let i = 1; + for (const key of allowed) { + if (req.body[key] !== undefined) { + fields.push(`${key}=${i++}`); + vals.push(req.body[key]); + } + } + if (!fields.length) return res.status(400).json({ error: 'Nothing to update' }); + fields.push(`updated_at=NOW()`); + vals.push(req.params.id); + const { rows } = await query( + `UPDATE channels SET ${fields.join(',')} WHERE id=${i} AND is_system=true RETURNING *`, + vals + ); + if (!rows.length) return res.status(404).json({ error: 'Not found' }); + res.json(rows[0]); + } catch (err) { res.status(500).json({ error: err.message }); } +}); + +// DELETE /api/channels/admin/:id +router.delete('/admin/:id', async (req, res) => { + try { + await query(`DELETE FROM channels WHERE id=$1 AND is_system=true`, [req.params.id]); + res.json({ ok: true }); + } catch (err) { res.status(500).json({ error: err.message }); } +}); + +// POST /api/channels/admin/:id/publish — опубликовать статью в канал +router.post('/admin/:id/publish', async (req, res) => { + try { + const { article_id, custom_text } = req.body; + const { rows } = await query(`SELECT * FROM channels WHERE id=$1 AND is_system=true`, [req.params.id]); + if (!rows.length) return res.status(404).json({ error: 'Channel not found' }); + const channel = rows[0]; + + let text = custom_text; + // Если текст не передан — берём статью и генерируем пост + if (!text && article_id) { + const { rows: arts } = await query(`SELECT * FROM articles WHERE id=$1`, [article_id]); + if (!arts.length) return res.status(404).json({ error: 'Article not found' }); + const art = arts[0]; + // Простой текст поста из заголовка и excerpt + text = `*${art.title}*\n\n${art.excerpt || ''}\n\nhttps://zeropost.ru/blog/${art.slug}`; + } + if (!text) return res.status(400).json({ error: 'text or article_id required' }); + + const result = { ok: true, platform: channel.platform, text }; + + // Telegram + if (channel.platform === 'telegram' && channel.bot_token && channel.tg_channel_id) { + const axios = require('axios'); + const tgRes = await axios.post( + `https://api.telegram.org/bot${channel.bot_token}/sendMessage`, + { chat_id: channel.tg_channel_id, text, parse_mode: 'Markdown', disable_web_page_preview: false }, + { timeout: 15000 } + ); + result.tg_message_id = tgRes.data?.result?.message_id; + // Сохраняем пост + await query( + `INSERT INTO posts (channel_id, content, status, published_at, tg_message_id) + VALUES ($1,$2,'published',NOW(),$3)`, + [channel.id, text, result.tg_message_id || null] + ); + } + + res.json(result); + } catch (err) { + const msg = err.response?.data?.description || err.message; + res.status(500).json({ error: msg }); + } +}); + +// GET /api/channels/admin/:id/posts — история публикаций канала +router.get('/admin/:id/posts', async (req, res) => { + try { + const { rows } = await query( + `SELECT * FROM posts WHERE channel_id=$1 ORDER BY created_at DESC LIMIT 50`, + [req.params.id] + ); + res.json(rows); + } catch (err) { res.status(500).json({ error: err.message }); } +}); + + router.get('/', async (req, res) => { const userId = getUserId(req); if (!userId) return res.status(401).json({ error: 'x-user-id required' });