// Сбор статистики TG-каналов. // Сейчас: getChatMemberCount (подписчики). // Потом: TGStat API (views, ERR, прирост). // // Вызывается из cron'а раз в час: POST /api/channel-stats/collect const axios = require('axios'); const { query } = require('../config/db'); const settings = require('./settings'); /** * Собрать подписчиков для одного канала через Bot API. */ async function collectMembersForChannel(channel) { if (!channel.bot_token || !channel.tg_channel_id) return null; const base = await settings.get('TELEGRAM_API_BASE', 'https://api.telegram.org'); try { const res = await axios.post( `${base}/bot${channel.bot_token}/getChatMemberCount`, { chat_id: channel.tg_channel_id }, { timeout: 10000 } ); if (!res.data?.ok) return null; return res.data.result; // число } catch (err) { console.warn(`[stats] getChatMemberCount failed channel=${channel.id}: ${err.message}`); return null; } } /** * Собрать и сохранить статистику для всех активных системных TG-каналов. */ async function collectAll() { const { rows: channels } = await query( `SELECT id, name, platform, bot_token, tg_channel_id FROM channels WHERE is_system = true AND is_active = true AND platform = 'telegram'` ); const results = []; for (const ch of channels) { const members = await collectMembersForChannel(ch); if (members === null) { results.push({ channel_id: ch.id, name: ch.name, ok: false }); continue; } // Сохраняем только если значение изменилось или нет записи за последние 55 мин // (чтобы не дублировать при частых вызовах) const { rows: last } = await query( `SELECT members FROM channel_stats WHERE channel_id=$1 AND captured_at > NOW() - INTERVAL '55 minutes' ORDER BY captured_at DESC LIMIT 1`, [ch.id] ); if (last.length && last[0].members === members) { results.push({ channel_id: ch.id, name: ch.name, ok: true, members, saved: false, reason: 'no change' }); continue; } await query( `INSERT INTO channel_stats (channel_id, members) VALUES ($1, $2)`, [ch.id, members] ); console.log(`[stats] channel=${ch.name} members=${members}`); results.push({ channel_id: ch.id, name: ch.name, ok: true, members, saved: true }); } return results; } /** * Получить историю подписчиков за последние N дней. */ async function getMembersHistory(channelId, days = 30) { const { rows } = await query( `SELECT date_trunc('hour', captured_at) AS hour, MAX(members) AS members FROM channel_stats WHERE channel_id=$1 AND captured_at > NOW() - INTERVAL '${parseInt(days)} days' GROUP BY 1 ORDER BY 1 ASC`, [channelId] ); return rows; } /** * Получить текущую сводку по каналу. */ async function getChannelSummary(channelId) { // Последнее значение const { rows: latest } = await query( `SELECT members, captured_at FROM channel_stats WHERE channel_id=$1 ORDER BY captured_at DESC LIMIT 1`, [channelId] ); // 24 часа назад const { rows: yesterday } = await query( `SELECT members FROM channel_stats WHERE channel_id=$1 AND captured_at BETWEEN NOW() - INTERVAL '25 hours' AND NOW() - INTERVAL '23 hours' ORDER BY captured_at DESC LIMIT 1`, [channelId] ); // 7 дней назад const { rows: weekAgo } = await query( `SELECT members FROM channel_stats WHERE channel_id=$1 AND captured_at BETWEEN NOW() - INTERVAL '7 days 1 hour' AND NOW() - INTERVAL '6 days 23 hours' ORDER BY captured_at DESC LIMIT 1`, [channelId] ); // Кол-во постов const { rows: postsCount } = await query( `SELECT COUNT(*) AS cnt FROM posts WHERE channel_id=$1`, [channelId] ); const current = latest[0]?.members ?? null; const prev24h = yesterday[0]?.members ?? null; const prev7d = weekAgo[0]?.members ?? null; return { members: current, captured_at: latest[0]?.captured_at ?? null, delta_24h: current !== null && prev24h !== null ? current - prev24h : null, delta_7d: current !== null && prev7d !== null ? current - prev7d : null, posts_total: parseInt(postsCount[0]?.cnt ?? 0), }; } module.exports = { collectAll, collectMembersForChannel, getMembersHistory, getChannelSummary };