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;
}
}
}