2025-11-10 20:13:22 +00:00
|
|
|
|
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 = [
|
|
|
|
|
|
`<b>Статистика сервера</b> ${status.icon}`,
|
|
|
|
|
|
`⏰ <b>Время:</b> ${formatter.format(now)}`,
|
|
|
|
|
|
`🆙 <b>Аптайм системы:</b> ${uptime}`,
|
|
|
|
|
|
`🔁 <b>Аптайм процесса:</b> ${processUptime}`,
|
|
|
|
|
|
'',
|
|
|
|
|
|
`🧠 <b>Загрузка CPU:</b> ${(load[0] || 0).toFixed(2)} / ${(load[1] || 0).toFixed(2)} / ${(load[2] || 0).toFixed(2)}`,
|
|
|
|
|
|
`⚙️ <b>На ядро:</b> ${(loadPerCore * 100).toFixed(0)}% (ядер: ${cpuCount})`,
|
|
|
|
|
|
'',
|
|
|
|
|
|
`💾 <b>Память:</b> ${formatBytes(usedMem)} / ${formatBytes(totalMem)} (${memUsage.toFixed(1)}%)`,
|
|
|
|
|
|
`🟢 <b>Свободно:</b> ${formatBytes(freeMem)}`,
|
|
|
|
|
|
''
|
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
|
|
if (diskUsage) {
|
|
|
|
|
|
lines.push(
|
|
|
|
|
|
`💽 <b>Диск:</b> ${diskUsage.used} / ${diskUsage.size} (${diskUsage.percent}% занято)`,
|
|
|
|
|
|
`📂 <b>Свободно:</b> ${diskUsage.available}`
|
|
|
|
|
|
);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
lines.push('💽 <b>Диск:</b> не удалось получить информацию');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
lines.push('', `🏷️ <b>Платформа:</b> ${os.type()} ${os.release()}`, `🔧 <b>Node.js:</b> ${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, `<b>Модераторы MiniApp</b>\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') {
|
2025-11-11 00:40:29 +00:00
|
|
|
|
await sendMessage(chatId, '✅ Бот активен');
|
2025-11-10 20:13:22 +00:00
|
|
|
|
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;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-11 00:40:29 +00:00
|
|
|
|
// Игнорируем неизвестные команды
|
2025-11-10 20:13:22 +00:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
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();
|
2025-11-10 20:28:24 +00:00
|
|
|
|
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 } : {})
|
|
|
|
|
|
};
|
|
|
|
|
|
});
|
2025-11-10 20:13:22 +00:00
|
|
|
|
|
|
|
|
|
|
form.append('chat_id', chatId);
|
|
|
|
|
|
form.append('media', JSON.stringify(media));
|
|
|
|
|
|
|
|
|
|
|
|
files.forEach((file, index) => {
|
|
|
|
|
|
form.append(`file${index}`, fs.createReadStream(file.path), {
|
2025-11-10 20:28:24 +00:00
|
|
|
|
filename: file.originalname || file.filename || `media${index}`
|
2025-11-10 20:13:22 +00:00
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
2025-11-20 21:32:48 +00:00
|
|
|
|
const response = await axios.post(`${TELEGRAM_API}/sendMediaGroup`, form, {
|
2025-11-10 20:13:22 +00:00
|
|
|
|
headers: form.getHeaders()
|
|
|
|
|
|
});
|
2025-11-20 21:32:48 +00:00
|
|
|
|
|
|
|
|
|
|
// Вернуть ID первого сообщения из группы
|
|
|
|
|
|
const messageId = response.data?.result?.[0]?.message_id;
|
|
|
|
|
|
return messageId;
|
2025-11-10 20:13:22 +00:00
|
|
|
|
} catch (error) {
|
|
|
|
|
|
log('error', 'Не удалось отправить медиа-группу в канал', { error: error.response?.data || error.message });
|
|
|
|
|
|
throw error;
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
files.forEach((file) => {
|
|
|
|
|
|
fs.unlink(file.path, () => {});
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2025-11-11 00:33:22 +00:00
|
|
|
|
/**
|
|
|
|
|
|
* Отправить сообщение пользователю
|
|
|
|
|
|
*/
|
|
|
|
|
|
const sendMessageToUser = async (userId, message) => {
|
2025-11-11 00:46:01 +00:00
|
|
|
|
if (!TELEGRAM_API) {
|
2025-11-11 00:33:22 +00:00
|
|
|
|
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;
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2025-11-20 21:32:48 +00:00
|
|
|
|
/**
|
|
|
|
|
|
* Обновить сообщение в канале (только текст)
|
|
|
|
|
|
*/
|
|
|
|
|
|
const updateChannelMessage = async (messageId, content, hashtags) => {
|
|
|
|
|
|
if (!TELEGRAM_API) {
|
|
|
|
|
|
throw new Error('Бот модерации не инициализирован');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const chatId = config.moderationChannelUsername || '@reichenbfurry';
|
|
|
|
|
|
|
|
|
|
|
|
// Формируем текст с хэштегами
|
|
|
|
|
|
const text = [content, hashtags?.length ? hashtags.map(tag => `#${tag}`).join(' ') : ''].filter(Boolean).join('\n\n');
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
await axios.post(`${TELEGRAM_API}/editMessageCaption`, {
|
|
|
|
|
|
chat_id: chatId,
|
|
|
|
|
|
message_id: messageId,
|
|
|
|
|
|
caption: `${text}${ERROR_SUPPORT_SUFFIX}`,
|
|
|
|
|
|
parse_mode: 'HTML'
|
|
|
|
|
|
});
|
|
|
|
|
|
log('info', 'Сообщение в канале обновлено', { messageId });
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
log('error', 'Не удалось обновить сообщение в канале', {
|
|
|
|
|
|
messageId,
|
|
|
|
|
|
error: error.response?.data || error.message
|
|
|
|
|
|
});
|
|
|
|
|
|
throw error;
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2025-11-10 20:13:22 +00:00
|
|
|
|
module.exports = {
|
|
|
|
|
|
startServerMonitorBot,
|
|
|
|
|
|
sendChannelMediaGroup,
|
2025-11-11 00:33:22 +00:00
|
|
|
|
sendMessageToUser,
|
2025-11-20 21:32:48 +00:00
|
|
|
|
updateChannelMessage,
|
2025-11-10 20:13:22 +00:00
|
|
|
|
isModerationAdmin
|
|
|
|
|
|
};
|
|
|
|
|
|
|