using Microsoft.Extensions.Options; using PnvPanel.Application.Admin.Activation; using PnvPanel.Application.Admin.Billing; using PnvPanel.Application.Admin.Support; using PnvPanel.Application.Billing; using PnvPanel.Application.Billing.GetMyBillingStatus; using PnvPanel.Application.Common.Interfaces; using LiteCqrs; using PnvPanel.Application.Configs.GetConfigLink; using PnvPanel.Application.Configs.GetMyConfigs; using PnvPanel.Application.Telegram; using PnvPanel.Application.Telegram.Bot; using PnvPanel.Domain.Activation; using PnvPanel.Domain.Configs; using PnvPanel.Infrastructure.Telegram; using Telegram.Bot; using Telegram.Bot.Polling; using Telegram.Bot.Types; using Telegram.Bot.Types.Enums; using Telegram.Bot.Types.ReplyMarkups; namespace PnvPanel.Api.Telegram; /// /// Обрабатывает апдейты бота. Каждый апдейт — свой DI-scope (как HTTP-запрос), чтобы получить /// свежие scoped-сервисы (ISender, ICurrentUserSetter, ...). Бот — read-only по конфигам в MVP. /// public sealed class PnvBotUpdateHandler( IServiceScopeFactory scopeFactory, IOptions options, ILogger logger ) : IUpdateHandler { // Показывается везде, где боту нужен привязанный аккаунт, а его нет — явно проговариваем оба шага, // иначе новые пользователи не понимают, что сначала нужен обычный аккаунт на сайте. private const string NotLinkedMessage = "Сначала зарегистрируйтесь и войдите на сайте, затем привяжите Telegram: Настройки → «Привязать Telegram»."; public async Task HandleUpdateAsync( ITelegramBotClient botClient, Update update, CancellationToken cancellationToken ) { await using var scope = scopeFactory.CreateAsyncScope(); try { if (update.Message is { Text: { } text } message) await HandleMessageAsync( botClient, scope.ServiceProvider, message, text, cancellationToken ); else if (update.CallbackQuery is { } callback) await HandleCallbackAsync( botClient, scope.ServiceProvider, callback, cancellationToken ); } catch (Exception ex) { logger.LogError(ex, "Failed to process Telegram update {UpdateId}", update.Id); } } public Task HandleErrorAsync( ITelegramBotClient botClient, Exception exception, HandleErrorSource source, CancellationToken cancellationToken ) { logger.LogError(exception, "Telegram bot error (source {Source})", source); return Task.CompletedTask; } private async Task HandleMessageAsync( ITelegramBotClient botClient, IServiceProvider services, Message message, string text, CancellationToken cancellationToken ) { var chatId = message.Chat.Id; var fromId = message.From?.Id; if (fromId is null) return; if (text.StartsWith("/start", StringComparison.Ordinal)) { var payload = text.Length > 7 ? text[7..].Trim() : string.Empty; if (payload.StartsWith("link_", StringComparison.Ordinal)) await HandleLinkAsync( botClient, services, chatId, fromId.Value, message.From?.Username, payload[5..], cancellationToken ); else if (payload.StartsWith("login_", StringComparison.Ordinal)) await HandleLoginPromptAsync( botClient, services, chatId, fromId.Value, payload[6..], cancellationToken ); else await SendWelcomeAsync( botClient, services, chatId, fromId.Value, cancellationToken ); return; } switch (text) { case "/configs": await HandleConfigsAsync( botClient, services, chatId, fromId.Value, cancellationToken ); break; case "/billing": await HandleBillingStatusAsync( botClient, services, chatId, fromId.Value, cancellationToken ); break; case "/unlink": await HandleUnlinkAsync( botClient, services, chatId, fromId.Value, cancellationToken ); break; case "/requests": await HandleRequestsAsync( botClient, services, chatId, fromId.Value, cancellationToken ); break; case "/help": await SendWelcomeAsync( botClient, services, chatId, fromId.Value, cancellationToken ); break; default: await botClient.SendMessage( chatId, "Не понимаю эту команду. /help — список команд.", cancellationToken: cancellationToken ); break; } } private async Task HandleCallbackAsync( ITelegramBotClient botClient, IServiceProvider services, CallbackQuery callback, CancellationToken cancellationToken ) { var data = callback.Data; var chatId = callback.Message?.Chat.Id; var fromId = callback.From.Id; if (data is null || chatId is null) return; if (data == "reg:new") { await HandleRegisterCallbackAsync( botClient, services, chatId.Value, fromId, callback.From.Username, callback.Id, cancellationToken ); return; } if (data == "menu:configs") { if (!await TrySetCurrentUserAsync(services, fromId, cancellationToken)) { await botClient.AnswerCallbackQuery( callback.Id, "Telegram не привязан.", cancellationToken: cancellationToken ); return; } await botClient.AnswerCallbackQuery(callback.Id, cancellationToken: cancellationToken); var (configsText, configsKeyboard) = await BuildConfigsMenuAsync( services, cancellationToken ); if (callback.Message is not null) await botClient.EditMessageText( chatId.Value, callback.Message.Id, configsText, replyMarkup: configsKeyboard, cancellationToken: cancellationToken ); return; } if (data == "menu:billing") { if (!await TrySetCurrentUserAsync(services, fromId, cancellationToken)) { await botClient.AnswerCallbackQuery( callback.Id, "Telegram не привязан.", cancellationToken: cancellationToken ); return; } await botClient.AnswerCallbackQuery(callback.Id, cancellationToken: cancellationToken); var (billingText, billingKeyboard) = await BuildBillingStatusMenuAsync( services, cancellationToken ); if (callback.Message is not null) await botClient.EditMessageText( chatId.Value, callback.Message.Id, billingText, replyMarkup: billingKeyboard, cancellationToken: cancellationToken ); return; } if (data == "menu:back") { await botClient.AnswerCallbackQuery(callback.Id, cancellationToken: cancellationToken); if (callback.Message is null) return; var identityService = services.GetRequiredService(); var linkedUserId = await identityService.FindUserIdByTelegramUserIdAsync( fromId, cancellationToken ); var linkedProfile = linkedUserId is { } uid ? await identityService.GetProfileAsync(uid, cancellationToken) : null; var (menuText, menuKeyboard) = BuildMainMenu( isLinked: linkedUserId is not null, billingEnabled: linkedProfile?.BillingEnabled ?? false ); await botClient.EditMessageText( chatId.Value, callback.Message.Id, menuText, replyMarkup: menuKeyboard, cancellationToken: cancellationToken ); return; } if (data == "menu:unlink") { if (!await TrySetCurrentUserAsync(services, fromId, cancellationToken)) { await botClient.AnswerCallbackQuery( callback.Id, "Telegram не привязан.", cancellationToken: cancellationToken ); return; } var unlinkSender = services.GetRequiredService(); var unlinkResult = await unlinkSender.Send( new UnlinkTelegramCommand(), cancellationToken ); await botClient.AnswerCallbackQuery(callback.Id, cancellationToken: cancellationToken); if (callback.Message is null) return; if (!unlinkResult.IsSuccess) { await botClient.EditMessageText( chatId.Value, callback.Message.Id, $"❌ Ошибка: {unlinkResult.Error.Message}", replyMarkup: BackToMenuKeyboard(), cancellationToken: cancellationToken ); return; } var (unlinkedText, unlinkedKeyboard) = BuildMainMenu(isLinked: false); await botClient.EditMessageText( chatId.Value, callback.Message.Id, "✅ Telegram отвязан от аккаунта.\n\n" + unlinkedText, replyMarkup: unlinkedKeyboard, cancellationToken: cancellationToken ); return; } var parts = data.Split(':'); if (parts.Length != 3 || !Guid.TryParse(parts[2], out var requestId)) return; var sender = services.GetRequiredService(); switch (parts[0]) { case "login": { var result = parts[1] == "approve" ? await sender.Send( new ApproveTelegramLoginCommand(requestId, fromId), cancellationToken ) : await sender.Send( new RejectTelegramLoginCommand(requestId, fromId), cancellationToken ); await botClient.AnswerCallbackQuery( callback.Id, result.IsSuccess ? "Готово" : result.Error.Message, cancellationToken: cancellationToken ); if (callback.Message is not null) { var statusText = result.IsSuccess ? (parts[1] == "approve" ? "✅ Вход подтверждён." : "❌ Вход отклонён.") : $"⚠️ {result.Error.Message}"; var text = $"{Escape(callback.Message.Text ?? "")}\n\n{statusText}"; await botClient.EditMessageText( chatId.Value, callback.Message.Id, text, parseMode: ParseMode.Html, cancellationToken: cancellationToken ); } break; } case "act": { if (!await TrySetAdminCurrentUserAsync(services, fromId, cancellationToken)) { await botClient.AnswerCallbackQuery( callback.Id, "Недостаточно прав.", cancellationToken: cancellationToken ); return; } var result = parts[1] == "approve" ? await sender.Send( new ApproveActivationCommand(requestId), cancellationToken ) : await sender.Send( new RejectActivationCommand(requestId, Reason: null), cancellationToken ); await botClient.AnswerCallbackQuery( callback.Id, result.IsSuccess ? "Готово" : result.Error.Message, cancellationToken: cancellationToken ); if (callback.Message is not null) { // Редактируем исходное сообщение с запросом вместо отдельного — иначе кнопки // «Активировать/Отклонить» остаются висеть под уже обработанным запросом (в т.ч. // если его обработали в другом месте — на сайте или из другого чата). var statusText = result.IsSuccess ? ( parts[1] == "approve" ? "✅ Пользователь активирован." : "❌ Запрос отклонён." ) : $"⚠️ {result.Error.Message}"; var text = $"{Escape(callback.Message.Text ?? "")}\n\n{statusText}"; await botClient.EditMessageText( chatId.Value, callback.Message.Id, text, parseMode: ParseMode.Html, cancellationToken: cancellationToken ); } break; } case "erq": { if (!await TrySetAdminCurrentUserAsync(services, fromId, cancellationToken)) { await botClient.AnswerCallbackQuery( callback.Id, "Недостаточно прав.", cancellationToken: cancellationToken ); return; } var result = parts[1] == "approve" ? await sender.Send( new ApproveExtensionRequestCommand(requestId), cancellationToken ) : await sender.Send( new RejectExtensionRequestCommand(requestId, Reason: null), cancellationToken ); await botClient.AnswerCallbackQuery( callback.Id, result.IsSuccess ? "Готово" : result.Error.Message, cancellationToken: cancellationToken ); if (callback.Message is not null) { var statusText = result.IsSuccess ? ( parts[1] == "approve" ? "✅ Заявка на продление одобрена." : "❌ Заявка отклонена." ) : $"⚠️ {result.Error.Message}"; var text = $"{Escape(callback.Message.Text ?? "")}\n\n{statusText}"; await botClient.EditMessageText( chatId.Value, callback.Message.Id, text, parseMode: ParseMode.Html, cancellationToken: cancellationToken ); } break; } case "pay": { if (!await TrySetAdminCurrentUserAsync(services, fromId, cancellationToken)) { await botClient.AnswerCallbackQuery( callback.Id, "Недостаточно прав.", cancellationToken: cancellationToken ); return; } var result = parts[1] == "approve" ? await sender.Send( new ConfirmPaymentRequestCommand(requestId), cancellationToken ) : await sender.Send( new RejectPaymentRequestCommand(requestId, Reason: null), cancellationToken ); await botClient.AnswerCallbackQuery( callback.Id, result.IsSuccess ? "Готово" : result.Error.Message, cancellationToken: cancellationToken ); if (callback.Message is not null) { var statusText = result.IsSuccess ? ( parts[1] == "approve" ? "✅ Оплата подтверждена." : "❌ Оплата отклонена." ) : $"⚠️ {result.Error.Message}"; var text = $"{Escape(callback.Message.Text ?? "")}\n\n{statusText}"; await botClient.EditMessageText( chatId.Value, callback.Message.Id, text, parseMode: ParseMode.Html, cancellationToken: cancellationToken ); } break; } case "cfg": { if (!await TrySetCurrentUserAsync(services, fromId, cancellationToken)) { await botClient.AnswerCallbackQuery( callback.Id, "Telegram не привязан.", cancellationToken: cancellationToken ); return; } var linkResult = await sender.Send( new GetConfigLinkQuery(requestId), cancellationToken ); await botClient.AnswerCallbackQuery( callback.Id, cancellationToken: cancellationToken ); if (!linkResult.IsSuccess) { await botClient.SendMessage( chatId.Value, $"Не удалось получить ссылку: {linkResult.Error.Message}", cancellationToken: cancellationToken ); return; } if (callback.Message is null) break; // Редактируем то же сообщение (не плодим отдельное с сырым URL) — ссылка моноширинным // блоком, по нему в Telegram можно тапнуть и скопировать целиком одним движением. // Убираем именно эту кнопку из клавиатуры — остальные конфиги и «В меню» остаются на месте. var text = $"{Escape(callback.Message.Text ?? "")}\n\n{Escape(linkResult.Value.ConnectionString)}"; var remainingRows = (callback.Message.ReplyMarkup?.InlineKeyboard ?? []) .Where(row => row.All(b => b.CallbackData != data)) .ToArray(); await botClient.EditMessageText( chatId.Value, callback.Message.Id, text, parseMode: ParseMode.Html, replyMarkup: remainingRows.Length > 0 ? new InlineKeyboardMarkup(remainingRows) : null, cancellationToken: cancellationToken ); break; } } } private async Task HandleLinkAsync( ITelegramBotClient botClient, IServiceProvider services, long chatId, long fromId, string? username, string token, CancellationToken cancellationToken ) { var sender = services.GetRequiredService(); var result = await sender.Send( new LinkTelegramCommand(token, fromId, username), cancellationToken ); var text = result.IsSuccess ? "✅ Telegram успешно привязан к вашему аккаунту." : $"❌ Не удалось привязать: {result.Error.Message}"; await botClient.SendMessage(chatId, text, cancellationToken: cancellationToken); } private async Task HandleLoginPromptAsync( ITelegramBotClient botClient, IServiceProvider services, long chatId, long fromId, string requestIdRaw, CancellationToken cancellationToken ) { if (!Guid.TryParse(requestIdRaw, out var requestId)) { await botClient.SendMessage( chatId, "Некорректная ссылка входа.", cancellationToken: cancellationToken ); return; } var identityService = services.GetRequiredService(); var userId = await identityService.FindUserIdByTelegramUserIdAsync( fromId, cancellationToken ); if (userId is null) { await botClient.SendMessage( chatId, NotLinkedMessage, replyMarkup: new InlineKeyboardMarkup( new[] { InlineKeyboardButton.WithCallbackData("📝 Зарегистрироваться", "reg:new"), } ), cancellationToken: cancellationToken ); return; } var keyboard = new InlineKeyboardMarkup( new[] { InlineKeyboardButton.WithCallbackData( "✅ Подтвердить вход", $"login:approve:{requestId}" ), InlineKeyboardButton.WithCallbackData("❌ Отклонить", $"login:reject:{requestId}"), } ); await botClient.SendMessage( chatId, "Кто-то пытается войти в PnvPanel через ваш аккаунт. Подтвердить вход?", replyMarkup: keyboard, cancellationToken: cancellationToken ); } private async Task HandleConfigsAsync( ITelegramBotClient botClient, IServiceProvider services, long chatId, long fromId, CancellationToken cancellationToken ) { if (!await TrySetCurrentUserAsync(services, fromId, cancellationToken)) { await botClient.SendMessage( chatId, NotLinkedMessage, replyMarkup: new InlineKeyboardMarkup( new[] { InlineKeyboardButton.WithCallbackData("📝 Зарегистрироваться", "reg:new"), } ), cancellationToken: cancellationToken ); return; } var (text, keyboard) = await BuildConfigsMenuAsync(services, cancellationToken); await botClient.SendMessage( chatId, text, replyMarkup: keyboard, cancellationToken: cancellationToken ); } /// Список конфигов одним сообщением: строка на конфиг + кнопка «🔗 {Label}» на каждый /// не отозванный, плюс «🔙 В меню» внизу. private static async Task<(string Text, InlineKeyboardMarkup Keyboard)> BuildConfigsMenuAsync( IServiceProvider services, CancellationToken cancellationToken ) { var sender = services.GetRequiredService(); var result = await sender.Send(new GetMyConfigsQuery(), cancellationToken); if (!result.IsSuccess || result.Value.Configs.Count == 0) return ("У вас пока нет конфигов.", BackToMenuKeyboard()); var text = "Ваши конфиги:\n" + string.Join( '\n', result.Value.Configs.Select(c => $"• {c.Label ?? c.Location} ({c.Protocol}) — {c.Status}" ) ); // Отозванному конфигу нечего показывать — кнопку не даём. var rows = result .Value.Configs.Where(c => c.Status != ConfigStatus.Revoked) .Select(c => new[] { InlineKeyboardButton.WithCallbackData( $"🔗 {c.Label ?? c.Location}", $"cfg:link:{c.Id}" ), } ) .Append(BackToMenuRow()) .ToArray(); return (text, new InlineKeyboardMarkup(rows)); } private async Task HandleBillingStatusAsync( ITelegramBotClient botClient, IServiceProvider services, long chatId, long fromId, CancellationToken cancellationToken ) { if (!await TrySetCurrentUserAsync(services, fromId, cancellationToken)) { await botClient.SendMessage(chatId, NotLinkedMessage, cancellationToken: cancellationToken); return; } var (text, keyboard) = await BuildBillingStatusMenuAsync(services, cancellationToken); await botClient.SendMessage( chatId, text, replyMarkup: keyboard, cancellationToken: cancellationToken ); } /// До какого числа оплачено + остаток в человекочитаемом виде (дни, либо часы/минуты, /// если меньше суток) — то же форматирование, что и на сайте (PaidUntilBadge). private static async Task<(string Text, InlineKeyboardMarkup Keyboard)> BuildBillingStatusMenuAsync( IServiceProvider services, CancellationToken cancellationToken ) { var sender = services.GetRequiredService(); var result = await sender.Send(new GetMyBillingStatusQuery(), cancellationToken); if (!result.IsSuccess) return ($"⚠️ {result.Error.Message}", BackToMenuKeyboard()); var status = result.Value; if (!status.BillingEnabled) return ("Биллинг не применяется к вашей роли.", BackToMenuKeyboard()); if (status.PaidUntil is not { } paidUntil) return ("Оплата ещё не производилась.", BackToMenuKeyboard()); var text = $"💳 Оплачено до {paidUntil:dd.MM.yyyy HH:mm}\nОсталось: {FormatRemaining(paidUntil)}" + (status.Suspended ? "\n\n⛔ Конфиги приостановлены за неуплату." : ""); return (text, BackToMenuKeyboard()); } private static string FormatRemaining(DateTimeOffset paidUntil) { var remaining = paidUntil - DateTimeOffset.UtcNow; if (remaining <= TimeSpan.Zero) return "истекло"; if (remaining < TimeSpan.FromDays(1)) { var totalMinutes = (int)Math.Ceiling(remaining.TotalMinutes); var hours = totalMinutes / 60; var minutes = totalMinutes % 60; return hours > 0 ? $"{hours} ч {minutes} мин" : $"{minutes} мин"; } var days = (int)Math.Ceiling(remaining.TotalDays); return $"{days} дн."; } private static InlineKeyboardButton[] BackToMenuRow() => new[] { InlineKeyboardButton.WithCallbackData("🔙 В меню", "menu:back") }; private static InlineKeyboardMarkup BackToMenuKeyboard() => new(new[] { BackToMenuRow() }); private async Task HandleUnlinkAsync( ITelegramBotClient botClient, IServiceProvider services, long chatId, long fromId, CancellationToken cancellationToken ) { if (!await TrySetCurrentUserAsync(services, fromId, cancellationToken)) { await botClient.SendMessage( chatId, "Telegram не привязан.", cancellationToken: cancellationToken ); return; } var sender = services.GetRequiredService(); var result = await sender.Send(new UnlinkTelegramCommand(), cancellationToken); await botClient.SendMessage( chatId, result.IsSuccess ? "Telegram отвязан от аккаунта." : $"Ошибка: {result.Error.Message}", cancellationToken: cancellationToken ); } private async Task HandleRequestsAsync( ITelegramBotClient botClient, IServiceProvider services, long chatId, long fromId, CancellationToken cancellationToken ) { if (!await TrySetAdminCurrentUserAsync(services, fromId, cancellationToken)) { await botClient.SendMessage( chatId, "Недостаточно прав.", cancellationToken: cancellationToken ); return; } var sender = services.GetRequiredService(); var result = await sender.Send( new ListActivationRequestsQuery(ActivationStatus.Pending, 1, 10), cancellationToken ); if (!result.IsSuccess || result.Value.Items.Count == 0) { await botClient.SendMessage( chatId, "Нет ожидающих запросов на активацию.", cancellationToken: cancellationToken ); return; } foreach (var item in result.Value.Items) { var text = $"Запрос от {item.UserName}" + (string.IsNullOrWhiteSpace(item.Comment) ? "" : $"\n{item.Comment}"); var keyboard = new InlineKeyboardMarkup( new[] { InlineKeyboardButton.WithCallbackData( "✅ Активировать", $"act:approve:{item.Id}" ), InlineKeyboardButton.WithCallbackData("❌ Отклонить", $"act:reject:{item.Id}"), } ); await botClient.SendMessage( chatId, text, parseMode: ParseMode.Html, replyMarkup: keyboard, cancellationToken: cancellationToken ); } } private async Task SendWelcomeAsync( ITelegramBotClient botClient, IServiceProvider services, long chatId, long fromId, CancellationToken cancellationToken ) { var identityService = services.GetRequiredService(); var userId = await identityService.FindUserIdByTelegramUserIdAsync( fromId, cancellationToken ); var profile = userId is { } uid ? await identityService.GetProfileAsync(uid, cancellationToken) : null; var (text, keyboard) = BuildMainMenu( isLinked: userId is not null, billingEnabled: profile?.BillingEnabled ?? false ); await botClient.SendMessage( chatId, text, replyMarkup: keyboard, cancellationToken: cancellationToken ); } /// Привязанному аккаунту — кнопки-действия вместо текстовых команд; непривязанному — /// только регистрация (остальное ему всё равно недоступно). Кнопка на сайт — если задан PublicSiteUrl. /// billingEnabled — показать «Статус оплаты» только для billing-ролей (см. AppRole.BillingEnabled). private (string Text, InlineKeyboardMarkup Keyboard) BuildMainMenu(bool isLinked, bool billingEnabled = false) { const string text = "Привет! Это бот PnvPanel.\n\n" + "Вход без пароля запускается кнопкой «Войти через Telegram» на сайте — бот пришлёт запрос на подтверждение."; var rows = new List(); if (isLinked) { rows.Add( new[] { InlineKeyboardButton.WithCallbackData("📋 Мои конфиги", "menu:configs") } ); if (billingEnabled) rows.Add( new[] { InlineKeyboardButton.WithCallbackData("💳 Статус оплаты", "menu:billing"), } ); rows.Add( new[] { InlineKeyboardButton.WithCallbackData("🔓 Отвязать Telegram", "menu:unlink"), } ); } else { rows.Add( new[] { InlineKeyboardButton.WithCallbackData("📝 Зарегистрироваться", "reg:new") } ); } if (!string.IsNullOrWhiteSpace(options.Value.PublicSiteUrl)) rows.Add( new[] { InlineKeyboardButton.WithUrl("🌐 Сайт панели", options.Value.PublicSiteUrl), } ); return (text, new InlineKeyboardMarkup(rows)); } private static async Task HandleRegisterCallbackAsync( ITelegramBotClient botClient, IServiceProvider services, long chatId, long fromId, string? username, string callbackId, CancellationToken cancellationToken ) { var sender = services.GetRequiredService(); var result = await sender.Send( new RegisterViaTelegramCommand(fromId, username), cancellationToken ); await botClient.AnswerCallbackQuery(callbackId, cancellationToken: cancellationToken); if (!result.IsSuccess) { await botClient.SendMessage( chatId, $"Не удалось зарегистрироваться: {result.Error.Message}", cancellationToken: cancellationToken ); return; } var text = "✅ Аккаунт создан.\n\n" + $"Логин: {result.Value.UserName}\n" + $"Пароль: {result.Value.Password}\n\n" + "Сохраните пароль — он присылается только один раз. Логин можно сменить в Настройках на сайте.\n\n" + "Дальше нужно дождаться активации администратором — после неё будут доступны конфиги. " + "Входить можно как по паролю, так и кнопкой «Войти через Telegram»."; await botClient.SendMessage( chatId, text, parseMode: ParseMode.Html, cancellationToken: cancellationToken ); } private static string Escape(string text) => text.Replace("&", "&").Replace("<", "<").Replace(">", ">"); private static async Task TrySetCurrentUserAsync( IServiceProvider services, long telegramUserId, CancellationToken cancellationToken ) { var identityService = services.GetRequiredService(); var userId = await identityService.FindUserIdByTelegramUserIdAsync( telegramUserId, cancellationToken ); if (userId is null) return false; var profile = await identityService.GetProfileAsync(userId.Value, cancellationToken); if (profile is null) return false; services.GetRequiredService().SetUser(profile.Id, profile.UserName); return true; } private async Task TrySetAdminCurrentUserAsync( IServiceProvider services, long telegramUserId, CancellationToken cancellationToken ) { if (!options.Value.ParseAdminTelegramUserIds().Contains(telegramUserId)) return false; return await TrySetCurrentUserAsync(services, telegramUserId, cancellationToken); } }