namespace Supercell.Laser.Bot.Bot { using System; using System.Collections.Generic; using System.Diagnostics; using System.Threading; using System.Threading.Tasks; using Telegram.Bot; using Telegram.Bot.Exceptions; using Telegram.Bot.Polling; using Telegram.Bot.Types; using Telegram.Bot.Types.Enums; using Telegram.Bot.Types.ReplyMarkups; using Supercell.Laser.Bot; using Supercell.Laser.Bot.Bot.Commands; using Supercell.Laser.Bot.DataBase; using Supercell.Laser.Bot.Settings; /// /// Сам телеграм-бот: команды, инлайн-кнопки и обработка апдейтов. /// public static class TelegramBot { private const string CbAdminPanel = "admin_panel"; private const string CbServerStats = "server_stats"; private const string CbStatsRefresh = "stats_refresh"; // internal — этими же значениями пользуются клавиатуры в Bot/Commands. internal const string CbResources = "res_menu"; internal const string CbBackToAdmin = "back_admin"; internal const string CbBackToMain = "back_main"; private static TelegramBotClient _client; private static CancellationTokenSource _cts; public static void Init(string token) { _client = new TelegramBotClient(token); _cts = new CancellationTokenSource(); var receiverOptions = new ReceiverOptions { AllowedUpdates = new[] { UpdateType.Message, UpdateType.CallbackQuery } }; // StartReceiving не блокирует поток — приём апдейтов идёт в фоне. _client.StartReceiving(HandleUpdateAsync, HandleErrorAsync, receiverOptions, _cts.Token); } public static void Stop() { _cts?.Cancel(); } private static async Task HandleUpdateAsync(ITelegramBotClient bot, Update update, CancellationToken ct) { try { if (update.Message is { Text: { } text } message) { string command = text.Split(' ')[0].Split('@')[0]; if (command == "/start") { // Начало заново — незаконченный ввод тэга/количества сбрасываем. ResourceCommand.Cancel(message.Chat.Id, message.From?.Id ?? 0); await SendMainMenu(bot, message.Chat.Id, message.From?.Id ?? 0, ct); } else if (!text.StartsWith("/") && IsAdmin(message.From?.Id ?? 0)) { // Обычный текст — это ответ на текущий шаг диалога о ресурсах, // если такой диалог у пользователя открыт. await ResourceCommand.HandleTextAsync(bot, message, ct); } } else if (update.CallbackQuery is { } query) { await HandleCallback(bot, query, ct); } } catch (Exception e) { Start.Log($"ошибка обработки апдейта: {e.Message}"); } } private static Task HandleErrorAsync(ITelegramBotClient bot, Exception exception, CancellationToken ct) { string message = exception switch { ApiRequestException apiEx => $"Telegram API error [{apiEx.ErrorCode}]: {apiEx.Message}", _ => exception.ToString() }; Start.Log(message); return Task.CompletedTask; } private static bool IsAdmin(long userId) => Configuration.Instance.IsAdmin(userId); // /start: "Выбери действие:" + кнопка "⚙ Панель Админа" — только если пишущий тг-админ. // Если админов, знающих про бота, нет вообще (или пишет не админ) — тот же текст, без кнопки. private static async Task SendMainMenu(ITelegramBotClient bot, long chatId, long userId, CancellationToken ct) { const string text = "Выбери действие:"; if (IsAdmin(userId)) await bot.SendTextMessageAsync(chatId, text, replyMarkup: MainMenuKeyboard(), cancellationToken: ct); else await bot.SendTextMessageAsync(chatId, text, cancellationToken: ct); } private static async Task HandleCallback(ITelegramBotClient bot, CallbackQuery query, CancellationToken ct) { if (query.Message == null) { await bot.AnswerCallbackQueryAsync(query.Id, cancellationToken: ct); return; } long userId = query.From.Id; long chatId = query.Message.Chat.Id; int messageId = query.Message.MessageId; switch (query.Data) { case CbAdminPanel: if (!IsAdmin(userId)) { await Deny(bot, query, ct); return; } BotData.LogAdminAction(userId, "open_admin_panel"); await bot.EditMessageTextAsync(chatId, messageId, "⚙ Панель администратора\nВыбери действие:", replyMarkup: AdminPanelKeyboard(), cancellationToken: ct); await bot.AnswerCallbackQueryAsync(query.Id, cancellationToken: ct); break; case CbServerStats: if (!IsAdmin(userId)) { await Deny(bot, query, ct); return; } BotData.LogAdminAction(userId, "view_server_stats"); await bot.EditMessageTextAsync(chatId, messageId, BuildStatsText(), replyMarkup: StatsKeyboard(), cancellationToken: ct); await bot.AnswerCallbackQueryAsync(query.Id, cancellationToken: ct); break; case CbStatsRefresh: if (!IsAdmin(userId)) { await Deny(bot, query, ct); return; } try { await bot.EditMessageTextAsync(chatId, messageId, BuildStatsText(), replyMarkup: StatsKeyboard(), cancellationToken: ct); } catch (ApiRequestException) { // "message is not modified" — за секунду статистика не изменилась. } await bot.AnswerCallbackQueryAsync(query.Id, "Обновлено", cancellationToken: ct); break; case CbResources: if (!IsAdmin(userId)) { await Deny(bot, query, ct); return; } ResourceCommand.Cancel(chatId, userId); await bot.EditMessageTextAsync(chatId, messageId, ResourceCommand.ResourcesMenuText, replyMarkup: ResourceCommand.ResourcesMenuKeyboard(), cancellationToken: ct); await bot.AnswerCallbackQueryAsync(query.Id, cancellationToken: ct); break; case CbBackToAdmin: if (!IsAdmin(userId)) { await Deny(bot, query, ct); return; } ResourceCommand.Cancel(chatId, userId); await bot.EditMessageTextAsync(chatId, messageId, "⚙ Панель администратора\nВыбери действие:", replyMarkup: AdminPanelKeyboard(), cancellationToken: ct); await bot.AnswerCallbackQueryAsync(query.Id, cancellationToken: ct); break; case CbBackToMain: ResourceCommand.Cancel(chatId, userId); if (IsAdmin(userId)) await bot.EditMessageTextAsync(chatId, messageId, "Выбери действие:", replyMarkup: MainMenuKeyboard(), cancellationToken: ct); else await bot.EditMessageTextAsync(chatId, messageId, "Выбери действие:", cancellationToken: ct); await bot.AnswerCallbackQueryAsync(query.Id, cancellationToken: ct); break; default: // Кнопки ресурсов (res_gems_*, res_gold_*) — у них callback_data // строится из ключа ресурса, поэтому в switch их не перечислить. if (!IsAdmin(userId)) { await Deny(bot, query, ct); return; } if (await ResourceCommand.HandleCallbackAsync(bot, query, ct)) return; await bot.AnswerCallbackQueryAsync(query.Id, cancellationToken: ct); break; } } private static async Task Deny(ITelegramBotClient bot, CallbackQuery query, CancellationToken ct) { await bot.AnswerCallbackQueryAsync(query.Id, "Нет доступа.", showAlert: true, cancellationToken: ct); } private static InlineKeyboardMarkup MainMenuKeyboard() { return new InlineKeyboardMarkup(new[] { new[] { InlineKeyboardButton.WithCallbackData("⚙ Панель Админа", CbAdminPanel) } }); } private static InlineKeyboardMarkup AdminPanelKeyboard() { return new InlineKeyboardMarkup(new List> { new() { InlineKeyboardButton.WithCallbackData("📝 Статистика сервера", CbServerStats) }, new() { InlineKeyboardButton.WithCallbackData("💰 Ресурсы", CbResources) }, new() { InlineKeyboardButton.WithCallbackData("⬅️ Назад", CbBackToMain) } }); } private static InlineKeyboardMarkup StatsKeyboard() { return new InlineKeyboardMarkup(new List> { new() { InlineKeyboardButton.WithCallbackData("🔄 Обновить", CbStatsRefresh) }, new() { InlineKeyboardButton.WithCallbackData("⬅️ Назад", CbBackToAdmin) } }); } private static string BuildStatsText() { long ramMb = Process.GetCurrentProcess().PrivateMemorySize64 / (1024 * 1024); int online = Start.GetOnlineCount(); TimeSpan uptime = DateTime.Now - Start.ProcessStartTime; return "📝 Статистика сервера\n\n" + $"Используется RAM: {ramMb} MB\n" + $"Онлайн: {online}\n" + $"Аптайм: {FormatUptime(uptime)}"; } // <1 минуты -> только секунды. >=1 часа -> часы, минуты, секунды (+ дни, если >=1 суток). // Иначе (от минуты до часа) -> минуты, секунды. Плюс верное русское склонение (1 час / 3 часа / 5 часов). private static string FormatUptime(TimeSpan ts) { long totalSeconds = (long)ts.TotalSeconds; if (totalSeconds < 0) totalSeconds = 0; long days = totalSeconds / 86400; long hours = (totalSeconds % 86400) / 3600; long minutes = (totalSeconds % 3600) / 60; long seconds = totalSeconds % 60; if (totalSeconds < 60) return $"{seconds} {Plural(seconds, "секунда", "секунды", "секунд")}"; var parts = new List(); if (days > 0) parts.Add($"{days} {Plural(days, "день", "дня", "дней")}"); if (days > 0 || hours > 0) parts.Add($"{hours} {Plural(hours, "час", "часа", "часов")}"); parts.Add($"{minutes} {Plural(minutes, "минута", "минуты", "минут")}"); parts.Add($"{seconds} {Plural(seconds, "секунда", "секунды", "секунд")}"); return string.Join(" ", parts); } // Русское склонение числительных: 1 час, 2-4 часа, 0/5-20 часов и т.д. private static string Plural(long n, string one, string few, string many) { long mod100 = n % 100; long mod10 = n % 10; if (mod100 >= 11 && mod100 <= 14) return many; if (mod10 == 1) return one; if (mod10 >= 2 && mod10 <= 4) return few; return many; } } }