nakama/backend/bots/mainBot.js

291 lines
9.7 KiB
JavaScript
Raw Normal View History

2025-12-04 17:44:05 +00:00
const axios = require('axios');
const config = require('../config');
const { log, logError } = require('../middleware/logger');
const User = require('../models/User');
2025-12-07 15:19:22 +00:00
const { normalizeUsername } = require('../services/moderationAdmin');
const OWNER_USERNAMES = new Set(config.moderationOwnerUsernames || []);
2025-12-04 17:44:05 +00:00
const BOT_TOKEN = config.telegramBotToken;
const TELEGRAM_API = BOT_TOKEN ? `https://api.telegram.org/bot${BOT_TOKEN}` : null;
let isPolling = false;
let offset = 0;
const sendMessage = async (chatId, text, options = {}) => {
if (!TELEGRAM_API) {
log('warn', 'TELEGRAM_BOT_TOKEN не установлен, отправка сообщения невозможна');
return null;
}
try {
const response = await axios.post(`${TELEGRAM_API}/sendMessage`, {
chat_id: chatId,
text,
parse_mode: 'HTML',
...options
});
return response.data;
} catch (error) {
logError('Ошибка отправки сообщения', error, { chatId, text: text.substring(0, 50) });
return null;
}
};
const sendMessageToAllUsers = async (messageText) => {
if (!TELEGRAM_API) {
throw new Error('TELEGRAM_BOT_TOKEN не установлен');
}
try {
const users = await User.find({ banned: { $ne: true } }).select('telegramId');
let sent = 0;
let failed = 0;
for (const user of users) {
try {
await sendMessage(user.telegramId, messageText);
sent++;
// Небольшая задержка, чтобы не превысить лимиты API
await new Promise(resolve => setTimeout(resolve, 50));
} catch (error) {
failed++;
logError('Ошибка отправки сообщения пользователю', error, { telegramId: user.telegramId });
}
}
return { sent, failed, total: users.length };
} catch (error) {
logError('Ошибка массовой отправки сообщений', error);
throw error;
}
};
2025-12-07 15:19:22 +00:00
const requireAdmin = async (message) => {
try {
const telegramId = message.from?.id;
if (!telegramId) return false;
const user = await User.findOne({ telegramId }).select('role');
const username = normalizeUsername(message.from?.username || '');
return user && (user.role === 'admin' || OWNER_USERNAMES.has(username));
} catch (error) {
logError('Ошибка проверки прав админа', error);
return false;
}
};
2025-12-04 17:44:05 +00:00
const getStartMessage = () => {
2025-12-07 02:36:30 +00:00
return `<b>Добро пожаловать в Nakama!</b>
2025-12-04 17:44:05 +00:00
2025-12-07 02:36:30 +00:00
📱 <b>Nakama</b> - социальная сеть для фурри и аниме сообщества.
2025-12-04 17:44:05 +00:00
<b>Основные возможности:</b>
Создание постов с текстом и изображениями
Поиск контента через e621 и Gelbooru
Комментарии и лайки
Подписки на пользователей
Система уведомлений
Фильтры и теги
<b>Как начать:</b>
1. Нажмите кнопку "Войти" ниже, чтобы запустить приложение
2. Создайте свой первый пост
3. Подписывайтесь на интересных пользователей
<b>Поддержка:</b>
Если возникли проблемы, напишите @NakamaReportbot
Приятного использования!`;
};
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') {
const startParam = message.text.split(' ')[1] || '';
// Если есть start_param (например, post_12345 или ref_ABC123)
// Это обрабатывается при открытии миниаппа, здесь просто показываем инструкцию
const startMessage = getStartMessage();
// Добавить кнопку для открытия миниаппа
let botUsername = 'NakamaSpaceBot';
if (config.telegramBotToken) {
try {
const botInfo = await axios.get(`${TELEGRAM_API}/getMe`);
botUsername = botInfo.data.result.username || 'NakamaSpaceBot';
} catch (error) {
log('warn', 'Не удалось получить имя бота', { error: error.message });
}
}
2025-12-04 18:02:36 +00:00
// Использовать web_app с правильным URL миниаппа
const miniappUrl = 'https://nakama.glpshchn.ru/';
2025-12-04 17:44:05 +00:00
await sendMessage(chatId, startMessage, {
reply_markup: {
inline_keyboard: [[
{
text: '🚀 Открыть Nakama',
web_app: {
2025-12-04 18:02:36 +00:00
url: miniappUrl
2025-12-04 17:44:05 +00:00
}
}
]]
}
});
return;
}
2025-12-07 15:19:22 +00:00
if (command === '/broadcast') {
// Проверка прав
const isAdmin = await requireAdmin(message);
if (!isAdmin) {
await sendMessage(chatId, '❌ Команда доступна только администраторам.');
return;
}
// Получить текст сообщения (все после /broadcast)
const messageText = args.slice(1).join(' ').trim();
if (!messageText) {
await sendMessage(chatId, 'Использование: /broadcast <сообщение>\n\nПример: /broadcast Всем привет!');
return;
}
// Отправить подтверждение
await sendMessage(chatId, '📤 Начинаю рассылку сообщения всем пользователям...');
try {
const result = await sendMessageToAllUsers(messageText);
await sendMessage(chatId,
`✅ Рассылка завершена!\n\n` +
`📊 Статистика:\n` +
`• Отправлено: ${result.sent}\n` +
`• Ошибок: ${result.failed}\n` +
`Всего пользователей: ${result.total}`
);
} catch (error) {
logError('Ошибка рассылки через команду', error);
await sendMessage(chatId, `❌ Ошибка рассылки: ${error.message}`);
}
return;
}
2025-12-04 17:44:05 +00:00
// Игнорируем неизвестные команды
};
const processUpdate = async (update) => {
const message = update.message || update.edited_message;
if (!message || !message.text) {
return;
}
try {
await handleCommand(message);
} catch (error) {
logError('Ошибка обработки команды основного бота', error, {
chatId: message.chat.id,
text: message.text?.substring(0, 50)
});
}
};
const pollUpdates = async () => {
if (!TELEGRAM_API) {
log('warn', 'Основной бот не запущен: TELEGRAM_BOT_TOKEN не установлен');
return;
}
if (isPolling) {
return;
}
isPolling = true;
log('info', 'Основной бот запущен, опрос обновлений...');
2025-12-04 18:02:36 +00:00
// При первом запуске получить все обновления и установить offset на последний,
// чтобы не отвечать на старые команды
const initializeOffset = async () => {
try {
const response = await axios.get(`${TELEGRAM_API}/getUpdates`, {
params: {
timeout: 1,
allowed_updates: ['message']
}
});
const updates = response.data.result || [];
if (updates.length > 0) {
// Установить offset на последний update_id + 1, чтобы пропустить все старые обновления
offset = updates[updates.length - 1].update_id + 1;
log('info', `Пропущено ${updates.length} старых обновлений, offset установлен на ${offset}`);
}
} catch (error) {
log('warn', 'Не удалось инициализировать offset, начнем с 0', { error: error.message });
}
};
2025-12-04 17:44:05 +00:00
const poll = async () => {
try {
const response = await axios.get(`${TELEGRAM_API}/getUpdates`, {
params: {
offset,
timeout: 30,
allowed_updates: ['message']
}
});
const updates = response.data.result || [];
for (const update of updates) {
offset = update.update_id + 1;
await processUpdate(update);
}
// Продолжить опрос
setTimeout(poll, 1000);
} catch (error) {
2025-12-04 21:23:24 +00:00
const errorData = error.response?.data || {};
const errorCode = errorData.error_code;
const errorDescription = errorData.description || error.message;
// Обработка конфликта 409 - другой экземпляр бота уже опрашивает
if (errorCode === 409) {
// Не логируем 409 - это ожидаемая ситуация при конфликте экземпляров
// Подождать дольше перед повторной попыткой
setTimeout(poll, 10000);
} else {
logError('Ошибка опроса Telegram для основного бота', error);
// Переподключиться через 5 секунд
setTimeout(poll, 5000);
}
2025-12-04 17:44:05 +00:00
}
};
2025-12-04 18:02:36 +00:00
// Сначала инициализировать offset, затем начать опрос
initializeOffset().then(() => {
poll();
});
2025-12-04 17:44:05 +00:00
};
const startMainBot = () => {
if (!BOT_TOKEN) {
log('warn', 'Основной бот не запущен: TELEGRAM_BOT_TOKEN не установлен');
return;
}
pollUpdates();
};
module.exports = {
startMainBot,
sendMessageToAllUsers,
sendMessage
};