const axios = require('axios'); const os = require('os'); const { exec } = require('child_process'); const FormData = require('form-data'); const fs = require('fs'); const config = require('../config'); const { log } = require('../middleware/logger'); const { listAdmins, addAdmin, removeAdmin, isModerationAdmin, normalizeUsername } = require('../services/moderationAdmin'); const BOT_TOKEN = config.moderationBotToken; const TELEGRAM_API = BOT_TOKEN ? `https://api.telegram.org/bot${BOT_TOKEN}` : null; const ERROR_SUPPORT_SUFFIX = ' Сообщите об ошибке в https://t.me/NakamaReportbot'; const OWNER_USERNAMES = new Set(config.moderationOwnerUsernames || []); let isPolling = false; let offset = 0; const execAsync = (command) => new Promise((resolve, reject) => { exec(command, (error, stdout, stderr) => { if (error) { return reject(new Error(stderr || error.message)); } resolve(stdout); }); }); const formatBytes = (bytes) => { if (bytes === 0) return '0 B'; const k = 1024; const sizes = ['B', 'KB', 'MB', 'GB', 'TB']; const i = Math.floor(Math.log(bytes) / Math.log(k)); const value = bytes / Math.pow(k, i); return `${value.toFixed(value >= 10 ? 0 : 1)} ${sizes[i]}`; }; const formatDuration = (seconds) => { const days = Math.floor(seconds / 86400); const hours = Math.floor((seconds % 86400) / 3600); const minutes = Math.floor((seconds % 3600) / 60); const segments = []; if (days) segments.push(`${days}д`); if (hours) segments.push(`${hours}ч`); if (minutes || segments.length === 0) segments.push(`${minutes}м`); return segments.join(' '); }; const getDiskUsage = async () => { try { const output = await execAsync('df -h /'); const lines = output.trim().split('\n'); if (lines.length < 2) return null; const parts = lines[1].split(/\s+/); return { filesystem: parts[0], size: parts[1], used: parts[2], available: parts[3], percent: parseInt(parts[4], 10) }; } catch (error) { log('error', 'Не удалось получить информацию о диске', { error: error.message }); return null; } }; const buildStatus = ({ loadPerCore, memUsage, diskUsage }) => { const issues = []; let severity = 0; if (loadPerCore > 1.5) { issues.push('Высокая загрузка CPU (>150% на ядро)'); severity = Math.max(severity, 2); } else if (loadPerCore > 1.0) { issues.push('Нагрузка CPU растёт (>100% на ядро)'); severity = Math.max(severity, 1); } if (memUsage > 90) { issues.push('Критический уровень памяти (>90%)'); severity = Math.max(severity, 2); } else if (memUsage > 75) { issues.push('Память почти заполнена (>75%)'); severity = Math.max(severity, 1); } if (diskUsage && diskUsage.percent) { if (diskUsage.percent > 90) { issues.push('Заканчивается место на диске (>90%)'); severity = Math.max(severity, 2); } else if (diskUsage.percent > 80) { issues.push('Мало свободного места на диске (>80%)'); severity = Math.max(severity, 1); } } if (severity === 0) { return { icon: '✅', text: 'Нагрузка в норме' }; } if (severity === 1) { return { icon: '⚠️', text: `Есть предупреждения:\n${issues.map((i) => `• ${i}`).join('\n')}` }; } return { icon: '🔥', text: `Критические метрики:\n${issues.map((i) => `• ${i}`).join('\n')}` }; }; const buildStatsMessage = async () => { const load = os.loadavg(); const cpuCount = os.cpus().length || 1; const loadPerCore = load[0] / cpuCount; const totalMem = os.totalmem(); const freeMem = os.freemem(); const usedMem = totalMem - freeMem; const memUsage = (usedMem / totalMem) * 100; const diskUsage = await getDiskUsage(); const uptime = formatDuration(os.uptime()); const processUptime = formatDuration(process.uptime()); const status = buildStatus({ loadPerCore, memUsage, diskUsage }); const now = new Date(); const formatter = new Intl.DateTimeFormat('ru-RU', { dateStyle: 'long', timeStyle: 'medium' }); const lines = [ `Статистика сервера ${status.icon}`, `⏰ Время: ${formatter.format(now)}`, `🆙 Аптайм системы: ${uptime}`, `🔁 Аптайм процесса: ${processUptime}`, '', `🧠 Загрузка CPU: ${(load[0] || 0).toFixed(2)} / ${(load[1] || 0).toFixed(2)} / ${(load[2] || 0).toFixed(2)}`, `⚙️ На ядро: ${(loadPerCore * 100).toFixed(0)}% (ядер: ${cpuCount})`, '', `💾 Память: ${formatBytes(usedMem)} / ${formatBytes(totalMem)} (${memUsage.toFixed(1)}%)`, `🟢 Свободно: ${formatBytes(freeMem)}`, '' ]; if (diskUsage) { lines.push( `💽 Диск: ${diskUsage.used} / ${diskUsage.size} (${diskUsage.percent}% занято)`, `📂 Свободно: ${diskUsage.available}` ); } else { lines.push('💽 Диск: не удалось получить информацию'); } lines.push('', `🏷️ Платформа: ${os.type()} ${os.release()}`, `🔧 Node.js: ${process.version}`); if (status.text) { lines.push('', status.text); } return lines.join('\n'); }; const sendMessage = async (chatId, text) => { if (!TELEGRAM_API) return; try { await axios.post(`${TELEGRAM_API}/sendMessage`, { chat_id: chatId, text: `${text}${ERROR_SUPPORT_SUFFIX}`, parse_mode: 'HTML', disable_web_page_preview: true }); } catch (error) { log('error', 'Не удалось отправить сообщение модераторским ботом', { error: error.response?.data || error.message }); } }; const requireOwner = (message) => { const username = normalizeUsername(message.from?.username || ''); return OWNER_USERNAMES.has(username); }; const handleListAdmins = async (chatId) => { const admins = await listAdmins(); if (!admins.length) { await sendMessage(chatId, 'Список модераторов пуст.'); return; } const lines = admins.map((admin, index) => { const name = [admin.firstName, admin.lastName].filter(Boolean).join(' ') || '-'; return `${index + 1}. @${admin.username} (${name || 'нет имени'})`; }); await sendMessage(chatId, `Модераторы MiniApp\n${lines.join('\n')}`); }; const handleAddAdmin = async (chatId, message, args) => { if (!requireOwner(message)) { await sendMessage(chatId, 'У вас нет прав добавлять модераторов.'); return; } const username = normalizeUsername(args[1] || ''); if (!username) { await sendMessage(chatId, 'Использование: /addadmin @username'); return; } try { const admin = await addAdmin({ username, addedBy: message.from?.username }); await sendMessage( chatId, `✅ @${admin.username} добавлен в список модераторов MiniApp. Теперь этому пользователю доступен модераторский интерфейс.` ); } catch (error) { await sendMessage(chatId, `❌ ${error.message}`); } }; const handleRemoveAdmin = async (chatId, message, args) => { if (!requireOwner(message)) { await sendMessage(chatId, 'У вас нет прав удалять модераторов.'); return; } const username = normalizeUsername(args[1] || ''); if (!username) { await sendMessage(chatId, 'Использование: /removeadmin @username'); return; } try { await removeAdmin(username); await sendMessage(chatId, `✅ @${username} удалён из списка модераторов MiniApp.`); } catch (error) { await sendMessage(chatId, `❌ ${error.message}`); } }; const handleCommand = async (message) => { const chatId = message.chat.id; const text = (message.text || '').trim(); const args = text.split(/\s+/); const command = args[0].toLowerCase(); if (command === '/start') { await sendMessage( chatId, 'NakamaHost Moderation\nКоманды:\n• /load — состояние сервера\n• /admins — список админов\n• /addadmin @username — добавить админа (только владельцы)\n• /removeadmin @username — убрать админа (только владельцы)' ); return; } if (command === '/load') { if (!requireOwner(message)) { await sendMessage(chatId, 'Команда доступна только владельцу.'); return; } const reply = await buildStatsMessage(); await sendMessage(chatId, reply); return; } if (command === '/admins') { if (!requireOwner(message)) { await sendMessage(chatId, 'Команда доступна только владельцу.'); return; } await handleListAdmins(chatId); return; } if (command === '/addadmin') { if (!requireOwner(message)) { await sendMessage(chatId, 'Команда доступна только владельцу.'); return; } await handleAddAdmin(chatId, message, args); return; } if (command === '/removeadmin') { if (!requireOwner(message)) { await sendMessage(chatId, 'Команда доступна только владельцу.'); return; } await handleRemoveAdmin(chatId, message, args); return; } if (command.startsWith('/')) { await sendMessage(chatId, 'Неизвестная команда. Используйте /start, /load, /admins.'); } }; const processUpdate = async (update) => { const message = update.message || update.edited_message; if (!message || !message.text) { return; } try { await handleCommand(message); } catch (error) { log('error', 'Ошибка обработки команды модераторского бота', { error: error.message }); await sendMessage(message.chat.id, `Не удалось обработать команду: ${error.message}`); } }; const pollUpdates = async () => { if (!TELEGRAM_API) return; while (isPolling) { try { const response = await axios.get(`${TELEGRAM_API}/getUpdates`, { params: { timeout: 25, offset } }); const updates = response.data?.result || []; for (const update of updates) { offset = update.update_id + 1; await processUpdate(update); } } catch (error) { log('error', 'Ошибка опроса Telegram для модераторского бота', { error: error.response?.data || error.message }); await new Promise((resolve) => setTimeout(resolve, 5000)); } } }; const startServerMonitorBot = () => { if (!TELEGRAM_API) { log('warn', 'MODERATION_BOT_TOKEN не установлен, модераторский бот не запущен'); return; } if (isPolling) { return; } isPolling = true; log('info', 'Модераторский Telegram бот запущен'); pollUpdates().catch((error) => { log('error', 'Не удалось запустить модераторский бот', { error: error.message }); }); }; const sendChannelMediaGroup = async (files, caption) => { if (!TELEGRAM_API) throw new Error('Модераторский бот не настроен'); const chatId = config.moderationChannelUsername || '@reichenbfurry'; const form = new FormData(); const media = files.map((file, index) => { const isVideo = file.mimetype && file.mimetype.startsWith('video/'); return { type: isVideo ? 'video' : 'photo', media: `attach://file${index}`, ...(index === 0 ? { caption: `${caption}${ERROR_SUPPORT_SUFFIX}`, parse_mode: 'HTML' } : {}), ...(isVideo ? { supports_streaming: true } : {}) }; }); form.append('chat_id', chatId); form.append('media', JSON.stringify(media)); files.forEach((file, index) => { form.append(`file${index}`, fs.createReadStream(file.path), { filename: file.originalname || file.filename || `media${index}` }); }); try { await axios.post(`${TELEGRAM_API}/sendMediaGroup`, form, { headers: form.getHeaders() }); } catch (error) { log('error', 'Не удалось отправить медиа-группу в канал', { error: error.response?.data || error.message }); throw error; } finally { files.forEach((file) => { fs.unlink(file.path, () => {}); }); } }; /** * Отправить сообщение пользователю */ const sendMessageToUser = async (userId, message) => { if (!moderationBot) { throw new Error('Бот модерации не инициализирован'); } try { await axios.post(`${TELEGRAM_API}/sendMessage`, { chat_id: userId, text: message, parse_mode: 'HTML' }); log('info', 'Сообщение отправлено пользователю', { userId }); } catch (error) { log('error', 'Не удалось отправить сообщение пользователю', { userId, error: error.response?.data || error.message }); throw error; } }; module.exports = { startServerMonitorBot, sendChannelMediaGroup, sendMessageToUser, isModerationAdmin };