const express = require('express'); const router = express.Router(); const channelsSvc = require('../services/channels'); const settings = require('../services/settings'); const autoPublish = require('../services/articleAutoPublish'); const { query } = require('../config/db'); const getUserId = (req) => { const id = req.headers['x-user-id']; if (!id) return null; return parseInt(id); }; // ── 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] ); 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', // autopublish 'auto_publish_enabled','auto_publish_categories','auto_publish_delay_min', 'auto_publish_template','auto_publish_with_cover','auto_publish_button_text','auto_publish_image_source', ]; 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 — опубликовать статью в канал ПРЯМО СЕЙЧАС // Использует channel.auto_publish_template (если есть) и channel.auto_publish_with_cover. router.post('/admin/:id/publish', async (req, res) => { try { const { article_id, custom_text, with_cover } = 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]; // Создаём временный scheduled_post на NOW и сразу запускаем runner на него. const { rows: spRows } = await query( `INSERT INTO scheduled_posts (channel_id, article_id, custom_text, scheduled_at, status) VALUES ($1,$2,$3,NOW(),'pending') RETURNING *`, [channel.id, article_id || null, custom_text || null] ); const sp = spRows[0]; // Точечный запуск const runner = require('../services/scheduledPostsRunner'); try { const { messageId } = await runner.publishOne(sp); await query( `UPDATE scheduled_posts SET status='sent', published_at=NOW(), error=NULL WHERE id=$1`, [sp.id] ); return res.json({ ok: true, platform: channel.platform, tg_message_id: messageId || null, scheduled_post_id: sp.id }); } catch (err) { const msg = err.response?.data?.description || err.response?.data?.error?.error_msg || err.message; await query( `UPDATE scheduled_posts SET status='failed', error=$1 WHERE id=$2`, [String(msg).slice(0, 1000), sp.id] ); return res.status(500).json({ error: msg }); } } 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 }); } }); // ── Publish slots ───────────────────────────────────────────────────────────── // GET /api/channels/admin/:id/slots router.get('/admin/:id/slots', async (req, res) => { try { const { rows } = await query( `SELECT * FROM publish_slots WHERE channel_id=$1 ORDER BY sort_order, slot_hour, slot_minute`, [req.params.id] ); res.json(rows); } catch (err) { res.status(500).json({ error: err.message }); } }); // POST /api/channels/admin/:id/slots router.post('/admin/:id/slots', async (req, res) => { try { const { slot_hour, slot_minute, label, enabled = true } = req.body; const { rows: existing } = await query( `SELECT COUNT(*) as cnt FROM publish_slots WHERE channel_id=$1`, [req.params.id] ); const sort_order = parseInt(existing[0].cnt); const { rows } = await query( `INSERT INTO publish_slots (channel_id, slot_hour, slot_minute, label, enabled, sort_order) VALUES ($1,$2,$3,$4,$5,$6) ON CONFLICT (channel_id, slot_hour, slot_minute) DO UPDATE SET label=$4, enabled=$5 RETURNING *`, [req.params.id, slot_hour, slot_minute, label || null, enabled, sort_order] ); res.json(rows[0]); } catch (err) { res.status(500).json({ error: err.message }); } }); // DELETE /api/channels/admin/:id/slots/:slotId router.delete('/admin/:id/slots/:slotId', async (req, res) => { try { await query(`DELETE FROM publish_slots WHERE id=$1 AND channel_id=$2`, [req.params.slotId, req.params.id]); res.json({ ok: true }); } catch (err) { res.status(500).json({ error: err.message }); } }); // GET /api/channels/admin/:id/scheduled — запланированные посты канала (pending+failed+sent последние) router.get('/admin/:id/scheduled', async (req, res) => { try { const { rows } = await query( `SELECT sp.*, a.title as article_title, a.slug as article_slug, a.category as article_category FROM scheduled_posts sp LEFT JOIN articles a ON a.id = sp.article_id WHERE sp.channel_id=$1 ORDER BY sp.scheduled_at DESC LIMIT 30`, [req.params.id] ); res.json(rows); } catch (err) { res.status(500).json({ error: err.message }); } }); // POST /api/channels/admin/:id/schedule — поставить пост в очередь // scheduled_at: если не передан — берём ближайший слот канала (через autoPublish.pickScheduleTime). router.post('/admin/:id/schedule', async (req, res) => { try { const { article_id, custom_text, scheduled_at } = req.body; const { rows: chs } = await query(`SELECT * FROM channels WHERE id=$1 AND is_system=true`, [req.params.id]); if (!chs.length) return res.status(404).json({ error: 'Channel not found' }); const when = scheduled_at ? new Date(scheduled_at) : await autoPublish.pickScheduleTime(chs[0]); const { rows } = await query( `INSERT INTO scheduled_posts (channel_id, article_id, custom_text, scheduled_at) VALUES ($1,$2,$3,$4) RETURNING *`, [req.params.id, article_id || null, custom_text || null, when] ); res.json(rows[0]); } catch (err) { res.status(500).json({ error: err.message }); } }); // ── User routes (НЕ системные, для tool) ────────────────────────────────────── router.get('/', async (req, res) => { const userId = getUserId(req); if (!userId) return res.status(401).json({ error: 'x-user-id required' }); try { const channels = await channelsSvc.listChannels(userId); res.json(channels); } catch (err) { res.status(500).json({ error: err.message }); } }); router.get('/:id', async (req, res) => { const userId = getUserId(req); if (!userId) return res.status(401).json({ error: 'x-user-id required' }); try { const channel = await channelsSvc.getFullChannel(req.params.id, userId); if (!channel) return res.status(404).json({ error: 'Channel not found' }); res.json(channel); } catch (err) { res.status(500).json({ error: err.message }); } }); router.post('/', async (req, res) => { const userId = getUserId(req); if (!userId) return res.status(401).json({ error: 'x-user-id required' }); try { // Проверяем лимит каналов по тарифу const billing = require('../services/billing'); const bal = await billing.getBalance(userId); if (bal.channelsMax !== -1) { const { rows: [{ cnt }] } = await require('../config/db').query( 'SELECT count(*)::int as cnt FROM channels WHERE user_id=$1 AND is_active=true', [userId] ); if (cnt >= bal.channelsMax) { return res.status(402).json({ error: `Лимит каналов по тарифу ${bal.planName}: максимум ${bal.channelsMax}. Перейдите на следующий тариф.`, code: 'CHANNEL_LIMIT_REACHED', current: cnt, max: bal.channelsMax, plan: bal.plan, }); } } const channel = await channelsSvc.createChannel(userId, req.body); res.json(channel); } catch (err) { console.error('[Route] POST /channels', err); res.status(500).json({ error: err.message }); } }); router.patch('/:id', async (req, res) => { const userId = getUserId(req); if (!userId) return res.status(401).json({ error: 'x-user-id required' }); try { const channel = await channelsSvc.updateChannel(req.params.id, userId, req.body); res.json(channel); } catch (err) { res.status(500).json({ error: err.message }); } }); router.delete('/:id', async (req, res) => { const userId = getUserId(req); if (!userId) return res.status(401).json({ error: 'x-user-id required' }); try { await channelsSvc.deleteChannel(req.params.id, userId); res.json({ ok: true }); } catch (err) { res.status(500).json({ error: err.message }); } }); module.exports = router;