forked from admin/zeropost-engine
feat: admin channels API — system channels, publish to TG/VK/Max
This commit is contained in:
+139
-1
@@ -1,6 +1,7 @@
|
|||||||
const express = require('express');
|
const express = require('express');
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
const channelsSvc = require('../services/channels');
|
const channelsSvc = require('../services/channels');
|
||||||
|
const { query } = require('../config/db');
|
||||||
|
|
||||||
const getUserId = (req) => {
|
const getUserId = (req) => {
|
||||||
const id = req.headers['x-user-id'];
|
const id = req.headers['x-user-id'];
|
||||||
@@ -8,7 +9,144 @@ const getUserId = (req) => {
|
|||||||
return parseInt(id);
|
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) => {
|
router.get('/', async (req, res) => {
|
||||||
const userId = getUserId(req);
|
const userId = getUserId(req);
|
||||||
if (!userId) return res.status(401).json({ error: 'x-user-id required' });
|
if (!userId) return res.status(401).json({ error: 'x-user-id required' });
|
||||||
|
|||||||
Reference in New Issue
Block a user