namespace Supercell.Laser.Server.Message { using Newtonsoft.Json; using Newtonsoft.Json.Linq; using System.Net; using System.Text; using System.Text.Json; using Supercell.Laser.Logic.Home; using Supercell.Laser.Logic.Home.Items; using Supercell.Laser.Logic.Data; using Supercell.Laser.Logic.Message.Account; using Supercell.Laser.Logic.Message.Account.Auth; using Supercell.Laser.Logic.Message.Home; using Supercell.Laser.Logic.Util; using Supercell.Laser.Server.Database; using Supercell.Laser.Server.Database.Models; using Supercell.Laser.Server.Handler; using Supercell.Laser.Server.Networking.Session; using Supercell.Laser.Server.Settings; using Telegram.Bot; using Telegram.Bot.Exceptions; using Telegram.Bot.Polling; using Telegram.Bot.Types; using Telegram.Bot.Types.Enums; using Telegram.Bot.Types.InputFiles; using Telegram.Bot.Types.ReplyMarkups; public static class TelegramBot { private const string PublicWebBaseUrl = "https://shinyb.org"; private const int CreatorVipMonthElixirCost = 125; private static readonly object SyncRoot = new object(); private static readonly string JsonDirectory = Path.Combine(AppContext.BaseDirectory, "Json"); private static readonly string BindingsPath = Path.Combine(JsonDirectory, "telegram_bindings.json"); private static readonly string PendingPath = Path.Combine(JsonDirectory, "telegram_pending_links.json"); private static readonly string PaymentsPath = Path.Combine(JsonDirectory, "telegram_payments.json"); private static readonly string CreatorApplicationsPath = Path.Combine(JsonDirectory, "creator_applications.json"); private static readonly string CreatorProfilesPath = Path.Combine(JsonDirectory, "creator_profiles.json"); private static readonly string CreatorRewardRequestsPath = Path.Combine(JsonDirectory, "creator_reward_requests.json"); private static readonly string CreatorUpgradeRequestsPath = Path.Combine(JsonDirectory, "creator_upgrade_requests.json"); private const string SupportUsername = "@shiny_manager"; private static readonly string[] VoucherRewardOrder = { "gems", "gold", "starpoints" }; private static readonly HttpClient PaymentHttpClient = new HttpClient { Timeout = TimeSpan.FromSeconds(30) }; private static readonly ShopProduct[] ShopProducts = { new("gems_80", "gems", 80, 50m, "80 гемов"), new("gems_170", "gems", 170, 80m, "170 гемов"), new("gems_360", "gems", 360, 130m, "360 гемов"), new("gems_950", "gems", 950, 250m, "950 гемов"), new("gems_2000", "gems", 2000, 400m, "2000 гемов"), new("gems_4000", "gems", 4000, 750m, "4000 гемов"), new("gems_6000", "gems", 6000, 1000m, "6000 гемов"), new("vip_1m", "vip", 1, 150m, "VIP") }; private static readonly CreatorShopReward[] CreatorShopRewards = { new("gems_30", "30 гемов", 15, new[] { "gems=30" }, "💎", "gems", 1), new("gems_80", "80 гемов", 25, new[] { "gems=80" }, "💎", "gems", 2), new("gems_170", "170 гемов", 45, new[] { "gems=170" }, "💎", "gems", 3), new("gems_360", "360 гемов", 85, new[] { "gems=360" }, "💎", "gems", 4), new("gems_950", "950 гемов", 185, new[] { "gems=950" }, "💎", "gems", 5), new("gems_2000", "2000 гемов", 335, new[] { "gems=2000" }, "💎", "gems", 6), new("gold_300", "300 монет", 15, new[] { "gold=300" }, "🪙", "gold", 1), new("gold_880", "880 монет", 30, new[] { "gold=880" }, "🪙", "gold", 2), new("gold_2040", "2040 монет", 55, new[] { "gold=2040" }, "🪙", "gold", 3), new("gold_4680", "4680 монет", 105, new[] { "gold=4680" }, "🪙", "gold", 4), new("sp_30", "30 очков силы", 15, new[] { "starpoints=30" }, "⭐", "power", 1), new("sp_80", "80 очков силы", 25, new[] { "starpoints=80" }, "⭐", "power", 2), new("sp_170", "170 очков силы", 45, new[] { "starpoints=170" }, "⭐", "power", 3), new("sp_360", "360 очков силы", 85, new[] { "starpoints=360" }, "⭐", "power", 4), new("sp_950", "950 очков силы", 185, new[] { "starpoints=950" }, "⭐", "power", 5), new("sp_1410", "1410 очков силы", 245, new[] { "starpoints=1410" }, "⭐", "power", 6), new("brawler_rare", "Редкий боец", 20, new[] { "brawler_random=rare" }, "🥊", "brawler", 1), new("brawler_superrare", "Сверх-редкий боец", 35, new[] { "brawler_random=superrare" }, "🥊", "brawler", 2), new("brawler_epic", "Эпический боец", 60, new[] { "brawler_random=epic" }, "🥊", "brawler", 3), new("brawler_mythic", "Мифический боец", 110, new[] { "brawler_random=mythic" }, "🥊", "brawler", 4), new("brawler_legendary", "Легендарный боец", 220, new[] { "brawler_random=legendary" }, "🥊", "brawler", 5), new("skin_rare", "Редкий скин", 15, new[] { "skin_random=rare" }, "🎨", "skin", 1), new("skin_superrare", "Сверх-редкий скин", 30, new[] { "skin_random=superrare" }, "🎨", "skin", 2), new("skin_epic", "Эпический скин", 55, new[] { "skin_random=epic" }, "🎨", "skin", 3), new("skin_legendary", "Легендарный скин", 95, new[] { "skin_random=legendary" }, "🎨", "skin", 4), new("box_small_5", "5 маленьких ящиков", 20, new[] { "box_small=5" }, "📦", "box", 1), new("box_big_3", "3 больших ящика", 35, new[] { "box_big=3" }, "📦", "box", 2), new("box_mega_1", "1 мега ящик", 30, new[] { "box_mega=1" }, "📦", "box", 3) }; private static readonly Dictionary PaymentStates = new(StringComparer.OrdinalIgnoreCase); private static readonly Dictionary CreatorApplications = new(); private static readonly Dictionary CreatorProfiles = new(); private static readonly Dictionary CreatorRewardRequests = new(); private static readonly Dictionary CreatorUpgradeRequests = new(); private static long NextCreatorApplicationId = 1; private static long NextCreatorRewardRequestId = 1; private static long NextCreatorUpgradeRequestId = 1; private static CancellationTokenSource BotCancellation; private static TelegramBotClient BotClient; public static void Start() { EnsureStorage(); LoadPaymentStates(); LoadCreatorApplications(); LoadCreatorProfiles(); LoadCreatorRewardRequests(); LoadCreatorUpgradeRequests(); SyncCreatorCodeFile(); if (string.IsNullOrWhiteSpace(Configuration.Instance?.TelegramBotToken)) { Logger.Warning("Telegram bot token is not configured. Binding/login links will work only via stored tokens."); return; } BotCancellation = new CancellationTokenSource(); BotClient = new TelegramBotClient(Configuration.Instance.TelegramBotToken); ReceiverOptions options = new ReceiverOptions { AllowedUpdates = Array.Empty() }; BotClient.StartReceiving(HandleUpdateAsync, HandlePollingErrorAsync, options, BotCancellation.Token); _ = Task.Run(LogBotIdentityAsync); Logger.Info("Telegram bot with inline menus initialized."); } public static bool TryGetPendingInfo(string token, out string tag, out DateTime expiresAtUtc, out string mode) { tag = null; expiresAtUtc = default; mode = null; if (string.IsNullOrWhiteSpace(token)) { return false; } lock (SyncRoot) { List pendingLinks = LoadPendingLinks(); bool changed = CleanupPendingLinks(pendingLinks); if (changed) { SavePendingLinks(pendingLinks); } PendingLinkRecord record = pendingLinks .FirstOrDefault(link => string.Equals(link.Token, token, StringComparison.OrdinalIgnoreCase) && !link.Consumed); if (record == null) { return false; } tag = NormalizeAccountTag(record.AccountTag) ?? $"TG:{record.TelegramId}"; expiresAtUtc = record.ExpiresAtUtc; mode = record.Mode; return true; } } public static bool TryHandleBindingVoucher(HomeMode homeMode, string token) { if (homeMode?.Avatar == null || string.IsNullOrWhiteSpace(token)) { return false; } lock (SyncRoot) { List pendingLinks = LoadPendingLinks(); PendingLinkRecord record = pendingLinks .FirstOrDefault(link => string.Equals(link.Token, token, StringComparison.OrdinalIgnoreCase) && !link.Consumed); if (record == null) { return false; } if (record.ExpiresAtUtc <= DateTime.UtcNow) { record.Consumed = true; SavePendingLinks(pendingLinks); PushNotification(homeMode, "Срок действия ссылки истёк. Запросите новую ссылку в Telegram."); return true; } switch ((record.Mode ?? "voucher").Trim().ToLowerInvariant()) { case "register": HandleRegister(homeMode, record, pendingLinks); return true; case "login": HandleLogin(homeMode, record, pendingLinks); return true; default: return false; } } } public static string CreateRegisterLink(long telegramId, string telegramUsername, TimeSpan lifetime) { PendingLinkRecord record = CreatePendingLink("register", telegramId, telegramUsername, accountId: 0, accountTag: null, lifetime); return BuildLink(record); } public static bool TryCreateLoginLinkForAccount(long accountId, TimeSpan lifetime, out string url, out string error) { url = null; error = null; lock (SyncRoot) { TelegramBindingRecord binding = LoadBindings().FirstOrDefault(x => x.AccountId == accountId); if (binding == null) { error = "Для этого аккаунта нет Telegram-привязки."; return false; } PendingLinkRecord record = CreatePendingLink("login", binding.TelegramId, binding.TelegramUsername, binding.AccountId, binding.AccountTag, lifetime); url = BuildLink(record); return true; } } public static void BindTelegramToAccount(long accountId, long telegramId, string telegramUsername) { lock (SyncRoot) { Account account = Accounts.Load(accountId); if (account == null) { throw new InvalidOperationException($"Аккаунт {accountId} не найден."); } List bindings = LoadBindings(); bindings.RemoveAll(x => x.AccountId == accountId || x.TelegramId == telegramId); bindings.Add(BuildBindingRecord(account, telegramId, telegramUsername)); SaveBindings(bindings); } } public static bool TryUnbindTelegram(long telegramId, out TelegramBindingRecord binding) { lock (SyncRoot) { List bindings = LoadBindings(); binding = bindings.FirstOrDefault(x => x.TelegramId == telegramId); if (binding == null) { return false; } bindings.RemoveAll(x => x.TelegramId == telegramId); SaveBindings(bindings); List pendingLinks = LoadPendingLinks(); pendingLinks.RemoveAll(x => x.TelegramId == telegramId); SavePendingLinks(pendingLinks); binding = NormalizeBindingRecord(binding); return true; } } public static TelegramBindingRecord[] GetBindingsSnapshot() { lock (SyncRoot) { return LoadBindings() .Select(NormalizeBindingRecord) .OrderBy(x => x.TelegramId) .ToArray(); } } public static TelegramBindingRecord GetBindingByTelegramId(long telegramId) { lock (SyncRoot) { TelegramBindingRecord binding = LoadBindings().FirstOrDefault(x => x.TelegramId == telegramId); return binding != null ? NormalizeBindingRecord(binding) : null; } } public static bool TryCreateLoginLinkForTelegram(long telegramId, TimeSpan lifetime, out string url, out string error) { url = null; error = null; TelegramBindingRecord binding = GetBindingByTelegramId(telegramId); if (binding == null) { error = "Сначала привяжи аккаунт."; return false; } return TryCreateLoginLinkForAccount(binding.AccountId, lifetime, out url, out error); } private static void HandleRegister(HomeMode homeMode, PendingLinkRecord record, List pendingLinks) { Account account = Accounts.Load(homeMode.Avatar.AccountId); if (account == null) { PushNotification(homeMode, "Не удалось загрузить аккаунт для привязки."); return; } List bindings = LoadBindings(); bindings.RemoveAll(x => x.AccountId == account.AccountId || x.TelegramId == record.TelegramId); bindings.Add(BuildBindingRecord(account, record.TelegramId, record.TelegramUsername)); record.AccountId = account.AccountId; record.AccountTag = NormalizeAccountTag(record.AccountTag) ?? BuildAccountTag(account.AccountId); record.Consumed = true; SaveBindings(bindings); SavePendingLinks(pendingLinks); PushNotification(homeMode, $"Telegram успешно привязан к аккаунту {account.Avatar.Name} ({record.AccountTag})."); NotifyTelegramBindingSucceeded(record.TelegramId, account, record.AccountTag); Logger.Success($"Telegram {record.TelegramId} linked to {account.Avatar.Name}#{account.AccountId}"); } private static void NotifyTelegramBindingSucceeded(long telegramId, Account account, string accountTag) { if (BotClient == null || telegramId <= 0 || account == null) { return; } string tag = NormalizeAccountTag(accountTag) ?? BuildAccountTag(account.AccountId); _ = Task.Run(async () => { try { await BotClient.SendTextMessageAsync( telegramId, TrimTelegram( "✅ Аккаунт привязан\n\n" + $"👤 Ник: {Escape(account.Avatar.Name)}\n" + $"🏷 Тег: {Escape(tag)}\n\n" + "Теперь можно входить в игру и получать услуги через Telegram."), parseMode: ParseMode.Html, replyMarkup: BuildAccountMenuKeyboard(telegramId)); } catch { } }); } private static void HandleLogin(HomeMode homeMode, PendingLinkRecord record, List pendingLinks) { List bindings = LoadBindings(); TelegramBindingRecord binding = bindings.FirstOrDefault(x => x.TelegramId == record.TelegramId); if (binding == null) { record.Consumed = true; SavePendingLinks(pendingLinks); PushNotification(homeMode, "Для этого Telegram нет привязанного аккаунта."); return; } Account currentAccount = Accounts.Load(homeMode.Avatar.AccountId); if (currentAccount == null) { PushNotification(homeMode, "Не удалось подготовить вход в привязанный аккаунт."); return; } if (binding.AccountId == currentAccount.AccountId) { record.Consumed = true; binding.LastLoginAtUtc = DateTime.UtcNow; SaveBindings(bindings); SavePendingLinks(pendingLinks); PushNotification(homeMode, "Этот Telegram уже привязан к текущему аккаунту."); return; } currentAccount.Avatar.AccountIdRedirect = binding.AccountId; Accounts.Save(currentAccount); record.Consumed = true; binding.LastLoginAtUtc = DateTime.UtcNow; SaveBindings(bindings); SavePendingLinks(pendingLinks); if (Sessions.IsSessionActive(currentAccount.AccountId)) { Session session = Sessions.GetSession(currentAccount.AccountId); session?.GameListener?.SendTCPMessage(new AuthenticationFailedMessage { Message = $"Выполняется вход в привязанный аккаунт {binding.AccountTag}. Откройте игру снова." }); Sessions.Remove(currentAccount.AccountId); } Logger.Success($"Prepared linked-account login from {currentAccount.AccountId} to {binding.AccountId}"); } private static PendingLinkRecord CreatePendingLink(string mode, long telegramId, string telegramUsername, long accountId, string accountTag, TimeSpan lifetime) { EnsureStorage(); if (lifetime <= TimeSpan.Zero) { lifetime = TimeSpan.FromMinutes(15); } lock (SyncRoot) { List pendingLinks = LoadPendingLinks(); CleanupPendingLinks(pendingLinks); pendingLinks.RemoveAll(x => x.TelegramId == telegramId && string.Equals(x.Mode, mode, StringComparison.OrdinalIgnoreCase)); PendingLinkRecord record = new PendingLinkRecord { Token = Guid.NewGuid().ToString("N"), Mode = mode, TelegramId = telegramId, TelegramUsername = telegramUsername, AccountId = accountId, AccountTag = NormalizeAccountTag(accountTag), ExpiresAtUtc = DateTime.UtcNow.Add(lifetime), CreatedAtUtc = DateTime.UtcNow, Consumed = false }; pendingLinks.Add(record); SavePendingLinks(pendingLinks); return record; } } private static string BuildLink(PendingLinkRecord record) { return $"{PublicWebBaseUrl}/{record.Mode}/{record.Token}"; } private static string BuildVoucherActivationLink(string code) { return $"{PublicWebBaseUrl}/voucher.html?code={Uri.EscapeDataString(code ?? string.Empty)}"; } private static async Task LogBotIdentityAsync() { try { User me = await BotClient.GetMeAsync(); Logger.Success($"Telegram bot connected as @{me.Username}"); } catch (Exception ex) { Logger.Exception("Failed to fetch Telegram bot identity", ex); } } private static async Task HandleUpdateAsync(ITelegramBotClient botClient, Update update, CancellationToken cancellationToken) { try { if (update.Type == UpdateType.Message && update.Message?.Text != null) { await HandleMessageAsync(botClient, update.Message, cancellationToken); return; } if (update.Type == UpdateType.CallbackQuery && update.CallbackQuery != null) { await HandleCallbackAsync(botClient, update.CallbackQuery, cancellationToken); } } catch (Exception ex) { Logger.Exception("Telegram update handler failed", ex); } } private static Task HandlePollingErrorAsync(ITelegramBotClient botClient, Exception exception, CancellationToken cancellationToken) { string error = exception switch { ApiRequestException apiRequestException => $"Telegram API Error [{apiRequestException.ErrorCode}]: {apiRequestException.Message}", _ => exception.ToString() }; Logger.Error(error); return Task.CompletedTask; } private static async Task HandleMessageAsync(ITelegramBotClient botClient, Message message, CancellationToken cancellationToken) { string text = message.Text?.Trim() ?? string.Empty; if (string.IsNullOrWhiteSpace(text)) return; long userId = message.From?.Id ?? 0; // Обработка состояний админа if (IsAdmin(userId) && TryGetAdminState(userId, out string state)) { ClearAdminState(userId); await HandleAdminInputAsync(botClient, message, state, text, cancellationToken); return; } if (TryGetCreatorConversationState(userId, out CreatorConversationState creatorState)) { await HandleCreatorConversationInputAsync(botClient, message, creatorState, text, cancellationToken); return; } if (text.StartsWith("/start", StringComparison.OrdinalIgnoreCase)) { await botClient.SendTextMessageAsync( chatId: message.Chat.Id, text: BuildMainMenuText(message.From), parseMode: ParseMode.Html, replyMarkup: BuildMainMenuKeyboard(userId), disableWebPagePreview: true, cancellationToken: cancellationToken); return; } await botClient.SendTextMessageAsync( chatId: message.Chat.Id, text: "Нажми /start, чтобы открыть меню.", parseMode: ParseMode.Html, cancellationToken: cancellationToken); } private static async Task HandleAdminInputAsync(ITelegramBotClient botClient, Message message, string state, string input, CancellationToken cancellationToken) { string reply; bool keepPromptState = false; switch (state) { case "search": { string tag = input.Split(' ')[0]; if (!AdminConsoleTools.TryResolveAccount(tag, out var account, out _, out string resolvedTag)) { reply = $"❌ Игрок {Escape(tag)} не найден."; } else { bool online = Sessions.IsSessionActive(account.AccountId); reply = $"🔍 Игрок найден\n\n" + $"👤 Ник: {Escape(account.Avatar.Name)}\n" + $"🏷 Тег: {Escape(resolvedTag)}\n" + $"🏆 Кубки: {account.Avatar.Trophies}\n" + $"💎 Гемы: {account.Avatar.Diamonds}\n" + $"🪙 Монеты: {account.Avatar.Gold}\n" + $"🌐 Статус: {(online ? "🟢 онлайн" : "⚫ офлайн")}\n" + $"🔨 Бан: {(account.Avatar.Banned ? "да" : "нет")}"; } break; } // Формат: тег валюта количество → /gems #TAG amount case "give": { string[] parts = input.Split(' ', 3, StringSplitOptions.RemoveEmptyEntries); if (parts.Length < 3 || !int.TryParse(parts[2], out int amount) || amount <= 0) { keepPromptState = true; reply = "❌ Формат: тег валюта количество\nПример: #ABC123 gems 100\nВалюты: gems, gold, starpoints"; break; } string currency = parts[1].ToLower(); if (currency != "gems" && currency != "gold" && currency != "starpoints") { keepPromptState = true; reply = $"❌ Неизвестная валюта {Escape(parts[1])}.\nДоступно: gems, gold, starpoints"; break; } if (!AdminConsoleTools.TryResolveAccount(parts[0], out var account, out _, out string resolvedTag)) { keepPromptState = true; reply = $"❌ Игрок {Escape(parts[0])} не найден."; break; } AdminConsoleTools.GrantCurrency(account, currency, amount, "telegram-admin"); reply = $"✅ Выдано {amount} {Escape(parts[1])} игроку {Escape(account.Avatar.Name)} {Escape(resolvedTag)}."; break; } // Формат: тег [причина] → /ban TAG 1 1 99 case "ban": { string[] parts = input.Split(' ', 2, StringSplitOptions.RemoveEmptyEntries); if (!AdminConsoleTools.TryResolveAccount(parts[0], out var account, out _, out string resolvedTag)) { keepPromptState = true; reply = $"❌ Игрок {Escape(parts[0])} не найден."; break; } CmdHandler.Execute($"/ban {parts[0]} 1 1 99"); reply = $"🔨 Игрок {Escape(account.Avatar.Name)} {Escape(resolvedTag)} заблокирован."; break; } // Формат: тег → /unban TAG case "unban": { string tag = input.Trim().Split(' ')[0]; if (!AdminConsoleTools.TryResolveAccount(tag, out var account, out _, out string resolvedTag)) { keepPromptState = true; reply = $"❌ Игрок {Escape(tag)} не найден."; break; } CmdHandler.Execute($"/unban {tag}"); reply = $"✅ Игрок {Escape(account.Avatar.Name)} {Escape(resolvedTag)} разблокирован."; break; } // Формат: КОД gold=500 gems=100 → /vouchercreate КОД gold=500 gems=100 case "voucher": { string[] parts = input.Split(' ', StringSplitOptions.RemoveEmptyEntries); if (parts.Length == 0) { keepPromptState = true; reply = "❌ Формат: КОД gold=500 gems=100 или gold=500 gems=100."; break; } VoucherDraft draft = GetOrCreateVoucherDraft(message.From?.Id ?? 0); List rewardArgs; string code; if (parts[0].Contains('=')) { rewardArgs = parts.ToList(); code = EnsureVoucherCode(draft); } else { if (parts.Length < 2) { keepPromptState = true; reply = "❌ Укажи хотя бы одну награду: gold=500, gems=50, starpoints=250."; break; } code = parts[0]; draft.Code = code; rewardArgs = parts.Skip(1).ToList(); } AdminConsoleTools.CreateVoucher(code, rewardArgs); ReplaceVoucherRewards(draft, rewardArgs); string voucherLink = BuildVoucherActivationLink(code); reply = $"✅ Ваучер {Escape(code)} создан.\n" + $"🎁 Награды: {Escape(string.Join(" ", rewardArgs))}\n" + $"🔗 Ссылка: {Escape(voucherLink)}"; break; } case "creator_elixir_grant": { if (!long.TryParse(input.Trim(), out long delta)) { keepPromptState = true; reply = "❌ Введи целое число. Можно отрицательное."; break; } if (!TryGetAdminPendingCreator(message.From?.Id ?? 0, out CreatorProfile creator)) { reply = "❌ Креатор не найден."; break; } creator.Elixirs = Math.Max(0, creator.Elixirs + (int)delta); SaveCreatorProfiles(); ClearAdminPendingCreator(message.From?.Id ?? 0); reply = $"✅ Баланс {Escape(creator.CreatorCode)} обновлён. Теперь: {creator.Elixirs} эликсиров."; break; } case "creator_reward_approve": { if (!TryGetAdminPendingReward(message.From?.Id ?? 0, out CreatorRewardRequest request)) { reply = "❌ Заявка не найдена."; break; } if (!TryApplyCreatorRewardInput(request, input.Trim(), out string rewardResult)) { keepPromptState = true; reply = "❌ Формат неверный.\nИспользуй: elixir 100."; break; } request.Status = CreatorRecordStatus.Approved; request.Decision = rewardResult; request.ReviewedAtUtc = DateTime.UtcNow; request.ReviewedByTelegramId = message.From?.Id ?? 0; SaveCreatorRewardRequests(); ClearAdminPendingReward(message.From?.Id ?? 0); CreatorProfile creator = GetCreatorProfile(request.UserId); if (creator != null) { creator.RewardApprovals++; SaveCreatorProfiles(); } try { await botClient.SendTextMessageAsync(request.UserId, "✅ Заявка на эликсиры одобрена\n\n" + $"{Escape(rewardResult)}", parseMode: ParseMode.Html, replyMarkup: BuildCreatorMenuKeyboard(request.UserId), cancellationToken: cancellationToken); } catch { } reply = $"✅ Награда выдана: {Escape(rewardResult)}"; break; } default: reply = "❓ Неизвестное действие."; break; } if (keepPromptState) { RestoreAdminInputState(message.From?.Id ?? 0, state); } IReplyMarkup replyMarkup = state switch { "voucher" => BuildVoucherBuilderKeyboard(), "creator_elixir_grant" when TryGetAdminPendingCreator(message.From?.Id ?? 0, out CreatorProfile creatorProfile) => BuildAdminCreatorElixirPromptKeyboard(creatorProfile), "creator_reward_approve" when TryGetAdminPendingReward(message.From?.Id ?? 0, out CreatorRewardRequest rewardRequest) => BuildAdminCreatorRewardPromptKeyboard(rewardRequest.RequestId), "search" or "give" or "ban" or "unban" when keepPromptState => BuildAdminInputPromptKeyboard(state), _ => BuildAdminMenuKeyboard() }; await botClient.SendTextMessageAsync( chatId: message.Chat.Id, text: TrimTelegram(reply), parseMode: ParseMode.Html, replyMarkup: replyMarkup, cancellationToken: cancellationToken); } private static async Task HandleCallbackAsync(ITelegramBotClient botClient, CallbackQuery callback, CancellationToken cancellationToken) { if (callback.Message == null || callback.From == null) { return; } string data = callback.Data ?? string.Empty; long telegramId = callback.From.Id; switch (data) { case "menu:main": await EditMenuAsync(botClient, callback, BuildMainMenuText(callback.From), BuildMainMenuKeyboard(telegramId), cancellationToken); break; case "account:menu": await EditMenuAsync(botClient, callback, BuildAccountMenuText(callback.From), BuildAccountMenuKeyboard(callback.From.Id), cancellationToken); break; case "creator:menu": await EditMenuAsync(botClient, callback, BuildCreatorMenuText(callback.From.Id), BuildCreatorMenuKeyboard(callback.From.Id), cancellationToken); break; case "creator:apply": await StartCreatorApplicationAsync(botClient, callback, cancellationToken); break; case "creator:reward:new": await StartCreatorRewardRequestAsync(botClient, callback, cancellationToken); break; case "creator:upgrade:new": await StartCreatorUpgradeRequestAsync(botClient, callback, cancellationToken); break; case "creator:elixir:shop": await EditMenuAsync(botClient, callback, BuildCreatorElixirShopText(callback.From.Id), BuildCreatorElixirShopKeyboard(callback.From.Id), cancellationToken); break; case "creator:vip:buy": await HandleCreatorVipPurchaseAsync(botClient, callback, cancellationToken); return; case "creator:leave:confirm": await ShowCreatorLeaveConfirmAsync(botClient, callback, cancellationToken); break; case "creator:leave": await HandleCreatorLeaveAsync(botClient, callback, cancellationToken); return; case "creator:profile": await EditMenuAsync(botClient, callback, BuildCreatorProfileText(callback.From.Id), BuildCreatorMenuKeyboard(callback.From.Id), cancellationToken); break; case "info:menu": await EditMenuAsync(botClient, callback, BuildInfoMenuText(), BuildInfoMenuKeyboard(), cancellationToken); break; case "info:privacy": await EditMenuAsync(botClient, callback, BuildDocumentPlaceholderText("политика конфиденциальности"), BuildInfoMenuKeyboard(), cancellationToken); break; case "info:terms": await EditMenuAsync(botClient, callback, BuildDocumentPlaceholderText("пользовательское соглашение"), BuildInfoMenuKeyboard(), cancellationToken); break; case string s when s.StartsWith("creator:redeem:", StringComparison.Ordinal): await HandleCreatorElixirRedeemAsync(botClient, callback, s.Substring("creator:redeem:".Length), cancellationToken); return; case "admin:creators:menu": if (!IsAdmin(telegramId)) { await botClient.AnswerCallbackQueryAsync(callback.Id, "Нет доступа.", showAlert: true, cancellationToken: cancellationToken); return; } await EditMenuAsync(botClient, callback, BuildAdminCreatorsMenuText(), BuildAdminCreatorsMenuKeyboard(), cancellationToken); break; case "admin:creators:applications": await ShowAdminCreatorApplicationsAsync(botClient, callback, cancellationToken); break; case "admin:creators:rewards": await ShowAdminCreatorRewardRequestsAsync(botClient, callback, cancellationToken); break; case "admin:creators:upgrades": await ShowAdminCreatorUpgradeRequestsAsync(botClient, callback, cancellationToken); break; case "admin:creators:list": await ShowAdminCreatorsListAsync(botClient, callback, cancellationToken); break; case string s when s.StartsWith("admin:creator:approve:", StringComparison.Ordinal): await ApproveCreatorApplicationAsync(botClient, callback, s.Substring("admin:creator:approve:".Length), cancellationToken); return; case string s when s.StartsWith("admin:creator:reject:", StringComparison.Ordinal): await RejectCreatorApplicationAsync(botClient, callback, s.Substring("admin:creator:reject:".Length), cancellationToken); return; case string s when s.StartsWith("admin:creator:view:", StringComparison.Ordinal): await ShowAdminCreatorProfileAsync(botClient, callback, s.Substring("admin:creator:view:".Length), cancellationToken); return; case string s when s.StartsWith("admin:creator:level:ordinary:", StringComparison.Ordinal): await SetCreatorLevelAsync(botClient, callback, s.Substring("admin:creator:level:ordinary:".Length), CreatorTier.Ordinary, cancellationToken); return; case string s when s.StartsWith("admin:creator:level:super:", StringComparison.Ordinal): await SetCreatorLevelAsync(botClient, callback, s.Substring("admin:creator:level:super:".Length), CreatorTier.SuperCreator, cancellationToken); return; case string s when s.StartsWith("admin:creator:elixir:", StringComparison.Ordinal): await PromptCreatorElixirGrantAsync(botClient, callback, s.Substring("admin:creator:elixir:".Length), cancellationToken); return; case string s when s.StartsWith("admin:creator:kick:", StringComparison.Ordinal): await KickCreatorAsync(botClient, callback, s.Substring("admin:creator:kick:".Length), cancellationToken); return; case string s when s.StartsWith("admin:reward:approve:", StringComparison.Ordinal): await PromptRewardApprovalAsync(botClient, callback, s.Substring("admin:reward:approve:".Length), cancellationToken); return; case string s when s.StartsWith("admin:reward:view:", StringComparison.Ordinal): await ShowAdminCreatorRewardRequestAsync(botClient, callback, s.Substring("admin:reward:view:".Length), cancellationToken); return; case string s when s.StartsWith("admin:reward:grant:", StringComparison.Ordinal): await ApproveCreatorRewardRequestAsync(botClient, callback, s.Substring("admin:reward:grant:".Length), cancellationToken); return; case string s when s.StartsWith("admin:reward:reject:", StringComparison.Ordinal): await RejectCreatorRewardRequestAsync(botClient, callback, s.Substring("admin:reward:reject:".Length), cancellationToken); return; case string s when s.StartsWith("admin:upgrade:approve:", StringComparison.Ordinal): await ApproveCreatorUpgradeRequestAsync(botClient, callback, s.Substring("admin:upgrade:approve:".Length), cancellationToken); return; case string s when s.StartsWith("admin:upgrade:reject:", StringComparison.Ordinal): await RejectCreatorUpgradeRequestAsync(botClient, callback, s.Substring("admin:upgrade:reject:".Length), cancellationToken); return; case "bind:create": await ShowBindConfirmationAsync(botClient, callback, cancellationToken); break; case "bind:confirm": await ShowBindLinkAsync(botClient, callback, cancellationToken); break; case "bind:unlink:confirm": await ShowUnbindConfirmationAsync(botClient, callback, cancellationToken); break; case "bind:unlink:apply": await UnbindTelegramAccountAsync(botClient, callback, cancellationToken); break; case "login:create": await ShowLoginLinkAsync(botClient, callback, cancellationToken); break; case "binding:info": await ShowBindingInfoAsync(botClient, callback, cancellationToken); break; case "shop:menu": await ShowShopMenuAsync(botClient, callback, cancellationToken); break; case "shop:payments": await ShowMyPaymentsAsync(botClient, callback, cancellationToken); break; case string s when s.StartsWith("shop:buy:", StringComparison.Ordinal): await ShowPaymentMethodsAsync(botClient, callback, s.Substring("shop:buy:".Length), cancellationToken); break; case string s when s.StartsWith("shop:method:", StringComparison.Ordinal): { string[] parts = s.Split(':'); if (parts.Length >= 4) { await CreatePaymentAsync(botClient, callback, parts[2], parts[3], cancellationToken); break; } await botClient.AnswerCallbackQueryAsync(callback.Id, "Некорректные данные оплаты.", showAlert: true, cancellationToken: cancellationToken); return; } case string s when s.StartsWith("shop:check:", StringComparison.Ordinal): await HandleCheckPaymentAsync(botClient, callback, s.Substring("shop:check:".Length), cancellationToken); return; case "admin:menu": if (!IsAdmin(telegramId)) { await botClient.AnswerCallbackQueryAsync(callback.Id, "Нет доступа.", showAlert: true, cancellationToken: cancellationToken); return; } await EditMenuAsync(botClient, callback, BuildAdminMenuText(), BuildAdminMenuKeyboard(), cancellationToken); break; case "admin:stats": { var s = AdminConsoleTools.BuildServerStatsForBot(); await EditMenuAsync(botClient, callback, s, BuildAdminMenuKeyboard(), cancellationToken); break; } case "admin:top:players": await EditMenuAsync( botClient, callback, BuildTopPlayersText(), BuildAdminMenuKeyboard(), cancellationToken); break; case "admin:top:alliances": await EditMenuAsync( botClient, callback, BuildTopAlliancesText(), BuildAdminMenuKeyboard(), cancellationToken); break; case "admin:backup": { string backupPath = AdminConsoleTools.BackupDatabase(); await EditMenuAsync(botClient, callback, "💾 Бэкап создан\n\n" + $"{Escape(backupPath)}", BuildAdminMenuKeyboard(), cancellationToken); break; } case "admin:audit": { string exportPath = AdminConsoleTools.ExportAudit(50, null, suspiciousOnly: false); await SendAuditExportAsync(botClient, callback, exportPath, cancellationToken); break; } case "admin:bindings": await EditMenuAsync(botClient, callback, BuildBindingsText(), BuildAdminMenuKeyboard(), cancellationToken); break; case "admin:search:prompt": await EditMenuAsync(botClient, callback, "🔍 Поиск игрока\n\n" + "Отправь тег игрока в чат (например #ABC123) или числовой ID.\n\n" + "Следующее сообщение будет обработано как поиск.", new InlineKeyboardMarkup(new[] { new[] { InlineKeyboardButton.WithCallbackData("◀️ Назад", "admin:menu") } }), cancellationToken); SetAdminState(telegramId, "search"); break; case "admin:give:prompt": await EditMenuAsync(botClient, callback, "🎁 Выдать валюту\n\n" + "Формат: тег валюта количество\n\n" + "Пример: #ABC123 gems 100\n" + "Валюты: gems, gold, starpoints", new InlineKeyboardMarkup(new[] { new[] { InlineKeyboardButton.WithCallbackData("◀️ Назад", "admin:menu") } }), cancellationToken); SetAdminState(telegramId, "give"); break; case "admin:ban:prompt": await EditMenuAsync(botClient, callback, "🔨 Бан игрока\n\n" + "Формат: тег\n\n" + "Пример: #ABC123\n\n" + "Выдаётся перманентный бан.", new InlineKeyboardMarkup(new[] { new[] { InlineKeyboardButton.WithCallbackData("◀️ Назад", "admin:menu") } }), cancellationToken); SetAdminState(telegramId, "ban"); break; case "admin:unban:prompt": await EditMenuAsync(botClient, callback, "✅ Разбан игрока\n\n" + "Отправь тег или ID игрока:", new InlineKeyboardMarkup(new[] { new[] { InlineKeyboardButton.WithCallbackData("◀️ Назад", "admin:menu") } }), cancellationToken); SetAdminState(telegramId, "unban"); break; case "admin:voucher:prompt": EnsureVoucherDraft(telegramId, reset: false); await EditMenuAsync(botClient, callback, BuildVoucherBuilderText(telegramId), BuildVoucherBuilderKeyboard(), cancellationToken); ClearAdminState(telegramId); break; case "admin:voucher:uuid": EnsureVoucherDraft(telegramId, reset: false).Code = GenerateVoucherCode(); await EditMenuAsync(botClient, callback, BuildVoucherBuilderText(telegramId), BuildVoucherBuilderKeyboard(), cancellationToken); break; case "admin:voucher:reset": EnsureVoucherDraft(telegramId, reset: true); await EditMenuAsync(botClient, callback, BuildVoucherBuilderText(telegramId), BuildVoucherBuilderKeyboard(), cancellationToken); break; case "admin:voucher:add:gems:100": UpsertVoucherReward(telegramId, "gems", 100); await EditMenuAsync(botClient, callback, BuildVoucherBuilderText(telegramId), BuildVoucherBuilderKeyboard(), cancellationToken); break; case "admin:voucher:add:gold:1000": UpsertVoucherReward(telegramId, "gold", 1000); await EditMenuAsync(botClient, callback, BuildVoucherBuilderText(telegramId), BuildVoucherBuilderKeyboard(), cancellationToken); break; case "admin:voucher:add:gold:5000": UpsertVoucherReward(telegramId, "gold", 5000); await EditMenuAsync(botClient, callback, BuildVoucherBuilderText(telegramId), BuildVoucherBuilderKeyboard(), cancellationToken); break; case "admin:voucher:add:starpoints:1000": UpsertVoucherReward(telegramId, "starpoints", 1000); await EditMenuAsync(botClient, callback, BuildVoucherBuilderText(telegramId), BuildVoucherBuilderKeyboard(), cancellationToken); break; case "admin:voucher:manual": await EditMenuAsync(botClient, callback, "🎟 Ваучер: ручной ввод\n\n" + "Отправь в чат:\n" + "КОД gold=500 gems=100\n" + "или только награды:\n" + "gold=500 gems=100\n\n" + "Также доступны: brawler=ID, skin=ID, emote=ID.", new InlineKeyboardMarkup(new[] { new[] { InlineKeyboardButton.WithCallbackData("◀️ Назад", "admin:voucher:prompt") } }), cancellationToken); SetAdminState(telegramId, "voucher"); break; case "admin:voucher:create": { VoucherDraft draft = EnsureVoucherDraft(telegramId, reset: false); string code = EnsureVoucherCode(draft); List rewards = draft.Rewards.ToList(); if (rewards.Count == 0) { await botClient.AnswerCallbackQueryAsync(callback.Id, "Добавь хотя бы одну награду.", showAlert: true, cancellationToken: cancellationToken); return; } AdminConsoleTools.CreateVoucher(code, rewards); string voucherLink = BuildVoucherActivationLink(code); await EditMenuAsync(botClient, callback, "✅ Ваучер создан\n\n" + $"🏷 Код: {Escape(code)}\n" + $"🎁 Награды: {Escape(string.Join(" ", rewards))}\n" + $"🔗 Ссылка: {Escape(voucherLink)}", BuildVoucherBuilderKeyboard(), cancellationToken); break; } default: await botClient.AnswerCallbackQueryAsync(callback.Id, "Неизвестное действие.", cancellationToken: cancellationToken); return; } await botClient.AnswerCallbackQueryAsync(callback.Id, cancellationToken: cancellationToken); } private static async Task ShowBindLinkAsync(ITelegramBotClient botClient, CallbackQuery callback, CancellationToken cancellationToken) { string url = CreateRegisterLink(callback.From.Id, callback.From.Username, TimeSpan.FromMinutes(15)); await EditMenuAsync( botClient, callback, "🔗 Привязка аккаунта\n\n" + "Открой игру по кнопке ниже — Telegram привяжется автоматически.\n\n" + "Ссылка действует 15 минут.", new InlineKeyboardMarkup(new[] { new[] { InlineKeyboardButton.WithUrl("🎮 Открыть игру", url) }, new[] { InlineKeyboardButton.WithCallbackData("◀️ Назад", "menu:main") } }), cancellationToken); } private static async Task ShowBindConfirmationAsync(ITelegramBotClient botClient, CallbackQuery callback, CancellationToken cancellationToken) { TelegramBindingRecord binding = GetBindingByTelegramId(callback.From.Id); string warning = binding == null ? "После подтверждения открой игру по кнопке ниже и зайди на тот аккаунт, который хочешь привязать." : "Сейчас Telegram уже привязан к аккаунту ниже. Новая привязка заменит текущую."; string currentBinding = binding == null ? "🔓 Текущая привязка: отсутствует" : "🔓 Текущая привязка\n" + $"👤 Ник: {Escape(binding.AccountName)}\n" + $"🏷 Тег: {Escape(NormalizeAccountTag(binding.AccountTag))}"; await EditMenuAsync( botClient, callback, "🔗 Подтверждение привязки\n\n" + currentBinding + "\n\n" + warning + "\n\n" + "Продолжить?", new InlineKeyboardMarkup(new[] { new[] { InlineKeyboardButton.WithCallbackData("✅ Продолжить", "bind:confirm") }, new[] { InlineKeyboardButton.WithCallbackData("◀️ Назад", "account:menu") } }), cancellationToken); } private static async Task ShowUnbindConfirmationAsync(ITelegramBotClient botClient, CallbackQuery callback, CancellationToken cancellationToken) { TelegramBindingRecord binding = GetBindingByTelegramId(callback.From.Id); if (binding == null) { await botClient.AnswerCallbackQueryAsync(callback.Id, "Привязки нет.", showAlert: true, cancellationToken: cancellationToken); return; } await EditMenuAsync( botClient, callback, "🗑 Отвязка аккаунта\n\n" + $"👤 Ник: {Escape(binding.AccountName)}\n" + $"🏷 Тег: {Escape(NormalizeAccountTag(binding.AccountTag))}\n\n" + "После отвязки быстрый вход и покупки через Telegram для этого аккаунта перестанут работать, пока ты не привяжешь его снова.", new InlineKeyboardMarkup(new[] { new[] { InlineKeyboardButton.WithCallbackData("🗑 Да, отвязать", "bind:unlink:apply") }, new[] { InlineKeyboardButton.WithCallbackData("◀️ Назад", "account:menu") } }), cancellationToken); } private static async Task UnbindTelegramAccountAsync(ITelegramBotClient botClient, CallbackQuery callback, CancellationToken cancellationToken) { if (!TryUnbindTelegram(callback.From.Id, out TelegramBindingRecord binding)) { await botClient.AnswerCallbackQueryAsync(callback.Id, "Привязка уже отсутствует.", showAlert: true, cancellationToken: cancellationToken); return; } await EditMenuAsync( botClient, callback, "✅ Аккаунт отвязан\n\n" + $"Была удалена привязка: {Escape(NormalizeAccountTag(binding.AccountTag))}\n\n" + "При необходимости можешь сразу привязать другой аккаунт.", BuildAccountMenuKeyboard(callback.From.Id), cancellationToken); } private static async Task ShowLoginLinkAsync(ITelegramBotClient botClient, CallbackQuery callback, CancellationToken cancellationToken) { if (!TryCreateLoginLinkForTelegram(callback.From.Id, TimeSpan.FromMinutes(15), out string url, out string error)) { await EditMenuAsync( botClient, callback, "⚠️ Вход недоступен\n\n" + $"{Escape(error)}\n\n" + "Сначала привяжи аккаунт через кнопку ниже.", new InlineKeyboardMarkup(new[] { new[] { InlineKeyboardButton.WithCallbackData("🔗 Привязать аккаунт", "bind:create") }, new[] { InlineKeyboardButton.WithCallbackData("◀️ Назад", "menu:main") } }), cancellationToken); return; } await EditMenuAsync( botClient, callback, "🚪 Вход в аккаунт\n\n" + "Открой игру по кнопке ниже, чтобы войти в привязанный аккаунт.\n\n" + "Ссылка действует 15 минут.", new InlineKeyboardMarkup(new[] { new[] { InlineKeyboardButton.WithUrl("🎮 Войти в игру", url) }, new[] { InlineKeyboardButton.WithCallbackData("◀️ Назад", "menu:main") } }), cancellationToken); } private static async Task ShowBindingInfoAsync(ITelegramBotClient botClient, CallbackQuery callback, CancellationToken cancellationToken) { TelegramBindingRecord binding = GetBindingByTelegramId(callback.From.Id); string text; InlineKeyboardMarkup keyboard; if (binding == null) { text = "📋 Моя привязка\n\n" + "❌ Аккаунт не привязан.\n\n" + "Нажми «Привязать аккаунт», чтобы связать свой Telegram с игровым профилем."; keyboard = new InlineKeyboardMarkup(new[] { new[] { InlineKeyboardButton.WithCallbackData("🔗 Привязать аккаунт", "bind:create") }, new[] { InlineKeyboardButton.WithCallbackData("◀️ Назад", "account:menu") } }); } else { text = "📋 Моя привязка\n\n" + $"👤 Ник: {Escape(binding.AccountName)}\n" + $"🏷 Тег: {Escape(NormalizeAccountTag(binding.AccountTag))}\n\n" + "Аккаунт успешно привязан. Можно входить в игру одной кнопкой."; keyboard = new InlineKeyboardMarkup(new[] { new[] { InlineKeyboardButton.WithCallbackData("🚪 Войти в аккаунт", "login:create") }, new[] { InlineKeyboardButton.WithCallbackData("🗑 Отвязать", "bind:unlink:confirm") }, new[] { InlineKeyboardButton.WithCallbackData("◀️ Назад", "account:menu") } }); } await EditMenuAsync(botClient, callback, text, keyboard, cancellationToken); } private static async Task ShowShopMenuAsync(ITelegramBotClient botClient, CallbackQuery callback, CancellationToken cancellationToken) { TelegramBindingRecord binding = GetBindingByTelegramId(callback.From.Id); if (binding == null) { await EditMenuAsync( botClient, callback, "💳 Донат\n\nСначала привяжи аккаунт. Покупки зачисляются только на привязанный профиль.", new InlineKeyboardMarkup(new[] { new[] { InlineKeyboardButton.WithCallbackData("🔗 Привязать аккаунт", "bind:create") }, new[] { InlineKeyboardButton.WithCallbackData("◀️ Назад", "menu:main") } }), cancellationToken); return; } await EditMenuAsync( botClient, callback, BuildShopMenuText(binding), BuildShopMenuKeyboard(), cancellationToken); } private static async Task ShowMyPaymentsAsync(ITelegramBotClient botClient, CallbackQuery callback, CancellationToken cancellationToken) { PaymentState[] states; lock (SyncRoot) { states = PaymentStates.Values .Where(x => x.UserId == callback.From.Id) .OrderByDescending(x => x.CreatedAtUtc) .Take(10) .ToArray(); } string text = states.Length == 0 ? "🧾 Мои платежи\n\nИстория пока пустая." : "🧾 Мои платежи\n\n" + string.Join("\n\n", states.Select(BuildPaymentSummary)); await EditMenuAsync( botClient, callback, text, new InlineKeyboardMarkup(new[] { new[] { InlineKeyboardButton.WithCallbackData("◀️ Назад", "shop:menu") } }), cancellationToken); } private static async Task ShowPaymentMethodsAsync(ITelegramBotClient botClient, CallbackQuery callback, string productId, CancellationToken cancellationToken) { TelegramBindingRecord binding = GetBindingByTelegramId(callback.From.Id); if (binding == null) { await botClient.AnswerCallbackQueryAsync(callback.Id, "Сначала привяжи аккаунт.", showAlert: true, cancellationToken: cancellationToken); return; } ShopProduct product = GetShopProduct(productId); if (product == null) { await botClient.AnswerCallbackQueryAsync(callback.Id, "Товар не найден.", showAlert: true, cancellationToken: cancellationToken); return; } string tag = NormalizeAccountTag(binding.AccountTag) ?? BuildAccountTag(binding.AccountId); await EditMenuAsync( botClient, callback, "💳 Способ оплаты\n\n" + $"📦 Товар: {Escape(product.Title)}\n" + $"💰 Цена: {FormatShopPrice(product)}\n" + $"🏷 Аккаунт: {Escape(tag)}\n\n" + "Выбери способ оплаты.", new InlineKeyboardMarkup(new[] { new[] { InlineKeyboardButton.WithCallbackData("🏦 СБП", $"shop:method:{product.Id}:sbp") }, new[] { InlineKeyboardButton.WithCallbackData("💳 Банковская карта", $"shop:method:{product.Id}:card") }, new[] { InlineKeyboardButton.WithCallbackData("◀️ Назад", "shop:menu") } }), cancellationToken); } private static async Task EditMenuAsync( ITelegramBotClient botClient, CallbackQuery callback, string text, InlineKeyboardMarkup keyboard, CancellationToken cancellationToken) { try { await botClient.EditMessageTextAsync( chatId: callback.Message.Chat.Id, messageId: callback.Message.MessageId, text: TrimTelegram(text), parseMode: ParseMode.Html, replyMarkup: keyboard, disableWebPagePreview: true, cancellationToken: cancellationToken); } catch (ApiRequestException ex) when (!string.IsNullOrWhiteSpace(ex.Message) && ex.Message.IndexOf("message is not modified", StringComparison.OrdinalIgnoreCase) >= 0) { return; } } private static string BuildMainMenuText(User user) { string name = string.IsNullOrWhiteSpace(user?.FirstName) ? "игрок" : user.FirstName; TelegramBindingRecord binding = user != null ? GetBindingByTelegramId(user.Id) : null; CreatorProfile creator = user != null ? GetCreatorProfile(user.Id) : null; string bindingLine = binding != null ? $"🔐 Аккаунт: {Escape(binding.AccountName)}" : "🔐 Аккаунт: не привязан"; string creatorLine = creator != null && creator.IsActive ? $"🎬 Креатор: {Escape(creator.CreatorCode)} · {Escape(GetCreatorTierLabel(creator.Tier))}" : "🎬 Креатор: не выбран"; return $"Привет, {Escape(name)}.\n\n" + bindingLine + "\n" + creatorLine + "\n\n" + "Выбери нужный раздел."; } private static string BuildAccountMenuText(User user) { TelegramBindingRecord binding = user != null ? GetBindingByTelegramId(user.Id) : null; string bindingLine = binding != null ? $"👤 {Escape(binding.AccountName)}\n🏷 {Escape(NormalizeAccountTag(binding.AccountTag))}" : "Аккаунт пока не привязан."; return "🔐 Аккаунт\n\n" + bindingLine + "\n\n" + "Здесь можно привязать аккаунт и быстро входить в игру."; } private static string BuildCreatorMenuText(long telegramId) { CreatorProfile creator = GetCreatorProfile(telegramId); if (creator == null || !creator.IsActive) { bool hasPendingApplication = CreatorApplications.Values.Any(x => x.UserId == telegramId && string.Equals(x.Status, CreatorRecordStatus.Pending, StringComparison.OrdinalIgnoreCase)); return "🎬 Креаторская программа\n\n" + (hasPendingApplication ? "Твоя заявка уже отправлена и ждёт проверки." : "Подай заявку, чтобы вступить в программу креаторов. Нужен активный TikTok или YouTube.") + "\n\nПосле одобрения откроются кабинет креатора, эликсиры и заявки на повышение уровня."; } bool hasPendingRewardRequest = CreatorRewardRequests.Values.Any(x => x.UserId == telegramId && string.Equals(x.Status, CreatorRecordStatus.Pending, StringComparison.OrdinalIgnoreCase)); bool hasPendingUpgradeRequest = CreatorUpgradeRequests.Values.Any(x => x.UserId == telegramId && string.Equals(x.Status, CreatorRecordStatus.Pending, StringComparison.OrdinalIgnoreCase)); DateTime refreshAtUtc = GetNextCreatorShopRefreshUtc(DateTime.UtcNow); TimeSpan remaining = refreshAtUtc - DateTime.UtcNow; return "🎬 Кабинет креатора\n\n" + $"🔑 Код: {Escape(creator.CreatorCode)}\n" + $"⭐ Уровень: {Escape(GetCreatorTierLabel(creator.Tier))}\n" + $"🧪 Баланс: {creator.Elixirs} эликсиров\n" + $"📱 Платформа: {Escape(creator.PlatformName)}\n\n" + "📌 Статус\n" + $"• Видео на эликсиры: {(hasPendingRewardRequest ? "на проверке" : "можно отправить")}\n" + $"• Повышение: {(creator.Tier == CreatorTier.SuperCreator ? "максимальный уровень" : hasPendingUpgradeRequest ? "на проверке" : "можно подать")}\n" + $"• Магазин обновится через: {remaining.Hours}ч {remaining.Minutes}м\n\n" + "Выбери действие ниже."; } private static string BuildCreatorProfileText(long telegramId) { CreatorProfile creator = GetCreatorProfile(telegramId); if (creator == null || !creator.IsActive) { return "🎬 Креатор\n\nПрофиль не найден."; } return "🎬 Кабинет креатора\n\n" + $"🔑 Код: {Escape(creator.CreatorCode)}\n" + $"⭐ Уровень: {Escape(GetCreatorTierLabel(creator.Tier))}\n" + $"🧪 Эликсиры: {creator.Elixirs}\n" + $"📱 Платформа: {Escape(creator.PlatformName)}\n" + $"🔗 Ссылка: {Escape(creator.PlatformUrl)}\n" + $"🏷 Аккаунт: {Escape(creator.AccountTag)}\n" + $"📅 В программе с: {creator.JoinedAtUtc:dd.MM.yyyy}\n" + $"✅ Одобрений по видео: {creator.RewardApprovals}\n" + $"❌ Отклонений по видео: {creator.RewardRejections}"; } private static string BuildCreatorElixirShopText(long telegramId) { CreatorProfile creator = GetCreatorProfile(telegramId); if (creator == null || !creator.IsActive) { return "🧪 Эликсиры\n\nРаздел доступен только креаторам."; } CreatorShopReward[] rewards = GetCurrentCreatorShopRewards(); int affordableCount = rewards.Count(x => creator.Elixirs >= x.ElixirCost); DateTime refreshAtUtc = GetNextCreatorShopRefreshUtc(DateTime.UtcNow); TimeSpan remaining = refreshAtUtc - DateTime.UtcNow; return "🧪 Магазин креатора\n\n" + $"💠 Баланс: {creator.Elixirs} эликсиров\n" + $"🛍 В витрине: 9 наград\n" + $"✅ Доступно к покупке: {affordableCount}\n" + $"⏳ Обновление через: {remaining.Hours}ч {remaining.Minutes}м\n" + $"🕒 Следующая ротация: {refreshAtUtc:dd.MM HH:mm} UTC\n\n" + "Витрина обновляется каждые 10 часов.\n" + $"Постоянная услуга: 👑 VIP на 1 месяц за {CreatorVipMonthElixirCost} эликсиров.\n\n" + "Награды в этой ротации:\n" + string.Join("\n", rewards.Select(x => $"• {x.Icon} {Escape(x.Title)} — {x.ElixirCost} эликсира")); } private static string BuildInfoMenuText() { return "📄 Информация\n\n" + $"Поддержка: {Escape(SupportUsername)}\n" + "Ниже доступны основные документы и связь с поддержкой."; } private static string BuildDocumentPlaceholderText(string documentName) { return $"📄 {char.ToUpper(documentName[0]) + documentName.Substring(1)}\n\n" + "Сейчас документ временно недоступен. Напиши в поддержку, если он нужен прямо сейчас."; } private static string BuildShopMenuText(TelegramBindingRecord binding) { string tag = NormalizeAccountTag(binding?.AccountTag) ?? BuildAccountTag(binding?.AccountId ?? 0); int pendingCount = GetPendingPaymentsCount(binding?.TelegramId ?? 0); return "💳 Донат\n\n" + $"🏷 Аккаунт для зачисления: {Escape(tag)}\n\n" + "Доступные товары:\n" + string.Join("\n", ShopProducts.Select(product => $"• {Escape(product.Title)} — {FormatShopPrice(product)}")) + "\n\n" + $"🧾 Платежей в ожидании: {pendingCount}\n" + "🏦 Способы оплаты: СБП и банковская карта\n" + "\n\n👑 VIP включает:\n" + "• градиент ники\n" + "• ежедневный Big Box\n" + "• 170💎 бонусом"; } private static InlineKeyboardMarkup BuildShopMenuKeyboard() { return new InlineKeyboardMarkup(new[] { new[] { InlineKeyboardButton.WithCallbackData("80💎", "shop:buy:gems_80"), InlineKeyboardButton.WithCallbackData("170💎", "shop:buy:gems_170") }, new[] { InlineKeyboardButton.WithCallbackData("360💎", "shop:buy:gems_360"), InlineKeyboardButton.WithCallbackData("950💎", "shop:buy:gems_950") }, new[] { InlineKeyboardButton.WithCallbackData("2000💎", "shop:buy:gems_2000"), InlineKeyboardButton.WithCallbackData("4000💎", "shop:buy:gems_4000") }, new[] { InlineKeyboardButton.WithCallbackData("6000💎", "shop:buy:gems_6000"), InlineKeyboardButton.WithCallbackData("VIP👑", "shop:buy:vip_1m") }, new[] { InlineKeyboardButton.WithCallbackData("🧾 Мои платежи", "shop:payments") }, new[] { InlineKeyboardButton.WithCallbackData("◀️ Назад", "menu:main") } }); } private static InlineKeyboardMarkup BuildInfoMenuKeyboard() { return new InlineKeyboardMarkup(new[] { new[] { HasPrivacyPolicyUrl() ? InlineKeyboardButton.WithUrl("🔒 Политика", GetPrivacyPolicyUrl()) : InlineKeyboardButton.WithCallbackData("🔒 Политика", "info:privacy"), HasTermsOfServiceUrl() ? InlineKeyboardButton.WithUrl("📘 Соглашение", GetTermsOfServiceUrl()) : InlineKeyboardButton.WithCallbackData("📘 Соглашение", "info:terms") }, new[] { InlineKeyboardButton.WithUrl("💬 Поддержка", $"https://t.me/{SupportUsername.TrimStart('@')}") }, new[] { InlineKeyboardButton.WithCallbackData("◀️ Назад", "menu:main") } }); } private static InlineKeyboardMarkup BuildMainMenuKeyboard(long telegramId) { var rows = new List { new[] { InlineKeyboardButton.WithCallbackData("🔐 Аккаунт", "account:menu") }, new[] { InlineKeyboardButton.WithCallbackData("🎬 Креаторы", "creator:menu") }, new[] { InlineKeyboardButton.WithCallbackData("💳 Донат", "shop:menu") }, new[] { InlineKeyboardButton.WithCallbackData("📄 Информация", "info:menu") } }; if (IsAdmin(telegramId)) { rows.Add(new[] { InlineKeyboardButton.WithCallbackData("🛠 Панель администратора", "admin:menu") }); } return new InlineKeyboardMarkup(rows); } private static InlineKeyboardMarkup BuildAccountMenuKeyboard(long telegramId) { bool hasBinding = GetBindingByTelegramId(telegramId) != null; var rows = new List { new[] { InlineKeyboardButton.WithCallbackData(hasBinding ? "🔄 Перепривязать" : "🔗 Привязать аккаунт", "bind:create"), InlineKeyboardButton.WithCallbackData("📋 Моя привязка", "binding:info") }, new[] { InlineKeyboardButton.WithCallbackData("🚪 Войти", "login:create") } }; if (hasBinding) { rows.Add(new[] { InlineKeyboardButton.WithCallbackData("🗑 Отвязать аккаунт", "bind:unlink:confirm") }); } rows.Add(new[] { InlineKeyboardButton.WithCallbackData("◀️ Назад", "menu:main") }); return new InlineKeyboardMarkup(rows); } private static InlineKeyboardMarkup BuildCreatorMenuKeyboard(long telegramId) { CreatorProfile creator = GetCreatorProfile(telegramId); bool hasPendingApplication = CreatorApplications.Values.Any(x => x.UserId == telegramId && string.Equals(x.Status, CreatorRecordStatus.Pending, StringComparison.OrdinalIgnoreCase)); bool hasPendingRewardRequest = CreatorRewardRequests.Values.Any(x => x.UserId == telegramId && string.Equals(x.Status, CreatorRecordStatus.Pending, StringComparison.OrdinalIgnoreCase)); bool hasPendingUpgradeRequest = CreatorUpgradeRequests.Values.Any(x => x.UserId == telegramId && string.Equals(x.Status, CreatorRecordStatus.Pending, StringComparison.OrdinalIgnoreCase)); if (creator == null || !creator.IsActive) { return new InlineKeyboardMarkup(new[] { new[] { InlineKeyboardButton.WithCallbackData(hasPendingApplication ? "🕒 Заявка отправлена" : "📝 Подать заявку", hasPendingApplication ? "creator:menu" : "creator:apply") }, new[] { InlineKeyboardButton.WithCallbackData("🔄 Обновить", "creator:menu"), InlineKeyboardButton.WithCallbackData("◀️ Назад", "menu:main") } }); } return new InlineKeyboardMarkup(new[] { new[] { InlineKeyboardButton.WithCallbackData("📋 Профиль", "creator:profile"), InlineKeyboardButton.WithCallbackData("🧪 Магазин", "creator:elixir:shop") }, new[] { InlineKeyboardButton.WithCallbackData( hasPendingRewardRequest ? "🕒 Видео на проверке" : "📤 Подать видео", hasPendingRewardRequest ? "creator:menu" : "creator:reward:new"), InlineKeyboardButton.WithCallbackData( creator.Tier == CreatorTier.SuperCreator ? "⭐ Максимальный уровень" : hasPendingUpgradeRequest ? "🕒 Повышение на проверке" : "📈 Заявка на повышение", creator.Tier == CreatorTier.SuperCreator || hasPendingUpgradeRequest ? "creator:menu" : "creator:upgrade:new") }, new[] { InlineKeyboardButton.WithCallbackData("🚪 Уйти из программы", "creator:leave:confirm") }, new[] { InlineKeyboardButton.WithCallbackData("🔄 Обновить", "creator:menu"), InlineKeyboardButton.WithCallbackData("◀️ Назад", "menu:main") } }); } private static InlineKeyboardMarkup BuildCreatorConversationKeyboard(string mode) { string backTarget = mode switch { "creator_apply_code" => "creator:menu", "creator_apply_platform" => "creator:menu", "creator_apply_note" => "creator:menu", "creator_reward_url" => "creator:menu", "creator_reward_comment" => "creator:menu", "creator_upgrade_subscribers" => "creator:menu", "creator_upgrade_views" => "creator:menu", "creator_upgrade_best_views" => "creator:menu", "creator_upgrade_reason" => "creator:menu", "creator_upgrade_extra" => "creator:menu", _ => "creator:menu" }; return new InlineKeyboardMarkup(new[] { new[] { InlineKeyboardButton.WithCallbackData("◀️ Назад", backTarget) } }); } private static string BuildAdminMenuText() { int online = Sessions.Count; int total = Accounts.GetTotalCount(); int queue = Supercell.Laser.Server.Logic.Game.Matchmaking.GetTotalQueueCount(); int battles = Supercell.Laser.Server.Logic.Game.Battles.Count; return "🛠 Панель администратора\n\n" + $"🟢 Онлайн: {online} | 🎯 Бои: {battles}\n" + $"⏳ Очередь: {queue} | 👥 Аккаунтов: {total}\n\n" + "Выбери раздел:"; } private static InlineKeyboardMarkup BuildAdminMenuKeyboard() { return new InlineKeyboardMarkup(new[] { new[] { InlineKeyboardButton.WithCallbackData("📊 Статистика", "admin:stats"), InlineKeyboardButton.WithCallbackData("💾 Бэкап БД", "admin:backup") }, new[] { InlineKeyboardButton.WithCallbackData("🏆 Топ игроков", "admin:top:players"), InlineKeyboardButton.WithCallbackData("👥 Топ клубов", "admin:top:alliances") }, new[] { InlineKeyboardButton.WithCallbackData("📁 Аудит-логи", "admin:audit"), InlineKeyboardButton.WithCallbackData("🔗 Привязки", "admin:bindings") }, new[] { InlineKeyboardButton.WithCallbackData("🔍 Найти игрока", "admin:search:prompt"), InlineKeyboardButton.WithCallbackData("🎁 Выдать валюту", "admin:give:prompt") }, new[] { InlineKeyboardButton.WithCallbackData("🔨 Бан", "admin:ban:prompt"), InlineKeyboardButton.WithCallbackData("✅ Разбан", "admin:unban:prompt") }, new[] { InlineKeyboardButton.WithCallbackData("🎟 Ваучеры", "admin:voucher:prompt") }, new[] { InlineKeyboardButton.WithCallbackData("🎬 Креаторы", "admin:creators:menu") }, new[] { InlineKeyboardButton.WithCallbackData("◀️ Главное меню", "menu:main") } }); } private static bool IsAdmin(long telegramId) { return Configuration.Instance?.AdminTelegramIds?.Contains(telegramId) == true; } private static string GetPrivacyPolicyUrl() { return NormalizeConfiguredUrl(Configuration.Instance?.PrivacyPolicyUrl); } private static string GetTermsOfServiceUrl() { return NormalizeConfiguredUrl(Configuration.Instance?.TermsOfServiceUrl); } private static bool HasPrivacyPolicyUrl() { return GetPrivacyPolicyUrl() != null; } private static bool HasTermsOfServiceUrl() { return GetTermsOfServiceUrl() != null; } private static string NormalizeConfiguredUrl(string url) { if (string.IsNullOrWhiteSpace(url)) { return null; } string value = url.Trim(); return value.StartsWith("http://", StringComparison.OrdinalIgnoreCase) || value.StartsWith("https://", StringComparison.OrdinalIgnoreCase) ? value : null; } private static int GetPendingPaymentsCount(long telegramId) { lock (SyncRoot) { return PaymentStates.Values.Count(x => x.UserId == telegramId && !string.Equals(x.Status, "CONFIRMED", StringComparison.OrdinalIgnoreCase)); } } private static long GetCreatorShopRotationIndex(DateTime utcNow) { return (long)Math.Floor((utcNow - DateTime.UnixEpoch).TotalHours / 10d); } private static DateTime GetNextCreatorShopRefreshUtc(DateTime utcNow) { long nextIndex = GetCreatorShopRotationIndex(utcNow) + 1; return DateTime.UnixEpoch.AddHours(nextIndex * 10); } private static CreatorShopReward[] GetCurrentCreatorShopRewards() { long rotation = GetCreatorShopRotationIndex(DateTime.UtcNow); var random = new Random(unchecked((int)(rotation ^ 0x5A17BEEF))); var result = new List(9); var usedIds = new HashSet(StringComparer.OrdinalIgnoreCase); AddRandomReward(result, usedIds, random, "gems", 1, 3); AddRandomReward(result, usedIds, random, "gold", 1, 3); AddRandomReward(result, usedIds, random, "power", 1, 3); AddRandomReward(result, usedIds, random, "box", 1, 3); AddRandomReward(result, usedIds, random, "skin", 1, 2); AddRandomReward(result, usedIds, random, "brawler", 1, 2); AddRandomReward(result, usedIds, random, "gems", 4, 6); AddRandomReward(result, usedIds, random, "power", 4, 6); AddRandomReward(result, usedIds, random, "skin", 3, 4); while (result.Count < 9) { CreatorShopReward[] fallbackPool = CreatorShopRewards.Where(x => usedIds.Add(x.Id)).ToArray(); if (fallbackPool.Length == 0) { break; } result.Add(fallbackPool[random.Next(fallbackPool.Length)]); } return result .OrderBy(x => x.ElixirCost) .ThenBy(x => x.Category, StringComparer.OrdinalIgnoreCase) .ToArray(); } private static void AddRandomReward(List result, HashSet usedIds, Random random, string category, int minTier, int maxTier) { CreatorShopReward[] pool = CreatorShopRewards .Where(x => string.Equals(x.Category, category, StringComparison.OrdinalIgnoreCase)) .Where(x => x.Tier >= minTier && x.Tier <= maxTier) .Where(x => !usedIds.Contains(x.Id)) .ToArray(); if (pool.Length == 0) { return; } CreatorShopReward selected = pool[random.Next(pool.Length)]; usedIds.Add(selected.Id); result.Add(selected); } private static bool TryNormalizeCreatorPlatformUrl(string input, out string normalizedUrl, out string platformName) { normalizedUrl = null; platformName = null; if (string.IsNullOrWhiteSpace(input) || !Uri.TryCreate(input.Trim(), UriKind.Absolute, out Uri uri)) { return false; } string host = (uri.Host ?? string.Empty).ToLowerInvariant(); if (host.StartsWith("www.", StringComparison.Ordinal)) { host = host.Substring(4); } if (host == "youtube.com" || host == "m.youtube.com" || host == "youtu.be") { normalizedUrl = uri.ToString(); platformName = "YouTube"; return true; } if (host == "tiktok.com" || host.EndsWith(".tiktok.com", StringComparison.Ordinal)) { normalizedUrl = uri.ToString(); platformName = "TikTok"; return true; } return false; } private static string NormalizeCreatorRarityKey(string value) { string normalized = (value ?? string.Empty).Trim().ToLowerInvariant().Replace("-", string.Empty).Replace("_", string.Empty).Replace("ё", "е"); return normalized switch { "редкий" or "rare" => "rare", "сверхредкий" or "superrare" or "superraree" => "superrare", "эпический" or "epic" => "epic", "мифический" or "mythic" => "mythic", "легендарный" or "legendary" => "legendary", _ => normalized }; } private static int GetCreatorCodeGems(CreatorTier tier) { return tier == CreatorTier.SuperCreator ? 50 : 30; } private static void SyncCreatorCodeFile() { try { string filePath = LogicHelperConfig.creatorPath; Directory.CreateDirectory(Path.GetDirectoryName(filePath) ?? AppContext.BaseDirectory); List creators; lock (SyncRoot) { creators = CreatorProfiles.Values .Where(x => x.IsActive) .OrderBy(x => x.CreatorCode, StringComparer.OrdinalIgnoreCase) .Select(x => new { Tag = x.AccountTag, Code = x.CreatorCode, Gems = GetCreatorCodeGems(x.Tier) }) .Cast() .ToList(); } System.IO.File.WriteAllText(filePath, JsonConvert.SerializeObject(creators, Formatting.Indented)); } catch (Exception ex) { Logger.Exception("Failed to sync creators.json", ex); } } // Хранилище состояний ожидания ввода от админа private static readonly Dictionary AdminStates = new(); private static readonly Dictionary CreatorConversations = new(); private static void SetAdminState(long telegramId, string state) { lock (AdminStates) { AdminInputState entry = GetOrCreateAdminStateLocked(telegramId); entry.Mode = state; } } private static bool TryGetAdminState(long telegramId, out string state) { lock (AdminStates) { if (AdminStates.TryGetValue(telegramId, out AdminInputState entry) && !string.IsNullOrWhiteSpace(entry.Mode)) { state = entry.Mode; return true; } state = null; return false; } } private static void ClearAdminState(long telegramId) { lock (AdminStates) { if (AdminStates.TryGetValue(telegramId, out AdminInputState entry)) { entry.Mode = null; if (entry.Voucher == null && entry.PendingCreatorUserId <= 0 && entry.PendingRewardRequestId <= 0) { AdminStates.Remove(telegramId); } } } } private static void RestoreAdminInputState(long telegramId, string state) { lock (AdminStates) { AdminInputState entry = GetOrCreateAdminStateLocked(telegramId); entry.Mode = state; } } private static AdminInputState GetOrCreateAdminStateLocked(long telegramId) { if (!AdminStates.TryGetValue(telegramId, out AdminInputState entry)) { entry = new AdminInputState(); AdminStates[telegramId] = entry; } return entry; } private static VoucherDraft EnsureVoucherDraft(long telegramId, bool reset) { lock (AdminStates) { AdminInputState entry = GetOrCreateAdminStateLocked(telegramId); if (reset || entry.Voucher == null) { entry.Voucher = new VoucherDraft(); } return entry.Voucher; } } private static VoucherDraft GetOrCreateVoucherDraft(long telegramId) { return EnsureVoucherDraft(telegramId, reset: false); } private static void UpsertVoucherReward(long telegramId, string reward, int amount) { VoucherDraft draft = EnsureVoucherDraft(telegramId, reset: false); string token = $"{reward}={amount}"; int index = draft.Rewards.FindIndex(x => x.StartsWith(reward + "=", StringComparison.OrdinalIgnoreCase)); if (index >= 0) { draft.Rewards[index] = token; } else { draft.Rewards.Add(token); } } private static void ReplaceVoucherRewards(VoucherDraft draft, IReadOnlyList rewards) { draft.Rewards.Clear(); draft.Rewards.AddRange(rewards); } private static string EnsureVoucherCode(VoucherDraft draft) { if (string.IsNullOrWhiteSpace(draft.Code)) { draft.Code = GenerateVoucherCode(); } return draft.Code; } private static string GenerateVoucherCode() { return Guid.NewGuid().ToString("N")[..12].ToUpperInvariant(); } private static string BuildVoucherBuilderText(long telegramId) { VoucherDraft draft = EnsureVoucherDraft(telegramId, reset: false); string code = string.IsNullOrWhiteSpace(draft.Code) ? "не задан, сгенерируется при создании" : $"{Escape(draft.Code)}"; string rewards = draft.Rewards.Count == 0 ? "пока пусто" : $"{Escape(string.Join(" ", OrderVoucherRewards(draft.Rewards)))}"; return "🎟 Конструктор ваучера\n\n" + $"🏷 Код: {code}\n" + $"🎁 Награды: {rewards}\n\n" + "Можно быстро собрать валютный ваучер кнопками ниже или перейти в ручной ввод для brawler/skin/emote."; } private static string[] OrderVoucherRewards(IEnumerable rewards) { return rewards .OrderBy(token => { string key = token.Split('=', 2, StringSplitOptions.TrimEntries)[0].ToLowerInvariant(); int index = Array.IndexOf(VoucherRewardOrder, key); return index >= 0 ? index : int.MaxValue; }) .ThenBy(token => token, StringComparer.OrdinalIgnoreCase) .ToArray(); } private static InlineKeyboardMarkup BuildVoucherBuilderKeyboard() { return new InlineKeyboardMarkup(new[] { new[] { InlineKeyboardButton.WithCallbackData("🆔 UUID код", "admin:voucher:uuid"), InlineKeyboardButton.WithCallbackData("🧹 Очистить", "admin:voucher:reset") }, new[] { InlineKeyboardButton.WithCallbackData("💎 100 gems", "admin:voucher:add:gems:100"), InlineKeyboardButton.WithCallbackData("🪙 1000 gold", "admin:voucher:add:gold:1000") }, new[] { InlineKeyboardButton.WithCallbackData("🪙 5000 gold", "admin:voucher:add:gold:5000"), InlineKeyboardButton.WithCallbackData("⭐ 1000 SP", "admin:voucher:add:starpoints:1000") }, new[] { InlineKeyboardButton.WithCallbackData("⌨️ Ручной ввод", "admin:voucher:manual"), InlineKeyboardButton.WithCallbackData("✅ Создать", "admin:voucher:create") }, new[] { InlineKeyboardButton.WithCallbackData("◀️ Назад", "admin:menu") } }); } private static CreatorProfile GetCreatorProfile(long telegramId) { lock (SyncRoot) { CreatorProfiles.TryGetValue(telegramId, out CreatorProfile profile); return profile; } } private static string GetCreatorTierLabel(CreatorTier tier) { return tier == CreatorTier.SuperCreator ? "суперкреатор" : "обычный"; } private static bool TryGetCreatorConversationState(long telegramId, out CreatorConversationState state) { lock (CreatorConversations) { return CreatorConversations.TryGetValue(telegramId, out state); } } private static void SetCreatorConversationState(long telegramId, CreatorConversationState state) { lock (CreatorConversations) { CreatorConversations[telegramId] = state; } } private static void ClearCreatorConversationState(long telegramId) { lock (CreatorConversations) { CreatorConversations.Remove(telegramId); } } private static async Task StartCreatorApplicationAsync(ITelegramBotClient botClient, CallbackQuery callback, CancellationToken cancellationToken) { TelegramBindingRecord binding = GetBindingByTelegramId(callback.From.Id); if (binding == null) { await botClient.AnswerCallbackQueryAsync(callback.Id, "Сначала привяжи аккаунт.", showAlert: true, cancellationToken: cancellationToken); return; } if (GetCreatorProfile(callback.From.Id) is { IsActive: true }) { await botClient.AnswerCallbackQueryAsync(callback.Id, "Ты уже в программе.", showAlert: true, cancellationToken: cancellationToken); return; } lock (SyncRoot) { if (CreatorApplications.Values.Any(x => x.UserId == callback.From.Id && string.Equals(x.Status, CreatorRecordStatus.Pending, StringComparison.OrdinalIgnoreCase))) { ; } else { SetCreatorConversationState(callback.From.Id, new CreatorConversationState { Mode = "creator_apply_code", DraftApplication = new CreatorApplicationRecord { UserId = callback.From.Id, TelegramUsername = callback.From.Username, AccountId = binding.AccountId, AccountTag = NormalizeAccountTag(binding.AccountTag) ?? BuildAccountTag(binding.AccountId), AccountName = binding.AccountName } }); } } if (TryGetCreatorConversationState(callback.From.Id, out _)) { await EditMenuAsync( botClient, callback, "📝 Заявка на креатора\n\n" + "Отправь желаемый код креатора.\n\n" + "Дальше бот запросит ссылку на YouTube или TikTok.\n\n" + "Требования: 3-10 символов, латиница и цифры.", new InlineKeyboardMarkup(new[] { new[] { InlineKeyboardButton.WithCallbackData("◀️ Назад", "creator:menu") } }), cancellationToken); } else { await botClient.AnswerCallbackQueryAsync(callback.Id, "Заявка уже ожидает проверки.", showAlert: true, cancellationToken: cancellationToken); } } private static async Task StartCreatorRewardRequestAsync(ITelegramBotClient botClient, CallbackQuery callback, CancellationToken cancellationToken) { CreatorProfile creator = GetCreatorProfile(callback.From.Id); if (creator == null || !creator.IsActive) { await botClient.AnswerCallbackQueryAsync(callback.Id, "Раздел доступен только креаторам.", showAlert: true, cancellationToken: cancellationToken); return; } SetCreatorConversationState(callback.From.Id, new CreatorConversationState { Mode = "creator_reward_url", DraftReward = new CreatorRewardRequest { UserId = callback.From.Id, CreatorCode = creator.CreatorCode, AccountTag = creator.AccountTag } }); await EditMenuAsync( botClient, callback, "🧪 Заявка на эликсиры\n\n" + "Отправь ссылку на видео из YouTube или TikTok, за которое хочешь получить эликсиры.", new InlineKeyboardMarkup(new[] { new[] { InlineKeyboardButton.WithCallbackData("◀️ Назад", "creator:menu") } }), cancellationToken); } private static async Task StartCreatorUpgradeRequestAsync(ITelegramBotClient botClient, CallbackQuery callback, CancellationToken cancellationToken) { CreatorProfile creator = GetCreatorProfile(callback.From.Id); if (creator == null || !creator.IsActive) { await botClient.AnswerCallbackQueryAsync(callback.Id, "Раздел доступен только креаторам.", showAlert: true, cancellationToken: cancellationToken); return; } if (creator.Tier == CreatorTier.SuperCreator) { await botClient.AnswerCallbackQueryAsync(callback.Id, "У тебя уже максимальный уровень.", showAlert: true, cancellationToken: cancellationToken); return; } lock (SyncRoot) { if (CreatorUpgradeRequests.Values.Any(x => x.UserId == callback.From.Id && string.Equals(x.Status, CreatorRecordStatus.Pending, StringComparison.OrdinalIgnoreCase))) { ; } else { SetCreatorConversationState(callback.From.Id, new CreatorConversationState { Mode = "creator_upgrade_subscribers", DraftUpgrade = new CreatorUpgradeRequest { UserId = callback.From.Id, CreatorCode = creator.CreatorCode, AccountTag = creator.AccountTag, CurrentTier = creator.Tier } }); } } if (!TryGetCreatorConversationState(callback.From.Id, out _)) { await botClient.AnswerCallbackQueryAsync(callback.Id, "Заявка уже ожидает проверки.", showAlert: true, cancellationToken: cancellationToken); return; } await EditMenuAsync( botClient, callback, "📈 Заявка на повышение уровня\n\n" + "Напиши, сколько подписчиков сейчас у твоей платформы.", new InlineKeyboardMarkup(new[] { new[] { InlineKeyboardButton.WithCallbackData("◀️ Назад", "creator:menu") } }), cancellationToken); } private static async Task HandleCreatorConversationInputAsync(ITelegramBotClient botClient, Message message, CreatorConversationState state, string input, CancellationToken cancellationToken) { long userId = message.From?.Id ?? 0; string reply; switch (state.Mode) { case "creator_apply_code": { string code = input.Trim().ToUpperInvariant(); if (!IsCreatorCodeValid(code)) { reply = "❌ Код должен быть длиной 3-10 символов и содержать только латиницу и цифры."; break; } if (IsCreatorCodeTaken(code)) { reply = "❌ Такой код уже занят."; break; } state.DraftApplication.CreatorCode = code; state.Mode = "creator_apply_platform"; reply = "Теперь отправь ссылку на свой канал или аккаунт в YouTube или TikTok."; break; } case "creator_apply_platform": { if (!TryNormalizeCreatorPlatformUrl(input, out string platformUrl, out string platformName)) { reply = "❌ Для подачи нужен валидный профиль YouTube или TikTok."; break; } state.DraftApplication.PlatformUrl = platformUrl; state.DraftApplication.PlatformName = platformName; state.Mode = "creator_apply_note"; reply = "Хорошо. Теперь коротко опиши канал, контент и почему хочешь вступить в программу."; break; } case "creator_apply_note": { state.DraftApplication.Note = input.Trim(); lock (SyncRoot) { state.DraftApplication.ApplicationId = NextCreatorApplicationId++; state.DraftApplication.CreatedAtUtc = DateTime.UtcNow; state.DraftApplication.Status = CreatorRecordStatus.Pending; CreatorApplications[state.DraftApplication.ApplicationId] = state.DraftApplication; SaveCreatorApplications(); } await NotifyAdminsAboutCreatorApplicationAsync(botClient, state.DraftApplication, cancellationToken); ClearCreatorConversationState(userId); reply = "✅ Заявка отправлена на проверку администрации."; await botClient.SendTextMessageAsync(message.Chat.Id, TrimTelegram(reply), parseMode: ParseMode.Html, replyMarkup: BuildCreatorMenuKeyboard(userId), cancellationToken: cancellationToken); return; } case "creator_reward_url": { if (!TryNormalizeCreatorPlatformUrl(input, out string videoUrl, out string platformName)) { reply = "❌ Заявка принимается только по ссылке из YouTube или TikTok."; break; } state.DraftReward.VideoUrl = videoUrl; state.DraftReward.PlatformName = platformName; state.Mode = "creator_reward_comment"; reply = "Теперь коротко опиши материал и почему за него стоит начислить эликсиры."; break; } case "creator_reward_comment": { state.DraftReward.Comment = input.Trim(); lock (SyncRoot) { state.DraftReward.RequestId = NextCreatorRewardRequestId++; state.DraftReward.CreatedAtUtc = DateTime.UtcNow; state.DraftReward.Status = CreatorRecordStatus.Pending; CreatorRewardRequests[state.DraftReward.RequestId] = state.DraftReward; SaveCreatorRewardRequests(); } await NotifyAdminsAboutRewardRequestAsync(botClient, state.DraftReward, cancellationToken); ClearCreatorConversationState(userId); reply = "✅ Заявка на эликсиры отправлена. Ожидай решения администратора."; await botClient.SendTextMessageAsync(message.Chat.Id, TrimTelegram(reply), parseMode: ParseMode.Html, replyMarkup: BuildCreatorMenuKeyboard(userId), cancellationToken: cancellationToken); return; } case "creator_upgrade_subscribers": { if (!TryParseFlexiblePositiveInt(input, out int subscribers)) { reply = "❌ Введи количество подписчиков числом."; break; } state.DraftUpgrade.SubscribersCount = subscribers; state.Mode = "creator_upgrade_views"; reply = "Теперь напиши средние просмотры на последних роликах."; break; } case "creator_upgrade_views": { if (!TryParseFlexiblePositiveInt(input, out int views)) { reply = "❌ Введи количество просмотров числом."; break; } state.DraftUpgrade.AverageViews = views; state.Mode = "creator_upgrade_best_views"; reply = "Теперь напиши максимальные просмотры на одном из последних роликов."; break; } case "creator_upgrade_best_views": { if (!TryParseFlexiblePositiveInt(input, out int views)) { reply = "❌ Введи количество просмотров числом."; break; } state.DraftUpgrade.BestViews = views; state.Mode = "creator_upgrade_reason"; reply = "Зачем тебе повышение до суперкреатора?"; break; } case "creator_upgrade_reason": { state.DraftUpgrade.Reason = input.Trim(); state.Mode = "creator_upgrade_extra"; reply = "Последний вопрос: что ты планируешь делать после повышения и какие результаты можешь дать проекту?"; break; } case "creator_upgrade_extra": { state.DraftUpgrade.AdditionalInfo = input.Trim(); lock (SyncRoot) { state.DraftUpgrade.RequestId = NextCreatorUpgradeRequestId++; state.DraftUpgrade.CreatedAtUtc = DateTime.UtcNow; state.DraftUpgrade.Status = CreatorRecordStatus.Pending; CreatorUpgradeRequests[state.DraftUpgrade.RequestId] = state.DraftUpgrade; SaveCreatorUpgradeRequests(); } await NotifyAdminsAboutCreatorUpgradeRequestAsync(botClient, state.DraftUpgrade, cancellationToken); ClearCreatorConversationState(userId); reply = "✅ Заявка на повышение уровня отправлена. Ожидай решения администратора."; await botClient.SendTextMessageAsync(message.Chat.Id, TrimTelegram(reply), parseMode: ParseMode.Html, replyMarkup: BuildCreatorMenuKeyboard(userId), cancellationToken: cancellationToken); return; } default: ClearCreatorConversationState(userId); return; } await botClient.SendTextMessageAsync( message.Chat.Id, TrimTelegram(reply), parseMode: ParseMode.Html, replyMarkup: BuildCreatorConversationKeyboard(state.Mode), cancellationToken: cancellationToken); } private static bool IsCreatorCodeValid(string code) { if (string.IsNullOrWhiteSpace(code) || code.Length < 3 || code.Length > 10) { return false; } return code.All(ch => char.IsLetterOrDigit(ch) && ch < 128); } private static bool IsCreatorCodeTaken(string code) { lock (SyncRoot) { return CreatorProfiles.Values.Any(x => string.Equals(x.CreatorCode, code, StringComparison.OrdinalIgnoreCase) && x.IsActive) || CreatorApplications.Values.Any(x => string.Equals(x.CreatorCode, code, StringComparison.OrdinalIgnoreCase) && string.Equals(x.Status, CreatorRecordStatus.Pending, StringComparison.OrdinalIgnoreCase)); } } private static bool TryParseFlexiblePositiveInt(string input, out int value) { string normalized = new string((input ?? string.Empty).Where(ch => char.IsDigit(ch)).ToArray()); return int.TryParse(normalized, out value) && value > 0; } private static string BuildAdminCreatorsMenuText() { int activeCreators = CreatorProfiles.Values.Count(x => x.IsActive); int pendingApps = CreatorApplications.Values.Count(x => string.Equals(x.Status, CreatorRecordStatus.Pending, StringComparison.OrdinalIgnoreCase)); int pendingRewards = CreatorRewardRequests.Values.Count(x => string.Equals(x.Status, CreatorRecordStatus.Pending, StringComparison.OrdinalIgnoreCase)); int pendingUpgrades = CreatorUpgradeRequests.Values.Count(x => string.Equals(x.Status, CreatorRecordStatus.Pending, StringComparison.OrdinalIgnoreCase)); return "🎬 Креаторы\n\n" + $"• Активных: {activeCreators}\n" + $"• Заявок: {pendingApps}\n" + $"• Заявок на эликсиры: {pendingRewards}\n" + $"• Заявок на повышение: {pendingUpgrades}"; } private static InlineKeyboardMarkup BuildAdminCreatorsMenuKeyboard() { return new InlineKeyboardMarkup(new[] { new[] { InlineKeyboardButton.WithCallbackData("📝 Вступление", "admin:creators:applications"), InlineKeyboardButton.WithCallbackData("🧪 Эликсиры", "admin:creators:rewards") }, new[] { InlineKeyboardButton.WithCallbackData("📈 Повышения", "admin:creators:upgrades") }, new[] { InlineKeyboardButton.WithCallbackData("👥 Список креаторов", "admin:creators:list") }, new[] { InlineKeyboardButton.WithCallbackData("◀️ Назад", "admin:menu") } }); } private static InlineKeyboardMarkup BuildAdminCreatorElixirPromptKeyboard(CreatorProfile creator) { return new InlineKeyboardMarkup(new[] { new[] { InlineKeyboardButton.WithCallbackData("◀️ Назад", $"admin:creator:view:{creator.UserId}") } }); } private static CreatorRewardRequest[] GetPendingCreatorRewardRequests() { return CreatorRewardRequests.Values .Where(x => string.Equals(x.Status, CreatorRecordStatus.Pending, StringComparison.OrdinalIgnoreCase)) .OrderBy(x => x.CreatedAtUtc) .ToArray(); } private static int GetPendingCreatorRewardRequestIndex(CreatorRewardRequest[] requests, long requestId) { for (int i = 0; i < requests.Length; i++) { if (requests[i].RequestId == requestId) { return i; } } return -1; } private static InlineKeyboardMarkup BuildAdminCreatorRewardPromptKeyboard(long requestId, bool includeManualButton = true) { CreatorRewardRequest request = CreatorRewardRequests.TryGetValue(requestId, out CreatorRewardRequest currentRequest) ? currentRequest : null; CreatorRewardRequest[] requests = GetPendingCreatorRewardRequests(); int index = GetPendingCreatorRewardRequestIndex(requests, requestId); var rows = new List { new[] { InlineKeyboardButton.WithCallbackData("➕ 20", $"admin:reward:grant:{requestId}:20"), InlineKeyboardButton.WithCallbackData("➕ 35", $"admin:reward:grant:{requestId}:35"), InlineKeyboardButton.WithCallbackData("➕ 50", $"admin:reward:grant:{requestId}:50") }, new[] { InlineKeyboardButton.WithCallbackData("➕ 75", $"admin:reward:grant:{requestId}:75"), InlineKeyboardButton.WithCallbackData("➕ 100", $"admin:reward:grant:{requestId}:100"), InlineKeyboardButton.WithCallbackData("➕ 150", $"admin:reward:grant:{requestId}:150") } }; if (request != null) { rows.Add(new[] { InlineKeyboardButton.WithUrl("▶️ Открыть ролик", request.VideoUrl), InlineKeyboardButton.WithCallbackData("📄 Карточка", $"admin:reward:view:{requestId}") }); } if (includeManualButton) { rows.Add(new[] { InlineKeyboardButton.WithCallbackData("✍️ Своя сумма", $"admin:reward:approve:{requestId}") }); } if (requests.Length > 1 && index >= 0) { var navigationRow = new List(); if (index > 0) { navigationRow.Add(InlineKeyboardButton.WithCallbackData("⬅️ Пред.", $"admin:reward:view:{requests[index - 1].RequestId}")); } if (index < requests.Length - 1) { navigationRow.Add(InlineKeyboardButton.WithCallbackData("След. ➡️", $"admin:reward:view:{requests[index + 1].RequestId}")); } if (navigationRow.Count > 0) { rows.Add(navigationRow.ToArray()); } } rows.Add(new[] { InlineKeyboardButton.WithCallbackData("◀️ Назад", "admin:creators:rewards") }); return new InlineKeyboardMarkup(rows); } private static string BuildCreatorRewardQueueHeader(long requestId) { CreatorRewardRequest[] requests = GetPendingCreatorRewardRequests(); int index = GetPendingCreatorRewardRequestIndex(requests, requestId); if (index < 0) { return "📬 Очередь: заявка вне активной очереди"; } return $"📬 Очередь: {index + 1} из {requests.Length}"; } private static InlineKeyboardMarkup BuildAdminRewardReviewKeyboard(CreatorRewardRequest request) { var rows = new List { new[] { InlineKeyboardButton.WithCallbackData("✍️ Своя сумма", $"admin:reward:approve:{request.RequestId}"), InlineKeyboardButton.WithCallbackData("❌ Отклонить", $"admin:reward:reject:{request.RequestId}") } }; rows.AddRange(BuildAdminCreatorRewardPromptKeyboard(request.RequestId, includeManualButton: false).InlineKeyboard.Select(x => x.ToArray())); return new InlineKeyboardMarkup(rows); } private static InlineKeyboardMarkup BuildAdminInputPromptKeyboard(string state) { string backTarget = state == "voucher" ? "admin:voucher:prompt" : "admin:menu"; return new InlineKeyboardMarkup(new[] { new[] { InlineKeyboardButton.WithCallbackData("◀️ Назад", backTarget) } }); } private static async Task ShowAdminCreatorApplicationsAsync(ITelegramBotClient botClient, CallbackQuery callback, CancellationToken cancellationToken) { CreatorApplicationRecord[] apps = CreatorApplications.Values .Where(x => string.Equals(x.Status, CreatorRecordStatus.Pending, StringComparison.OrdinalIgnoreCase)) .OrderBy(x => x.CreatedAtUtc) .Take(10) .ToArray(); if (apps.Length == 0) { await EditMenuAsync(botClient, callback, "📝 Заявки креаторов\n\nНовых заявок нет.", BuildAdminCreatorsMenuKeyboard(), cancellationToken); return; } CreatorApplicationRecord app = apps[0]; await EditMenuAsync( botClient, callback, BuildCreatorApplicationReviewText(app), new InlineKeyboardMarkup(new[] { new[] { InlineKeyboardButton.WithCallbackData("✅ Одобрить", $"admin:creator:approve:{app.ApplicationId}"), InlineKeyboardButton.WithCallbackData("❌ Отклонить", $"admin:creator:reject:{app.ApplicationId}") }, new[] { InlineKeyboardButton.WithCallbackData("◀️ Назад", "admin:creators:menu") } }), cancellationToken); } private static async Task ShowAdminCreatorRewardRequestsAsync(ITelegramBotClient botClient, CallbackQuery callback, CancellationToken cancellationToken) { CreatorRewardRequest[] requests = GetPendingCreatorRewardRequests(); if (requests.Length == 0) { await EditMenuAsync(botClient, callback, "🧪 Заявки на эликсиры\n\nОжидающих заявок нет.", BuildAdminCreatorsMenuKeyboard(), cancellationToken); return; } await ShowAdminCreatorRewardRequestAsync(botClient, callback, requests[0].RequestId.ToString(), cancellationToken); } private static async Task ShowAdminCreatorRewardRequestAsync(ITelegramBotClient botClient, CallbackQuery callback, string requestIdRaw, CancellationToken cancellationToken) { if (!long.TryParse(requestIdRaw, out long requestId) || !CreatorRewardRequests.TryGetValue(requestId, out CreatorRewardRequest request)) { await botClient.AnswerCallbackQueryAsync(callback.Id, "Заявка не найдена.", showAlert: true, cancellationToken: cancellationToken); return; } await EditMenuAsync( botClient, callback, BuildCreatorRewardRequestText(request) + "\n\n" + BuildCreatorRewardQueueHeader(request.RequestId), BuildAdminRewardReviewKeyboard(request), cancellationToken); } private static async Task ShowAdminCreatorUpgradeRequestsAsync(ITelegramBotClient botClient, CallbackQuery callback, CancellationToken cancellationToken) { CreatorUpgradeRequest[] requests = CreatorUpgradeRequests.Values .Where(x => string.Equals(x.Status, CreatorRecordStatus.Pending, StringComparison.OrdinalIgnoreCase)) .OrderBy(x => x.CreatedAtUtc) .Take(10) .ToArray(); if (requests.Length == 0) { await EditMenuAsync(botClient, callback, "📈 Заявки на повышение\n\nОжидающих заявок нет.", BuildAdminCreatorsMenuKeyboard(), cancellationToken); return; } CreatorUpgradeRequest request = requests[0]; await EditMenuAsync( botClient, callback, BuildCreatorUpgradeRequestText(request), new InlineKeyboardMarkup(new[] { new[] { InlineKeyboardButton.WithCallbackData("✅ Одобрить", $"admin:upgrade:approve:{request.RequestId}"), InlineKeyboardButton.WithCallbackData("❌ Отклонить", $"admin:upgrade:reject:{request.RequestId}") }, new[] { InlineKeyboardButton.WithCallbackData("◀️ Назад", "admin:creators:menu") } }), cancellationToken); } private static async Task ShowAdminCreatorsListAsync(ITelegramBotClient botClient, CallbackQuery callback, CancellationToken cancellationToken) { CreatorProfile[] creators = CreatorProfiles.Values.Where(x => x.IsActive).OrderBy(x => x.CreatorCode).Take(20).ToArray(); if (creators.Length == 0) { await EditMenuAsync(botClient, callback, "👥 Креаторы\n\nСписок пуст.", BuildAdminCreatorsMenuKeyboard(), cancellationToken); return; } var rows = creators .Select(x => new[] { InlineKeyboardButton.WithCallbackData($"{x.CreatorCode} • {GetCreatorTierLabel(x.Tier)}", $"admin:creator:view:{x.UserId}") }) .Append(new[] { InlineKeyboardButton.WithCallbackData("◀️ Назад", "admin:creators:menu") }) .ToArray(); await EditMenuAsync( botClient, callback, $"👥 Креаторы ({creators.Length})\n\nВыбери креатора:", new InlineKeyboardMarkup(rows), cancellationToken); } private static async Task ApproveCreatorApplicationAsync(ITelegramBotClient botClient, CallbackQuery callback, string applicationIdRaw, CancellationToken cancellationToken) { if (!long.TryParse(applicationIdRaw, out long applicationId) || !CreatorApplications.TryGetValue(applicationId, out CreatorApplicationRecord application)) { await botClient.AnswerCallbackQueryAsync(callback.Id, "Заявка не найдена.", showAlert: true, cancellationToken: cancellationToken); return; } CreatorProfile profile = new CreatorProfile { UserId = application.UserId, AccountId = application.AccountId, AccountTag = application.AccountTag, AccountName = application.AccountName, CreatorCode = application.CreatorCode, PlatformName = application.PlatformName, PlatformUrl = application.PlatformUrl, Tier = CreatorTier.Ordinary, Elixirs = 0, JoinedAtUtc = DateTime.UtcNow, IsActive = true }; lock (SyncRoot) { application.Status = CreatorRecordStatus.Approved; application.ReviewedAtUtc = DateTime.UtcNow; CreatorApplications[applicationId] = application; CreatorProfiles[profile.UserId] = profile; SaveCreatorApplications(); SaveCreatorProfiles(); } SetAccountCreatorLevel(application.AccountId, 1); SyncCreatorCodeFile(); try { await botClient.SendTextMessageAsync(application.UserId, "✅ Заявка на креатора одобрена\n\n" + $"Твой код: {Escape(profile.CreatorCode)}\n" + $"Уровень: {Escape(GetCreatorTierLabel(profile.Tier))}", parseMode: ParseMode.Html, replyMarkup: BuildCreatorMenuKeyboard(application.UserId), cancellationToken: cancellationToken); } catch { } await EditMenuAsync(botClient, callback, "✅ Заявка одобрена.", BuildAdminCreatorsMenuKeyboard(), cancellationToken); } private static async Task RejectCreatorApplicationAsync(ITelegramBotClient botClient, CallbackQuery callback, string applicationIdRaw, CancellationToken cancellationToken) { if (!long.TryParse(applicationIdRaw, out long applicationId) || !CreatorApplications.TryGetValue(applicationId, out CreatorApplicationRecord application)) { await botClient.AnswerCallbackQueryAsync(callback.Id, "Заявка не найдена.", showAlert: true, cancellationToken: cancellationToken); return; } lock (SyncRoot) { application.Status = CreatorRecordStatus.Rejected; application.ReviewedAtUtc = DateTime.UtcNow; CreatorApplications[applicationId] = application; SaveCreatorApplications(); } try { await botClient.SendTextMessageAsync(application.UserId, "❌ Заявка на креатора отклонена.", cancellationToken: cancellationToken); } catch { } await EditMenuAsync(botClient, callback, "❌ Заявка отклонена.", BuildAdminCreatorsMenuKeyboard(), cancellationToken); } private static async Task ShowAdminCreatorProfileAsync(ITelegramBotClient botClient, CallbackQuery callback, string creatorUserIdRaw, CancellationToken cancellationToken) { if (!long.TryParse(creatorUserIdRaw, out long creatorUserId) || GetCreatorProfile(creatorUserId) is not CreatorProfile creator) { await botClient.AnswerCallbackQueryAsync(callback.Id, "Креатор не найден.", showAlert: true, cancellationToken: cancellationToken); return; } await EditMenuAsync( botClient, callback, BuildAdminCreatorProfileText(creator), BuildAdminCreatorProfileKeyboard(creator), cancellationToken); } private static async Task SetCreatorLevelAsync(ITelegramBotClient botClient, CallbackQuery callback, string creatorUserIdRaw, CreatorTier tier, CancellationToken cancellationToken) { if (!long.TryParse(creatorUserIdRaw, out long creatorUserId) || GetCreatorProfile(creatorUserId) is not CreatorProfile creator) { await botClient.AnswerCallbackQueryAsync(callback.Id, "Креатор не найден.", showAlert: true, cancellationToken: cancellationToken); return; } creator.Tier = tier; SaveCreatorProfiles(); SetAccountCreatorLevel(creator.AccountId, tier == CreatorTier.SuperCreator ? 2 : 1); SyncCreatorCodeFile(); await EditMenuAsync(botClient, callback, BuildAdminCreatorProfileText(creator), BuildAdminCreatorProfileKeyboard(creator), cancellationToken); } private static async Task PromptCreatorElixirGrantAsync(ITelegramBotClient botClient, CallbackQuery callback, string creatorUserIdRaw, CancellationToken cancellationToken) { if (!long.TryParse(creatorUserIdRaw, out long creatorUserId) || GetCreatorProfile(creatorUserId) is not CreatorProfile creator) { await botClient.AnswerCallbackQueryAsync(callback.Id, "Креатор не найден.", showAlert: true, cancellationToken: cancellationToken); return; } lock (AdminStates) { AdminInputState entry = GetOrCreateAdminStateLocked(callback.From.Id); entry.Mode = "creator_elixir_grant"; entry.PendingCreatorUserId = creatorUserId; } await EditMenuAsync( botClient, callback, $"🧪 Выдача эликсиров\n\nКреатор: {Escape(creator.CreatorCode)}\nТекущий баланс: {creator.Elixirs}\n\nОтправь числом, сколько эликсиров добавить или снять.", new InlineKeyboardMarkup(new[] { new[] { InlineKeyboardButton.WithCallbackData("◀️ Назад", $"admin:creator:view:{creator.UserId}") } }), cancellationToken); } private static async Task KickCreatorAsync(ITelegramBotClient botClient, CallbackQuery callback, string creatorUserIdRaw, CancellationToken cancellationToken) { if (!long.TryParse(creatorUserIdRaw, out long creatorUserId) || GetCreatorProfile(creatorUserId) is not CreatorProfile creator) { await botClient.AnswerCallbackQueryAsync(callback.Id, "Креатор не найден.", showAlert: true, cancellationToken: cancellationToken); return; } creator.IsActive = false; SaveCreatorProfiles(); SetAccountCreatorLevel(creator.AccountId, 0); SyncCreatorCodeFile(); try { await botClient.SendTextMessageAsync(creator.UserId, "❌ Ты исключён из креаторской программы.", cancellationToken: cancellationToken); } catch { } await EditMenuAsync(botClient, callback, "❌ Креатор исключён из программы.", BuildAdminCreatorsMenuKeyboard(), cancellationToken); } private static async Task PromptRewardApprovalAsync(ITelegramBotClient botClient, CallbackQuery callback, string requestIdRaw, CancellationToken cancellationToken) { if (!long.TryParse(requestIdRaw, out long requestId) || !CreatorRewardRequests.TryGetValue(requestId, out CreatorRewardRequest request)) { await botClient.AnswerCallbackQueryAsync(callback.Id, "Заявка не найдена.", showAlert: true, cancellationToken: cancellationToken); return; } lock (AdminStates) { AdminInputState entry = GetOrCreateAdminStateLocked(callback.From.Id); entry.Mode = "creator_reward_approve"; entry.PendingRewardRequestId = requestId; } await EditMenuAsync( botClient, callback, BuildCreatorRewardRequestText(request) + "\n\nОтправь сумму числом или в формате:\n" + "100\n" + "elixir 100", BuildAdminCreatorRewardPromptKeyboard(request.RequestId), cancellationToken); } private static async Task RejectCreatorRewardRequestAsync(ITelegramBotClient botClient, CallbackQuery callback, string requestIdRaw, CancellationToken cancellationToken) { if (!long.TryParse(requestIdRaw, out long requestId) || !CreatorRewardRequests.TryGetValue(requestId, out CreatorRewardRequest request)) { await botClient.AnswerCallbackQueryAsync(callback.Id, "Заявка не найдена.", showAlert: true, cancellationToken: cancellationToken); return; } request.Status = CreatorRecordStatus.Rejected; request.Decision = "Отклонено"; request.ReviewedAtUtc = DateTime.UtcNow; request.ReviewedByTelegramId = callback.From.Id; SaveCreatorRewardRequests(); CreatorProfile creator = GetCreatorProfile(request.UserId); if (creator != null) { creator.RewardRejections++; SaveCreatorProfiles(); } try { await botClient.SendTextMessageAsync(request.UserId, "❌ Заявка на эликсиры отклонена.", cancellationToken: cancellationToken); } catch { } CreatorRewardRequest nextRequest = GetPendingCreatorRewardRequests().FirstOrDefault(); if (nextRequest != null) { await EditMenuAsync( botClient, callback, "❌ Заявка отклонена\n\nПоказываю следующую в очереди.\n\n" + BuildCreatorRewardRequestText(nextRequest) + "\n\n" + BuildCreatorRewardQueueHeader(nextRequest.RequestId), BuildAdminRewardReviewKeyboard(nextRequest), cancellationToken); return; } await EditMenuAsync(botClient, callback, "❌ Заявка отклонена\n\nНовых заявок в очереди больше нет.", BuildAdminCreatorsMenuKeyboard(), cancellationToken); } private static async Task ApproveCreatorRewardRequestAsync(ITelegramBotClient botClient, CallbackQuery callback, string payload, CancellationToken cancellationToken) { string[] parts = payload.Split(':', StringSplitOptions.RemoveEmptyEntries); if (parts.Length != 2 || !long.TryParse(parts[0], out long requestId) || !int.TryParse(parts[1], out int elixirAmount) || elixirAmount <= 0) { await botClient.AnswerCallbackQueryAsync(callback.Id, "Некорректная сумма.", showAlert: true, cancellationToken: cancellationToken); return; } if (!CreatorRewardRequests.TryGetValue(requestId, out CreatorRewardRequest request)) { await botClient.AnswerCallbackQueryAsync(callback.Id, "Заявка не найдена.", showAlert: true, cancellationToken: cancellationToken); return; } if (!TryApplyCreatorRewardInput(request, elixirAmount.ToString(), out string rewardResult)) { await botClient.AnswerCallbackQueryAsync(callback.Id, "Не удалось начислить эликсиры.", showAlert: true, cancellationToken: cancellationToken); return; } request.Status = CreatorRecordStatus.Approved; request.Decision = rewardResult; request.ReviewedAtUtc = DateTime.UtcNow; request.ReviewedByTelegramId = callback.From.Id; SaveCreatorRewardRequests(); ClearAdminPendingReward(callback.From.Id); CreatorProfile creator = GetCreatorProfile(request.UserId); if (creator != null) { creator.RewardApprovals++; SaveCreatorProfiles(); } try { await botClient.SendTextMessageAsync( request.UserId, "✅ Заявка на эликсиры одобрена\n\n" + $"{Escape(rewardResult)}", parseMode: ParseMode.Html, replyMarkup: BuildCreatorMenuKeyboard(request.UserId), cancellationToken: cancellationToken); } catch { } CreatorRewardRequest nextRequest = GetPendingCreatorRewardRequests().FirstOrDefault(); if (nextRequest != null) { await EditMenuAsync( botClient, callback, "✅ Эликсиры начислены\n" + $"{Escape(rewardResult)}\n\n" + "Показываю следующую заявку.\n\n" + BuildCreatorRewardRequestText(nextRequest) + "\n\n" + BuildCreatorRewardQueueHeader(nextRequest.RequestId), BuildAdminRewardReviewKeyboard(nextRequest), cancellationToken); return; } await EditMenuAsync( botClient, callback, "✅ Эликсиры начислены\n\n" + $"{Escape(rewardResult)}\n\n" + "Очередь заявок пуста.", BuildAdminCreatorsMenuKeyboard(), cancellationToken); } private static async Task ApproveCreatorUpgradeRequestAsync(ITelegramBotClient botClient, CallbackQuery callback, string requestIdRaw, CancellationToken cancellationToken) { if (!long.TryParse(requestIdRaw, out long requestId) || !CreatorUpgradeRequests.TryGetValue(requestId, out CreatorUpgradeRequest request)) { await botClient.AnswerCallbackQueryAsync(callback.Id, "Заявка не найдена.", showAlert: true, cancellationToken: cancellationToken); return; } CreatorProfile creator = GetCreatorProfile(request.UserId); if (creator == null) { await botClient.AnswerCallbackQueryAsync(callback.Id, "Креатор не найден.", showAlert: true, cancellationToken: cancellationToken); return; } creator.Tier = CreatorTier.SuperCreator; SaveCreatorProfiles(); SetAccountCreatorLevel(creator.AccountId, 2); SyncCreatorCodeFile(); request.Status = CreatorRecordStatus.Approved; request.Decision = "Уровень повышен до суперкреатора"; request.ReviewedAtUtc = DateTime.UtcNow; request.ReviewedByTelegramId = callback.From.Id; SaveCreatorUpgradeRequests(); try { await botClient.SendTextMessageAsync( request.UserId, "✅ Заявка на повышение одобрена\n\nТвой уровень повышен до суперкреатора.", parseMode: ParseMode.Html, replyMarkup: BuildCreatorMenuKeyboard(request.UserId), cancellationToken: cancellationToken); } catch { } await EditMenuAsync(botClient, callback, "✅ Уровень креатора повышен.", BuildAdminCreatorsMenuKeyboard(), cancellationToken); } private static async Task RejectCreatorUpgradeRequestAsync(ITelegramBotClient botClient, CallbackQuery callback, string requestIdRaw, CancellationToken cancellationToken) { if (!long.TryParse(requestIdRaw, out long requestId) || !CreatorUpgradeRequests.TryGetValue(requestId, out CreatorUpgradeRequest request)) { await botClient.AnswerCallbackQueryAsync(callback.Id, "Заявка не найдена.", showAlert: true, cancellationToken: cancellationToken); return; } request.Status = CreatorRecordStatus.Rejected; request.Decision = "Отклонено"; request.ReviewedAtUtc = DateTime.UtcNow; request.ReviewedByTelegramId = callback.From.Id; SaveCreatorUpgradeRequests(); try { await botClient.SendTextMessageAsync(request.UserId, "❌ Заявка на повышение уровня отклонена.", cancellationToken: cancellationToken); } catch { } await EditMenuAsync(botClient, callback, "❌ Заявка отклонена.", BuildAdminCreatorsMenuKeyboard(), cancellationToken); } private static string BuildCreatorApplicationReviewText(CreatorApplicationRecord application) { return "📝 Заявка на креатора\n\n" + $"👤 Пользователь: {Escape(application.AccountName)}\n" + $"🏷 Аккаунт: {Escape(application.AccountTag)}\n" + $"🔑 Код: {Escape(application.CreatorCode)}\n" + $"📱 Платформа: {Escape(application.PlatformName)}\n" + $"🔗 Ссылка: {Escape(application.PlatformUrl)}\n" + $"🧾 Описание: {Escape(application.Note)}\n" + $"🕒 Создана: {application.CreatedAtUtc:dd.MM.yyyy HH:mm} UTC"; } private static string BuildCreatorRewardRequestText(CreatorRewardRequest request) { CreatorProfile creator = GetCreatorProfile(request.UserId); string creatorName = creator != null ? creator.AccountName : request.CreatorCode; int currentElixirs = creator?.Elixirs ?? 0; return "🧪 Заявка на эликсиры\n\n" + $"👤 Креатор: {Escape(creatorName)}\n" + $"🔑 Код: {Escape(request.CreatorCode)}\n" + $"🏷 Аккаунт: {Escape(request.AccountTag)}\n" + $"💠 Текущий баланс: {currentElixirs} эликсиров\n" + $"📱 Платформа: {Escape(request.PlatformName)}\n" + $"🔗 Материал: {Escape(request.VideoUrl)}\n" + $"🧾 Комментарий: {Escape(request.Comment)}\n" + $"🕒 Создана: {request.CreatedAtUtc:dd.MM.yyyy HH:mm} UTC"; } private static string BuildCreatorUpgradeRequestText(CreatorUpgradeRequest request) { return "📈 Заявка на повышение уровня\n\n" + $"🔑 Код: {Escape(request.CreatorCode)}\n" + $"🏷 Аккаунт: {Escape(request.AccountTag)}\n" + $"⭐ Текущий уровень: {Escape(GetCreatorTierLabel(request.CurrentTier))}\n" + $"👥 Подписчики: {request.SubscribersCount}\n" + $"👁 Средние просмотры: {request.AverageViews}\n" + $"🔥 Максимальные просмотры: {request.BestViews}\n" + $"🧾 Зачем повышение: {Escape(request.Reason)}\n" + $"📌 Дополнительно: {Escape(request.AdditionalInfo)}\n" + $"🕒 Создана: {request.CreatedAtUtc:dd.MM.yyyy HH:mm}"; } private static string BuildAdminCreatorProfileText(CreatorProfile creator) { return "🎬 Профиль креатора\n\n" + $"🔑 Код: {Escape(creator.CreatorCode)}\n" + $"👤 Игрок: {Escape(creator.AccountName)}\n" + $"🏷 Тег: {Escape(creator.AccountTag)}\n" + $"📱 Платформа: {Escape(creator.PlatformName)}\n" + $"🔗 Ссылка: {Escape(creator.PlatformUrl)}\n" + $"⭐ Уровень: {Escape(GetCreatorTierLabel(creator.Tier))}\n" + $"🧪 Эликсиры: {creator.Elixirs}\n" + $"📅 Вступил: {creator.JoinedAtUtc:dd.MM.yyyy HH:mm} UTC\n" + $"📍 Статус: {(creator.IsActive ? "активен" : "отключен")}"; } private static InlineKeyboardMarkup BuildAdminCreatorProfileKeyboard(CreatorProfile creator) { return new InlineKeyboardMarkup(new[] { new[] { InlineKeyboardButton.WithCallbackData("Обычный", $"admin:creator:level:ordinary:{creator.UserId}"), InlineKeyboardButton.WithCallbackData("Супер", $"admin:creator:level:super:{creator.UserId}") }, new[] { InlineKeyboardButton.WithCallbackData("🧪 Эликсиры", $"admin:creator:elixir:{creator.UserId}"), InlineKeyboardButton.WithCallbackData("❌ Кик", $"admin:creator:kick:{creator.UserId}") }, new[] { InlineKeyboardButton.WithCallbackData("◀️ Назад", "admin:creators:list") } }); } private static async Task NotifyAdminsAboutCreatorApplicationAsync(ITelegramBotClient botClient, CreatorApplicationRecord application, CancellationToken cancellationToken) { foreach (long adminId in Configuration.Instance?.AdminTelegramIds ?? Array.Empty()) { try { await botClient.SendTextMessageAsync( adminId, BuildCreatorApplicationReviewText(application), parseMode: ParseMode.Html, replyMarkup: new InlineKeyboardMarkup(new[] { new[] { InlineKeyboardButton.WithCallbackData("✅ Одобрить", $"admin:creator:approve:{application.ApplicationId}"), InlineKeyboardButton.WithCallbackData("❌ Отклонить", $"admin:creator:reject:{application.ApplicationId}") } }), cancellationToken: cancellationToken); } catch { } } } private static async Task NotifyAdminsAboutRewardRequestAsync(ITelegramBotClient botClient, CreatorRewardRequest request, CancellationToken cancellationToken) { foreach (long adminId in Configuration.Instance?.AdminTelegramIds ?? Array.Empty()) { try { await botClient.SendTextMessageAsync( adminId, BuildCreatorRewardRequestText(request) + "\n\n" + BuildCreatorRewardQueueHeader(request.RequestId), parseMode: ParseMode.Html, replyMarkup: BuildAdminRewardReviewKeyboard(request), cancellationToken: cancellationToken); } catch { } } } private static InlineKeyboardMarkup BuildCreatorElixirShopKeyboard(long telegramId) { CreatorProfile creator = GetCreatorProfile(telegramId); CreatorShopReward[] rewards = GetCurrentCreatorShopRewards(); var rows = rewards .Chunk(2) .Select(chunk => chunk.Select(reward => { bool enough = creator?.Elixirs >= reward.ElixirCost; string label = $"{reward.Icon} {reward.ElixirCost} • {reward.Title}"; return InlineKeyboardButton.WithCallbackData(enough ? label : $"{label} 🔒", $"creator:redeem:{reward.Id}"); }).ToArray()) .ToList(); rows.Add(new[] { InlineKeyboardButton.WithCallbackData( creator?.Elixirs >= CreatorVipMonthElixirCost ? $"👑 VIP 1 мес. • {CreatorVipMonthElixirCost}" : $"👑 VIP 1 мес. • {CreatorVipMonthElixirCost} 🔒", "creator:vip:buy") }); rows.Add(new[] { InlineKeyboardButton.WithCallbackData("🔄 Обновить витрину", "creator:elixir:shop") }); rows.Add(new[] { InlineKeyboardButton.WithCallbackData("◀️ Назад", "creator:menu") }); return new InlineKeyboardMarkup(rows); } private static async Task HandleCreatorElixirRedeemAsync(ITelegramBotClient botClient, CallbackQuery callback, string itemId, CancellationToken cancellationToken) { CreatorProfile creator = GetCreatorProfile(callback.From.Id); if (creator == null || !creator.IsActive) { await botClient.AnswerCallbackQueryAsync(callback.Id, "Раздел доступен только креаторам.", showAlert: true, cancellationToken: cancellationToken); return; } CreatorShopReward reward = GetCurrentCreatorShopRewards().FirstOrDefault(x => string.Equals(x.Id, itemId, StringComparison.OrdinalIgnoreCase)); if (reward == null) { await botClient.AnswerCallbackQueryAsync(callback.Id, "Этой награды уже нет в текущей ротации.", showAlert: true, cancellationToken: cancellationToken); return; } if (creator.Elixirs < reward.ElixirCost) { await botClient.AnswerCallbackQueryAsync(callback.Id, "Недостаточно эликсиров.", showAlert: true, cancellationToken: cancellationToken); return; } string code = $"CR{Guid.NewGuid():N}".Substring(0, 10).ToUpperInvariant(); AdminConsoleTools.CreateVoucher(code, reward.VoucherRewards); string voucherLink = BuildVoucherActivationLink(code); creator.Elixirs -= reward.ElixirCost; SaveCreatorProfiles(); await EditMenuAsync( botClient, callback, "✅ Покупка выполнена\n\n" + $"🎁 Награда: {Escape(reward.Title)}\n" + $"🏷 Код ваучера: {Escape(code)}\n" + $"🔗 Ссылка: {Escape(voucherLink)}\n" + $"Остаток эликсиров: {creator.Elixirs}", BuildCreatorElixirShopKeyboard(callback.From.Id), cancellationToken); } private static async Task HandleCreatorVipPurchaseAsync(ITelegramBotClient botClient, CallbackQuery callback, CancellationToken cancellationToken) { CreatorProfile creator = GetCreatorProfile(callback.From.Id); if (creator == null || !creator.IsActive) { await botClient.AnswerCallbackQueryAsync(callback.Id, "Раздел доступен только креаторам.", showAlert: true, cancellationToken: cancellationToken); return; } if (creator.Elixirs < CreatorVipMonthElixirCost) { await botClient.AnswerCallbackQueryAsync(callback.Id, "Недостаточно эликсиров для VIP.", showAlert: true, cancellationToken: cancellationToken); return; } if (!GrantCreatorVip(creator, 1)) { await botClient.AnswerCallbackQueryAsync(callback.Id, "Не удалось выдать VIP. Проверь привязку аккаунта.", showAlert: true, cancellationToken: cancellationToken); return; } creator.Elixirs -= CreatorVipMonthElixirCost; SaveCreatorProfiles(); await EditMenuAsync( botClient, callback, "✅ VIP активирован\n\n" + $"👑 Услуга: VIP на 1 месяц\n" + $"🧪 Списано: {CreatorVipMonthElixirCost} эликсиров\n" + $"💠 Остаток: {creator.Elixirs} эликсиров", BuildCreatorElixirShopKeyboard(callback.From.Id), cancellationToken); } private static async Task ShowCreatorLeaveConfirmAsync(ITelegramBotClient botClient, CallbackQuery callback, CancellationToken cancellationToken) { CreatorProfile creator = GetCreatorProfile(callback.From.Id); if (creator == null || !creator.IsActive) { await botClient.AnswerCallbackQueryAsync(callback.Id, "Ты уже не состоишь в программе.", showAlert: true, cancellationToken: cancellationToken); return; } await EditMenuAsync( botClient, callback, "🚪 Выход из программы креаторов\n\n" + $"Код: {Escape(creator.CreatorCode)}\n\n" + "После выхода твой креаторский статус будет снят, магазин и заявки станут недоступны.\n\n" + "Продолжить?", new InlineKeyboardMarkup(new[] { new[] { InlineKeyboardButton.WithCallbackData("✅ Да, выйти", "creator:leave") }, new[] { InlineKeyboardButton.WithCallbackData("◀️ Назад", "creator:menu") } }), cancellationToken); } private static async Task HandleCreatorLeaveAsync(ITelegramBotClient botClient, CallbackQuery callback, CancellationToken cancellationToken) { CreatorProfile creator = GetCreatorProfile(callback.From.Id); if (creator == null || !creator.IsActive) { await botClient.AnswerCallbackQueryAsync(callback.Id, "Ты уже не состоишь в программе.", showAlert: true, cancellationToken: cancellationToken); return; } creator.IsActive = false; SaveCreatorProfiles(); SetAccountCreatorLevel(creator.AccountId, 0); SyncCreatorCodeFile(); await EditMenuAsync( botClient, callback, "✅ Ты вышел из программы креаторов\n\n" + "Если захочешь вернуться позже, можно будет подать новую заявку.", BuildCreatorMenuKeyboard(callback.From.Id), cancellationToken); } private static bool TryGetAdminPendingCreator(long adminTelegramId, out CreatorProfile creator) { lock (AdminStates) { creator = null; if (!AdminStates.TryGetValue(adminTelegramId, out AdminInputState entry) || entry.PendingCreatorUserId <= 0) { return false; } creator = GetCreatorProfile(entry.PendingCreatorUserId); return creator != null; } } private static bool TryGetAdminPendingReward(long adminTelegramId, out CreatorRewardRequest request) { lock (AdminStates) { request = null; if (!AdminStates.TryGetValue(adminTelegramId, out AdminInputState entry) || entry.PendingRewardRequestId <= 0) { return false; } CreatorRewardRequests.TryGetValue(entry.PendingRewardRequestId, out request); return request != null; } } private static void ClearAdminPendingCreator(long adminTelegramId) { lock (AdminStates) { if (!AdminStates.TryGetValue(adminTelegramId, out AdminInputState entry)) { return; } entry.PendingCreatorUserId = 0; if (string.IsNullOrWhiteSpace(entry.Mode) && entry.Voucher == null && entry.PendingRewardRequestId <= 0) { AdminStates.Remove(adminTelegramId); } } } private static async Task NotifyAdminsAboutCreatorUpgradeRequestAsync(ITelegramBotClient botClient, CreatorUpgradeRequest request, CancellationToken cancellationToken) { foreach (long adminId in Configuration.Instance?.AdminTelegramIds ?? Array.Empty()) { try { await botClient.SendTextMessageAsync( adminId, BuildCreatorUpgradeRequestText(request), parseMode: ParseMode.Html, replyMarkup: new InlineKeyboardMarkup(new[] { new[] { InlineKeyboardButton.WithCallbackData("✅ Одобрить", $"admin:upgrade:approve:{request.RequestId}"), InlineKeyboardButton.WithCallbackData("❌ Отклонить", $"admin:upgrade:reject:{request.RequestId}") } }), cancellationToken: cancellationToken); } catch { } } } private static void ClearAdminPendingReward(long adminTelegramId) { lock (AdminStates) { if (!AdminStates.TryGetValue(adminTelegramId, out AdminInputState entry)) { return; } entry.PendingRewardRequestId = 0; if (string.IsNullOrWhiteSpace(entry.Mode) && entry.Voucher == null && entry.PendingCreatorUserId <= 0) { AdminStates.Remove(adminTelegramId); } } } private static bool TryApplyCreatorRewardInput(CreatorRewardRequest request, string input, out string result) { result = null; CreatorProfile creator = GetCreatorProfile(request.UserId); if (creator == null) { return false; } string normalizedInput = (input ?? string.Empty).Trim(); int elixirAmount; if (int.TryParse(normalizedInput, out elixirAmount)) { if (elixirAmount <= 0) { return false; } } else { string[] parts = normalizedInput.Split(' ', 2, StringSplitOptions.RemoveEmptyEntries); if (parts.Length != 2) { return false; } string type = parts[0].Trim().ToLowerInvariant(); string value = parts[1].Trim(); if (type != "elixir" || !int.TryParse(value, out elixirAmount) || elixirAmount <= 0) { return false; } } if (elixirAmount <= 0) { return false; } creator.Elixirs = Math.Max(0, creator.Elixirs + elixirAmount); SaveCreatorProfiles(); result = $"Начислено {elixirAmount} эликсиров"; return true; } private static void GrantCreatorAccountCurrency(CreatorProfile creator, string currency, int amount) { if (!AdminConsoleTools.TryResolveAccount(creator.AccountTag, out Account account, out _, out _)) { return; } AdminConsoleTools.GrantCurrency(account, currency, amount, "creator-program"); } private static bool GrantCreatorVip(CreatorProfile creator, int months) { if (!AdminConsoleTools.TryResolveAccount(creator.AccountTag, out Account account, out _, out _)) { return false; } if (account.Home.PremiumEndTime < DateTime.UtcNow) { account.Home.PremiumEndTime = DateTime.UtcNow.AddMonths(months); } else { account.Home.PremiumEndTime = account.Home.PremiumEndTime.AddMonths(months); } account.Avatar.IsPremium = true; account.Avatar.PremiumLevel = 1; Accounts.Save(account); AdminConsoleTools.RefreshOnlineAccount(account.AccountId, "creator-vip"); return true; } private static void SetAccountCreatorLevel(long accountId, int level) { Account account = Accounts.Load(accountId); if (account == null) { return; } account.Avatar.CreatorLevel = level; Accounts.Save(account); AdminConsoleTools.RefreshOnlineAccount(accountId, "creator-level"); } private static string BuildBindingsText() { TelegramBindingRecord[] bindings = GetBindingsSnapshot(); if (bindings.Length == 0) return "🔗 Привязки\n\nПривязок пока нет."; string rows = string.Join("\n", bindings.Take(20).Select((x, i) => { string tg = string.IsNullOrWhiteSpace(x.TelegramUsername) ? $"id:{x.TelegramId}" : "@" + Escape(x.TelegramUsername); return $"{i + 1}. {Escape(x.AccountName)} {Escape(NormalizeAccountTag(x.AccountTag))} — {tg}"; })); string footer = bindings.Length > 20 ? $"\n\nПоказано 20 из {bindings.Length}" : ""; return $"🔗 Привязки ({bindings.Length})\n\n{rows}{footer}"; } private static string BuildTopPlayersText() { string report = AdminConsoleTools.BuildLeaderboardReport("players", 10); if (string.IsNullOrWhiteSpace(report)) { return "🏆 Топ игроков\n\nРейтинг пока недоступен."; } return TrimTelegram( "🏆 Топ игроков\n\n" + $"
{Escape(report)}
"); } private static string BuildTopAlliancesText() { string report = AdminConsoleTools.BuildLeaderboardReport("alliances", 10); if (string.IsNullOrWhiteSpace(report)) { return "👥 Топ клубов\n\nРейтинг пока недоступен."; } return TrimTelegram( "👥 Топ клубов\n\n" + $"
{Escape(report)}
"); } private static string TrimTelegram(string text) { const int max = 3900; if (string.IsNullOrWhiteSpace(text)) { return "-"; } return text.Length <= max ? text : text.Substring(0, max - 3) + "..."; } private static string BuildAccountTag(long accountId) { return "#" + LogicLongCodeGenerator.ToCode(accountId); } private static string NormalizeAccountTag(string tag) { if (string.IsNullOrWhiteSpace(tag)) { return null; } string normalized = tag.Trim(); while (normalized.StartsWith("##", StringComparison.Ordinal)) { normalized = normalized.Substring(1); } return normalized.StartsWith('#') ? normalized : "#" + normalized; } private static TelegramBindingRecord BuildBindingRecord(Account account, long telegramId, string telegramUsername) { return new TelegramBindingRecord { TelegramId = telegramId, TelegramUsername = telegramUsername, AccountId = account.AccountId, AccountName = account.Avatar.Name, AccountTag = BuildAccountTag(account.AccountId), BoundAtUtc = DateTime.UtcNow, LastLoginAtUtc = null }; } private static TelegramBindingRecord NormalizeBindingRecord(TelegramBindingRecord binding) { if (binding == null) { return null; } binding.AccountTag = NormalizeAccountTag(binding.AccountTag) ?? BuildAccountTag(binding.AccountId); return binding; } private static bool CleanupPendingLinks(List pendingLinks) { int countBefore = pendingLinks.Count; pendingLinks.RemoveAll(x => x.ExpiresAtUtc <= DateTime.UtcNow || x.Consumed); return pendingLinks.Count != countBefore; } private static async Task SendAuditExportAsync(ITelegramBotClient botClient, CallbackQuery callback, string exportPath, CancellationToken cancellationToken) { await EditMenuAsync(botClient, callback, "📁 Аудит-лог подготовлен\n\nФайл отправлен следующим сообщением.", BuildAdminMenuKeyboard(), cancellationToken); await using FileStream stream = System.IO.File.OpenRead(exportPath); InputOnlineFile file = new InputOnlineFile(stream, Path.GetFileName(exportPath)); await botClient.SendDocumentAsync( chatId: callback.Message.Chat.Id, document: file, caption: "📁 Аудит-лог", cancellationToken: cancellationToken); } private static async Task CreatePaymentAsync(ITelegramBotClient botClient, CallbackQuery callback, string productId, string paymentMethod, CancellationToken cancellationToken) { if (!IsPaymentConfigured()) { await botClient.AnswerCallbackQueryAsync(callback.Id, "Оплата сейчас недоступна.", showAlert: true, cancellationToken: cancellationToken); return; } TelegramBindingRecord binding = GetBindingByTelegramId(callback.From.Id); if (binding == null) { await botClient.AnswerCallbackQueryAsync(callback.Id, "Сначала привяжи аккаунт.", showAlert: true, cancellationToken: cancellationToken); return; } ShopProduct product = GetShopProduct(productId); if (product == null) { await botClient.AnswerCallbackQueryAsync(callback.Id, "Товар не найден.", showAlert: true, cancellationToken: cancellationToken); return; } string paymentId = Guid.NewGuid().ToString("N"); string tag = NormalizeAccountTag(binding.AccountTag) ?? BuildAccountTag(binding.AccountId); int paymentMethodId = string.Equals(paymentMethod, "sbp", StringComparison.OrdinalIgnoreCase) ? 2 : 10; var paymentState = new PaymentState { PaymentId = paymentId, UserId = callback.From.Id, Tag = tag, ProductType = product.Type, Amount = product.Amount, PriceRub = product.PriceRub, Status = "created", CreatedAtUtc = DateTime.UtcNow }; string tgUrl = "https://t.me/"; var paymentRequest = new { paymentMethod = paymentMethodId, paymentDetails = new { amount = (float)product.PriceRub, currency = "RUB" }, description = $"Покупка {GetProductName(product.Type, product.Amount)} для аккаунта {tag}", @return = tgUrl, failedUrl = tgUrl, payload = paymentId }; using var request = new HttpRequestMessage(HttpMethod.Post, $"{GetPlategaBaseUrl()}transaction/process"); request.Headers.Add("X-MerchantId", Configuration.Instance.PlategaMerchantId); request.Headers.Add("X-Secret", Configuration.Instance.PlategaSecret); request.Content = new StringContent(System.Text.Json.JsonSerializer.Serialize(paymentRequest), Encoding.UTF8, "application/json"); HttpResponseMessage response; string responseBody; try { response = await PaymentHttpClient.SendAsync(request, cancellationToken); responseBody = await response.Content.ReadAsStringAsync(cancellationToken); } catch (Exception ex) { Logger.Exception("Failed to create Platega payment", ex); await botClient.AnswerCallbackQueryAsync(callback.Id, "Не удалось создать платёж.", showAlert: true, cancellationToken: cancellationToken); return; } if (!response.IsSuccessStatusCode) { Logger.Warning($"Platega create payment failed: {(int)response.StatusCode} {responseBody}"); await botClient.AnswerCallbackQueryAsync(callback.Id, "Platega вернула ошибку при создании платежа.", showAlert: true, cancellationToken: cancellationToken); return; } JsonElement payload; try { payload = System.Text.Json.JsonSerializer.Deserialize(responseBody); } catch (Exception ex) { Logger.Exception("Failed to parse Platega create payment response", ex); await botClient.AnswerCallbackQueryAsync(callback.Id, "Некорректный ответ от Platega.", showAlert: true, cancellationToken: cancellationToken); return; } if (payload.TryGetProperty("transactionId", out var transactionIdElement)) { paymentState.TransactionId = transactionIdElement.GetString(); } if (payload.TryGetProperty("redirect", out var redirectElement)) { paymentState.PaymentUrl = redirectElement.GetString(); } if (payload.TryGetProperty("qr", out var qrElement)) { paymentState.QrCodeUrl = qrElement.GetString(); } lock (SyncRoot) { PaymentStates[paymentId] = paymentState; SavePaymentStates(); } var buttons = new List(); if (!string.IsNullOrWhiteSpace(paymentState.PaymentUrl)) { buttons.Add(new[] { InlineKeyboardButton.WithUrl("🔗 Перейти к оплате", paymentState.PaymentUrl) }); } if (!string.IsNullOrWhiteSpace(paymentState.QrCodeUrl) && paymentMethodId == 2) { buttons.Add(new[] { InlineKeyboardButton.WithUrl("📱 QR для СБП", paymentState.QrCodeUrl) }); } buttons.Add(new[] { InlineKeyboardButton.WithCallbackData("✅ Я оплатил", $"shop:check:{paymentId}") }); buttons.Add(new[] { InlineKeyboardButton.WithCallbackData("◀️ В магазин", "shop:menu") }); await EditMenuAsync( botClient, callback, "💳 Платёж создан\n\n" + $"📦 Товар: {Escape(product.Title)}\n" + $"🏷 Аккаунт: {Escape(tag)}\n" + $"💰 Цена: {FormatShopPrice(product)}\n" + $"🆔 Платёж: {Escape(paymentId)}\n\n" + $"В Platega будет создан платёж на {product.PriceRub:0.##} 💸.\n\n" + "После оплаты нажми кнопку проверки.", new InlineKeyboardMarkup(buttons), cancellationToken); } private static async Task HandleCheckPaymentAsync(ITelegramBotClient botClient, CallbackQuery callback, string paymentId, CancellationToken cancellationToken) { PaymentState paymentState; lock (SyncRoot) { if (!PaymentStates.TryGetValue(paymentId, out paymentState) || paymentState.UserId != callback.From.Id) { paymentState = null; } } if (paymentState == null) { await botClient.AnswerCallbackQueryAsync(callback.Id, "Платёж не найден.", showAlert: true, cancellationToken: cancellationToken); return; } await botClient.AnswerCallbackQueryAsync(callback.Id, "Проверяем платёж...", cancellationToken: cancellationToken); using var request = new HttpRequestMessage(HttpMethod.Get, $"{GetPlategaBaseUrl()}transaction/{paymentState.TransactionId}"); request.Headers.Add("X-MerchantId", Configuration.Instance.PlategaMerchantId); request.Headers.Add("X-Secret", Configuration.Instance.PlategaSecret); HttpResponseMessage response; string responseBody; try { response = await PaymentHttpClient.SendAsync(request, cancellationToken); responseBody = await response.Content.ReadAsStringAsync(cancellationToken); } catch (Exception ex) { Logger.Exception("Failed to check Platega payment", ex); await botClient.AnswerCallbackQueryAsync(callback.Id, "Ошибка проверки платежа.", showAlert: true, cancellationToken: cancellationToken); return; } if (!response.IsSuccessStatusCode) { Logger.Warning($"Platega check payment failed: {(int)response.StatusCode} {responseBody}"); await botClient.AnswerCallbackQueryAsync(callback.Id, "Platega не дала статус платежа.", showAlert: true, cancellationToken: cancellationToken); return; } JsonElement payload = System.Text.Json.JsonSerializer.Deserialize(responseBody); string status = payload.TryGetProperty("status", out var statusElement) ? statusElement.GetString() ?? "unknown" : "unknown"; paymentState.Status = status; SavePaymentStates(); if (string.Equals(status, "CONFIRMED", StringComparison.OrdinalIgnoreCase)) { bool delivered = DeliverProduct(paymentState); if (!delivered) { await botClient.AnswerCallbackQueryAsync(callback.Id, "Платёж подтверждён, но зачисление не удалось.", showAlert: true, cancellationToken: cancellationToken); return; } lock (SyncRoot) { PaymentStates.Remove(paymentId); SavePaymentStates(); } await EditMenuAsync( botClient, callback, "✅ Платёж подтверждён\n\n" + $"📦 Товар: {Escape(GetProductName(paymentState.ProductType, paymentState.Amount))}\n" + $"🏷 Аккаунт: {Escape(paymentState.Tag)}\n" + $"💰 Сумма: {paymentState.PriceRub:0.##} ₽\n\n" + "Начисление выполнено.", new InlineKeyboardMarkup(new[] { new[] { InlineKeyboardButton.WithCallbackData("◀️ В магазин", "shop:menu") } }), cancellationToken); return; } await EditMenuAsync( botClient, callback, "⏳ Платёж ещё не завершён\n\n" + $"📍 Статус: {Escape(GetPaymentStatusLabel(status))}\n" + $"🆔 Платёж: {Escape(paymentId)}", new InlineKeyboardMarkup(new[] { new[] { InlineKeyboardButton.WithCallbackData("🔄 Проверить снова", $"shop:check:{paymentId}") }, new[] { InlineKeyboardButton.WithCallbackData("◀️ В магазин", "shop:menu") } }), cancellationToken); } public static async Task HandlePlategaCallbackAsync(HttpListenerContext context) { try { if (!IsPaymentConfigured()) { context.Response.StatusCode = 503; await RespondPlainAsync(context.Response, "Platega is not configured."); return; } string merchantId = context.Request.Headers["X-MerchantId"]; string secret = context.Request.Headers["X-Secret"]; if (!string.Equals(merchantId, Configuration.Instance.PlategaMerchantId, StringComparison.Ordinal) || !string.Equals(secret, Configuration.Instance.PlategaSecret, StringComparison.Ordinal)) { context.Response.StatusCode = 401; await RespondPlainAsync(context.Response, "Unauthorized"); return; } using var reader = new StreamReader(context.Request.InputStream, context.Request.ContentEncoding ?? Encoding.UTF8); string body = await reader.ReadToEndAsync(); JsonElement payload = System.Text.Json.JsonSerializer.Deserialize(body); string paymentId = payload.TryGetProperty("payload", out var paymentIdElement) ? paymentIdElement.GetString() : null; string status = payload.TryGetProperty("status", out var statusElement) ? statusElement.GetString() : null; string transactionId = payload.TryGetProperty("id", out var transactionIdElement) ? transactionIdElement.GetString() : null; if (string.IsNullOrWhiteSpace(paymentId)) { context.Response.StatusCode = 400; await RespondPlainAsync(context.Response, "Missing payload"); return; } PaymentState paymentState; lock (SyncRoot) { PaymentStates.TryGetValue(paymentId, out paymentState); } if (paymentState != null) { paymentState.Status = status ?? paymentState.Status; paymentState.TransactionId = transactionId ?? paymentState.TransactionId; if (string.Equals(status, "CONFIRMED", StringComparison.OrdinalIgnoreCase) && DeliverProduct(paymentState)) { lock (SyncRoot) { PaymentStates.Remove(paymentId); SavePaymentStates(); } if (BotClient != null) { await BotClient.SendTextMessageAsync( chatId: paymentState.UserId, text: "✅ Платёж подтверждён\n\n" + $"Товар {Escape(GetProductName(paymentState.ProductType, paymentState.Amount))} зачислен на {Escape(paymentState.Tag)}.", parseMode: ParseMode.Html); } } else { SavePaymentStates(); } } context.Response.StatusCode = 200; await RespondPlainAsync(context.Response, "OK"); } catch (Exception ex) { Logger.Exception("Platega callback failed", ex); context.Response.StatusCode = 500; await RespondPlainAsync(context.Response, "Error"); } } private static async Task RespondPlainAsync(HttpListenerResponse response, string text) { byte[] bytes = Encoding.UTF8.GetBytes(text); response.ContentType = "text/plain; charset=utf-8"; response.ContentLength64 = bytes.Length; await response.OutputStream.WriteAsync(bytes, 0, bytes.Length); response.OutputStream.Close(); } private static bool DeliverProduct(PaymentState paymentState) { long accountId = LogicLongCodeGenerator.ToId(paymentState.Tag); Account account = Accounts.Load(accountId); if (account == null) { Logger.Warning($"Platega delivery skipped: account {paymentState.Tag} not found."); return false; } switch (paymentState.ProductType) { case "gems": AdminConsoleTools.GrantCurrency(account, "gems", paymentState.Amount, "platega"); break; case "vip": case "premium": if (account.Home.PremiumEndTime < DateTime.UtcNow) { account.Home.PremiumEndTime = DateTime.UtcNow.AddMonths(paymentState.Amount); } else { account.Home.PremiumEndTime = account.Home.PremiumEndTime.AddMonths(paymentState.Amount); } account.Avatar.IsPremium = true; account.Avatar.PremiumLevel = 1; account.Avatar.AddDiamonds(170); Accounts.Save(account); AdminConsoleTools.RefreshOnlineAccount(account.AccountId, "platega-vip"); break; default: Logger.Warning($"Unsupported Platega product type: {paymentState.ProductType}"); return false; } Notification notification = new Notification { Id = 81, MessageEntry = $"Покупка {GetProductName(paymentState.ProductType, paymentState.Amount)} успешно зачислена. Спасибо за поддержку!" }; account.Home.NotificationFactory ??= new NotificationFactory(); account.Home.NotificationFactory.Add(notification); Accounts.Save(account); if (Sessions.IsSessionActive(account.AccountId)) { Sessions.GetSession(account.AccountId)?.GameListener?.SendTCPMessage(new AvailableServerCommandMessage { Command = new Supercell.Laser.Logic.Command.Home.LogicAddNotificationCommand { Notification = notification } }); } Logger.Success($"Platega delivered {paymentState.ProductType}:{paymentState.Amount} to {paymentState.Tag}"); return true; } private static string Escape(string value) { return string.IsNullOrEmpty(value) ? "-" : value .Replace("&", "&") .Replace("<", "<") .Replace(">", ">"); } private static ShopProduct GetShopProduct(string productId) { return ShopProducts.FirstOrDefault(x => string.Equals(x.Id, productId, StringComparison.OrdinalIgnoreCase)); } private static string BuildPaymentSummary(PaymentState state) { ShopProduct product = ShopProducts.FirstOrDefault(x => x.Type == state.ProductType && x.Amount == state.Amount); return $"🆔 {Escape(state.PaymentId[..Math.Min(8, state.PaymentId.Length)])}\n" + $"📦 {Escape(GetProductName(state.ProductType, state.Amount))}\n" + $"🏷 {Escape(state.Tag)}\n" + $"💰 {(product != null ? FormatShopPrice(product) : $"{state.PriceRub:0.##} 💸")}\n" + $"📍 {Escape(GetPaymentStatusLabel(state.Status))}\n" + $"🕒 {state.CreatedAtUtc:dd.MM.yyyy HH:mm} UTC"; } private static string GetPaymentStatusLabel(string status) { return (status ?? string.Empty).Trim().ToUpperInvariant() switch { "CREATED" => "создан", "PENDING" => "ожидает оплаты", "WAITING" => "ожидает подтверждения", "CONFIRMED" => "подтверждён", "FAILED" => "ошибка", "CANCELED" => "отменён", _ => string.IsNullOrWhiteSpace(status) ? "неизвестно" : status }; } private static string GetProductName(string productType, int amount) { return productType switch { "gems" => $"{amount} гемов", "premium" => $"Premium на {amount} мес.", "vip" => amount == 1 ? "VIP на 1 месяц" : $"VIP на {amount} мес.", _ => productType }; } private static string FormatShopPrice(ShopProduct product) { return $"{product.PriceRub:0.##} ₽"; } private static bool IsPaymentConfigured() { return !string.IsNullOrWhiteSpace(Configuration.Instance?.PlategaMerchantId) && !string.IsNullOrWhiteSpace(Configuration.Instance?.PlategaSecret); } private static string GetPlategaBaseUrl() { string value = Configuration.Instance?.PlategaBaseUrl; return string.IsNullOrWhiteSpace(value) ? "https://app.platega.io/" : value.TrimEnd('/') + "/"; } private static void LoadPaymentStates() { lock (SyncRoot) { if (!System.IO.File.Exists(PaymentsPath)) { PaymentStates.Clear(); return; } try { var loaded = System.Text.Json.JsonSerializer.Deserialize>(System.IO.File.ReadAllText(PaymentsPath)) ?? new Dictionary(StringComparer.OrdinalIgnoreCase); PaymentStates.Clear(); foreach (var pair in loaded) { PaymentStates[pair.Key] = pair.Value; } } catch (Exception ex) { Logger.Exception("Failed to load Telegram payment states", ex); PaymentStates.Clear(); } } } private static void SavePaymentStates() { lock (SyncRoot) { System.IO.File.WriteAllText(PaymentsPath, System.Text.Json.JsonSerializer.Serialize(PaymentStates, new JsonSerializerOptions { WriteIndented = true })); } } private static void LoadCreatorApplications() { lock (SyncRoot) { if (!System.IO.File.Exists(CreatorApplicationsPath)) { CreatorApplications.Clear(); NextCreatorApplicationId = 1; return; } try { var loaded = System.Text.Json.JsonSerializer.Deserialize>(System.IO.File.ReadAllText(CreatorApplicationsPath)) ?? new Dictionary(); CreatorApplications.Clear(); foreach (var pair in loaded) { CreatorApplications[pair.Key] = pair.Value; } NextCreatorApplicationId = CreatorApplications.Count == 0 ? 1 : CreatorApplications.Keys.Max() + 1; } catch (Exception ex) { Logger.Exception("Failed to load creator applications", ex); CreatorApplications.Clear(); NextCreatorApplicationId = 1; } } } private static void SaveCreatorApplications() { lock (SyncRoot) { System.IO.File.WriteAllText(CreatorApplicationsPath, System.Text.Json.JsonSerializer.Serialize(CreatorApplications, new JsonSerializerOptions { WriteIndented = true })); } } private static void LoadCreatorProfiles() { lock (SyncRoot) { if (!System.IO.File.Exists(CreatorProfilesPath)) { CreatorProfiles.Clear(); return; } try { var loaded = System.Text.Json.JsonSerializer.Deserialize>(System.IO.File.ReadAllText(CreatorProfilesPath)) ?? new Dictionary(); CreatorProfiles.Clear(); foreach (var pair in loaded) { CreatorProfiles[pair.Key] = pair.Value; } } catch (Exception ex) { Logger.Exception("Failed to load creator profiles", ex); CreatorProfiles.Clear(); } } } private static void SaveCreatorProfiles() { lock (SyncRoot) { System.IO.File.WriteAllText(CreatorProfilesPath, System.Text.Json.JsonSerializer.Serialize(CreatorProfiles, new JsonSerializerOptions { WriteIndented = true })); } } private static void LoadCreatorRewardRequests() { lock (SyncRoot) { if (!System.IO.File.Exists(CreatorRewardRequestsPath)) { CreatorRewardRequests.Clear(); NextCreatorRewardRequestId = 1; return; } try { var loaded = System.Text.Json.JsonSerializer.Deserialize>(System.IO.File.ReadAllText(CreatorRewardRequestsPath)) ?? new Dictionary(); CreatorRewardRequests.Clear(); foreach (var pair in loaded) { CreatorRewardRequests[pair.Key] = pair.Value; } NextCreatorRewardRequestId = CreatorRewardRequests.Count == 0 ? 1 : CreatorRewardRequests.Keys.Max() + 1; } catch (Exception ex) { Logger.Exception("Failed to load creator reward requests", ex); CreatorRewardRequests.Clear(); NextCreatorRewardRequestId = 1; } } } private static void SaveCreatorRewardRequests() { lock (SyncRoot) { System.IO.File.WriteAllText(CreatorRewardRequestsPath, System.Text.Json.JsonSerializer.Serialize(CreatorRewardRequests, new JsonSerializerOptions { WriteIndented = true })); } } private static void LoadCreatorUpgradeRequests() { lock (SyncRoot) { if (!System.IO.File.Exists(CreatorUpgradeRequestsPath)) { CreatorUpgradeRequests.Clear(); NextCreatorUpgradeRequestId = 1; return; } try { var loaded = System.Text.Json.JsonSerializer.Deserialize>(System.IO.File.ReadAllText(CreatorUpgradeRequestsPath)) ?? new Dictionary(); CreatorUpgradeRequests.Clear(); foreach (var pair in loaded) { CreatorUpgradeRequests[pair.Key] = pair.Value; } NextCreatorUpgradeRequestId = CreatorUpgradeRequests.Count == 0 ? 1 : CreatorUpgradeRequests.Keys.Max() + 1; } catch (Exception ex) { Logger.Exception("Failed to load creator upgrade requests", ex); CreatorUpgradeRequests.Clear(); NextCreatorUpgradeRequestId = 1; } } } private static void SaveCreatorUpgradeRequests() { lock (SyncRoot) { System.IO.File.WriteAllText(CreatorUpgradeRequestsPath, System.Text.Json.JsonSerializer.Serialize(CreatorUpgradeRequests, new JsonSerializerOptions { WriteIndented = true })); } } private static void PushNotification(HomeMode homeMode, string message) { Notification notification = new Notification { Id = 81, MessageEntry = $"{message}" }; homeMode.Home.NotificationFactory.Add(notification); homeMode.GameListener?.SendTCPMessage(new AvailableServerCommandMessage { Command = new Supercell.Laser.Logic.Command.Home.LogicAddNotificationCommand { Notification = notification } }); } private static void EnsureStorage() { Directory.CreateDirectory(JsonDirectory); EnsureFile(BindingsPath); EnsureFile(PendingPath); EnsureFile(PaymentsPath, "{}"); EnsureFile(CreatorApplicationsPath, "{}"); EnsureFile(CreatorProfilesPath, "{}"); EnsureFile(CreatorRewardRequestsPath, "{}"); EnsureFile(CreatorUpgradeRequestsPath, "{}"); } private static void EnsureFile(string path, string defaultContent = "[]") { if (!System.IO.File.Exists(path)) { System.IO.File.WriteAllText(path, defaultContent); } } private static List LoadBindings() { EnsureStorage(); return JsonConvert.DeserializeObject>(System.IO.File.ReadAllText(BindingsPath)) ?? new List(); } private static void SaveBindings(List bindings) { EnsureStorage(); System.IO.File.WriteAllText(BindingsPath, JsonConvert.SerializeObject(bindings, Formatting.Indented)); } private static List LoadPendingLinks() { EnsureStorage(); return JsonConvert.DeserializeObject>(System.IO.File.ReadAllText(PendingPath)) ?? new List(); } private static void SavePendingLinks(List links) { EnsureStorage(); System.IO.File.WriteAllText(PendingPath, JsonConvert.SerializeObject(links, Formatting.Indented)); } public sealed class TelegramBindingRecord { [JsonProperty] public long TelegramId { get; set; } [JsonProperty] public string TelegramUsername { get; set; } [JsonProperty] public long AccountId { get; set; } [JsonProperty] public string AccountName { get; set; } [JsonProperty] public string AccountTag { get; set; } [JsonProperty] public DateTime BoundAtUtc { get; set; } [JsonProperty] public DateTime? LastLoginAtUtc { get; set; } } private sealed class PendingLinkRecord { [JsonProperty] public string Token { get; set; } [JsonProperty] public string Mode { get; set; } [JsonProperty] public long TelegramId { get; set; } [JsonProperty] public string TelegramUsername { get; set; } [JsonProperty] public long AccountId { get; set; } [JsonProperty] public string AccountTag { get; set; } [JsonProperty] public DateTime CreatedAtUtc { get; set; } [JsonProperty] public DateTime ExpiresAtUtc { get; set; } [JsonProperty] public bool Consumed { get; set; } } private sealed class AdminInputState { public string Mode { get; set; } public VoucherDraft Voucher { get; set; } public long PendingCreatorUserId { get; set; } public long PendingRewardRequestId { get; set; } } private sealed class VoucherDraft { public string Code { get; set; } public List Rewards { get; } = new List(); } private sealed class PaymentState { public string PaymentId { get; set; } public long UserId { get; set; } public string Tag { get; set; } public string ProductType { get; set; } public int Amount { get; set; } public decimal PriceRub { get; set; } public string TransactionId { get; set; } public string Status { get; set; } public DateTime CreatedAtUtc { get; set; } public string PaymentUrl { get; set; } public string QrCodeUrl { get; set; } } private sealed class CreatorConversationState { public string Mode { get; set; } public CreatorApplicationRecord DraftApplication { get; set; } public CreatorRewardRequest DraftReward { get; set; } public CreatorUpgradeRequest DraftUpgrade { get; set; } } private sealed class CreatorApplicationRecord { public long ApplicationId { get; set; } public long UserId { get; set; } public string TelegramUsername { get; set; } public long AccountId { get; set; } public string AccountTag { get; set; } public string AccountName { get; set; } public string CreatorCode { get; set; } public string PlatformName { get; set; } public string PlatformUrl { get; set; } public string Note { get; set; } public DateTime CreatedAtUtc { get; set; } public DateTime? ReviewedAtUtc { get; set; } public string Status { get; set; } } private sealed class CreatorProfile { public long UserId { get; set; } public long AccountId { get; set; } public string AccountTag { get; set; } public string AccountName { get; set; } public string CreatorCode { get; set; } public string PlatformName { get; set; } public string PlatformUrl { get; set; } public CreatorTier Tier { get; set; } public int Elixirs { get; set; } public DateTime JoinedAtUtc { get; set; } public bool IsActive { get; set; } public int RewardApprovals { get; set; } public int RewardRejections { get; set; } } private sealed class CreatorRewardRequest { public long RequestId { get; set; } public long UserId { get; set; } public string CreatorCode { get; set; } public string AccountTag { get; set; } public string PlatformName { get; set; } public string VideoUrl { get; set; } public string Comment { get; set; } public DateTime CreatedAtUtc { get; set; } public DateTime? ReviewedAtUtc { get; set; } public long ReviewedByTelegramId { get; set; } public string Status { get; set; } public string Decision { get; set; } } private sealed class CreatorUpgradeRequest { public long RequestId { get; set; } public long UserId { get; set; } public string CreatorCode { get; set; } public string AccountTag { get; set; } public CreatorTier CurrentTier { get; set; } public int SubscribersCount { get; set; } public int AverageViews { get; set; } public int BestViews { get; set; } public string Reason { get; set; } public string AdditionalInfo { get; set; } public DateTime CreatedAtUtc { get; set; } public DateTime? ReviewedAtUtc { get; set; } public long ReviewedByTelegramId { get; set; } public string Status { get; set; } public string Decision { get; set; } } private enum CreatorTier { Ordinary = 0, SuperCreator = 1 } private static class CreatorRecordStatus { public const string Pending = "pending"; public const string Approved = "approved"; public const string Rejected = "rejected"; } private sealed record ShopProduct(string Id, string Type, int Amount, decimal PriceRub, string Title); private sealed record CreatorShopReward(string Id, string Title, int ElixirCost, IReadOnlyList VoucherRewards, string Icon, string Category, int Tier); } } // private static void UpsertCreatorCodeFile(CreatorProfile creator) // { // string filePath = Supercell.Laser.Logic.Util.LogicHelperConfig.creatorPath; // Directory.CreateDirectory(Path.GetDirectoryName(filePath) ?? AppContext.BaseDirectory); // JArray creators = System.IO.File.Exists(filePath) // ? JArray.Parse(System.IO.File.ReadAllText(filePath)) // : new JArray(); // JToken existing = creators.FirstOrDefault(x => // string.Equals(x["Code"]?.ToString(), creator.CreatorCode, StringComparison.OrdinalIgnoreCase) || // string.Equals(x["Tag"]?.ToString(), creator.AccountTag, StringComparison.OrdinalIgnoreCase)); // JObject entry = new JObject // { // ["Tag"] = creator.AccountTag, // ["Code"] = creator.CreatorCode, // ["Gems"] = creator.Tier == CreatorTier.SuperCreator ? 50 : 30 // }; // if (existing is JObject existingObject) // { // existingObject.Replace(entry); // } // else // { // creators.Add(entry); // } // System.IO.File.WriteAllText(filePath, creators.ToString()); // }