using Microsoft.Extensions.Options; using PnvPanel.Application.Common.Interfaces; using PnvPanel.Domain.Billing; using PnvPanel.Domain.Nodes; using PnvPanel.Domain.Support; using PnvPanel.Infrastructure.Telegram; using Telegram.Bot; using Telegram.Bot.Types.Enums; using Telegram.Bot.Types.ReplyMarkups; namespace PnvPanel.Api.Telegram; internal sealed class TelegramNotifier( ITelegramBotClient botClient, IIdentityService identityService, IOptions options ) : ITelegramNotifier { public async Task NotifyAdminsActivationRequestedAsync( Guid requestId, string userName, string? comment, CancellationToken cancellationToken ) { if (string.IsNullOrWhiteSpace(options.Value.BotToken)) return; var text = $"🆕 Запрос на активацию от {Escape(userName)}" + ( string.IsNullOrWhiteSpace(comment) ? string.Empty : $"\nКомментарий: {Escape(comment)}" ); var keyboard = new InlineKeyboardMarkup( new[] { InlineKeyboardButton.WithCallbackData("✅ Активировать", $"act:approve:{requestId}"), InlineKeyboardButton.WithCallbackData("❌ Отклонить", $"act:reject:{requestId}"), } ); foreach (var adminId in options.Value.ParseAdminTelegramUserIds()) { try { await botClient.SendMessage( adminId, text, parseMode: ParseMode.Html, replyMarkup: keyboard, cancellationToken: cancellationToken ); } catch { // Админ мог не запускать бота (нет чата с ботом) — пропускаем, не валим команду. } } } public async Task NotifyAdminsBugReportCreatedAsync( Guid ticketId, string userName, string message, CancellationToken cancellationToken ) { if (string.IsNullOrWhiteSpace(options.Value.BotToken)) return; var preview = message.Length > 300 ? message[..300] + "…" : message; var text = $"🐞 Новый тикет (баг/предложение) от {Escape(userName)}\n{Escape(preview)}"; // Только ссылка на сайт — переписка и картинки удобнее там, инлайн-действий для баг-тикетов нет. InlineKeyboardMarkup? keyboard = null; if (!string.IsNullOrWhiteSpace(options.Value.PublicSiteUrl)) { var url = $"{options.Value.PublicSiteUrl.TrimEnd('/')}/admin/support?ticket={ticketId}"; keyboard = new InlineKeyboardMarkup( new[] { InlineKeyboardButton.WithUrl("🌐 Открыть на сайте", url) } ); } foreach (var adminId in options.Value.ParseAdminTelegramUserIds()) { try { await botClient.SendMessage( adminId, text, parseMode: ParseMode.Html, replyMarkup: keyboard, cancellationToken: cancellationToken ); } catch { // Админ мог не запускать бота (нет чата с ботом) — пропускаем, не валим команду. } } } public async Task NotifyAdminsExtensionRequestCreatedAsync( Guid ticketId, string userName, int requestedDays, string justification, CancellationToken cancellationToken ) { if (string.IsNullOrWhiteSpace(options.Value.BotToken)) return; var text = $"🆕 Заявка на продление от {Escape(userName)} — {requestedDays} дн.\nОбоснование: {Escape(justification)}"; var keyboard = new InlineKeyboardMarkup( new[] { InlineKeyboardButton.WithCallbackData("✅ Одобрить", $"erq:approve:{ticketId}"), InlineKeyboardButton.WithCallbackData("❌ Отклонить", $"erq:reject:{ticketId}"), } ); foreach (var adminId in options.Value.ParseAdminTelegramUserIds()) { try { await botClient.SendMessage( adminId, text, parseMode: ParseMode.Html, replyMarkup: keyboard, cancellationToken: cancellationToken ); } catch { // Админ мог не запускать бота (нет чата с ботом) — пропускаем, не валим команду. } } } public async Task NotifyAdminsTicketReopenedAsync( Guid ticketId, string userName, TicketType type, CancellationToken cancellationToken ) { if (string.IsNullOrWhiteSpace(options.Value.BotToken)) return; var text = $"🔓 Тикет от {Escape(userName)} переоткрыт (баг/предложение)"; InlineKeyboardMarkup? keyboard = null; if (!string.IsNullOrWhiteSpace(options.Value.PublicSiteUrl)) { var url = $"{options.Value.PublicSiteUrl.TrimEnd('/')}/admin/support?ticket={ticketId}"; keyboard = new InlineKeyboardMarkup( new[] { InlineKeyboardButton.WithUrl("🌐 Открыть на сайте", url) } ); } foreach (var adminId in options.Value.ParseAdminTelegramUserIds()) { try { await botClient.SendMessage( adminId, text, parseMode: ParseMode.Html, replyMarkup: keyboard, cancellationToken: cancellationToken ); } catch { // Админ мог не запускать бота (нет чата с ботом) — пропускаем, не валим команду. } } } public async Task NotifyUsersNewsPublishedAsync( string title, CancellationToken cancellationToken ) { if (string.IsNullOrWhiteSpace(options.Value.BotToken)) return; var text = $"📰 Новая новость: {Escape(title)}"; InlineKeyboardMarkup? keyboard = null; if (!string.IsNullOrWhiteSpace(options.Value.PublicSiteUrl)) { var url = $"{options.Value.PublicSiteUrl.TrimEnd('/')}/news"; keyboard = new InlineKeyboardMarkup( new[] { InlineKeyboardButton.WithUrl("🌐 Открыть на сайте", url) } ); } var telegramUserIds = await identityService.GetActivatedLinkedTelegramUserIdsAsync( cancellationToken ); foreach (var telegramUserId in telegramUserIds) { try { await botClient.SendMessage( telegramUserId, text, parseMode: ParseMode.Html, replyMarkup: keyboard, cancellationToken: cancellationToken ); } catch { // Пользователь мог заблокировать бота — не критично, продолжаем рассылку остальным. } } } public async Task NotifyAdminsPaymentRequestedAsync( Guid requestId, string userName, PaymentRequestKind kind, PaymentPeriod? period, int amount, CancellationToken cancellationToken ) { if (string.IsNullOrWhiteSpace(options.Value.BotToken)) return; var reason = kind == PaymentRequestKind.PlanChangeTopUp ? "доплате за смену тарифа" : $"оплате за {PeriodLabel(period!.Value)}"; var text = $"💰 {Escape(userName)} заявляет о {reason} — {amount} ₽\nПроверьте поступление и подтвердите."; var keyboard = new InlineKeyboardMarkup( new[] { InlineKeyboardButton.WithCallbackData("✅ Подтвердить", $"pay:approve:{requestId}"), InlineKeyboardButton.WithCallbackData("❌ Отклонить", $"pay:reject:{requestId}"), } ); foreach (var adminId in options.Value.ParseAdminTelegramUserIds()) { try { await botClient.SendMessage( adminId, text, parseMode: ParseMode.Html, replyMarkup: keyboard, cancellationToken: cancellationToken ); } catch { // Админ мог не запускать бота (нет чата с ботом) — пропускаем, не валим команду. } } } private static string PeriodLabel(PaymentPeriod period) => period switch { PaymentPeriod.Quarter => "3 месяца", PaymentPeriod.HalfYear => "полгода", PaymentPeriod.Year => "год", _ => period.ToString(), }; public async Task NotifyUserAsync( Guid userId, string message, string? linkPath, CancellationToken cancellationToken ) { if (string.IsNullOrWhiteSpace(options.Value.BotToken)) return; var link = await identityService.GetTelegramLinkInfoAsync(userId, cancellationToken); if (!link.IsLinked || link.TelegramUserId is not { } telegramUserId) return; InlineKeyboardMarkup? keyboard = null; if (!string.IsNullOrWhiteSpace(options.Value.PublicSiteUrl)) { var url = string.IsNullOrWhiteSpace(linkPath) ? options.Value.PublicSiteUrl : $"{options.Value.PublicSiteUrl.TrimEnd('/')}{linkPath}"; keyboard = new InlineKeyboardMarkup( new[] { InlineKeyboardButton.WithUrl("🌐 Открыть на сайте", url) } ); } try { await botClient.SendMessage( telegramUserId, message, replyMarkup: keyboard, cancellationToken: cancellationToken ); } catch { // Пользователь мог заблокировать бота — не критично для основной операции. } } public async Task NotifyAdminsNodeStatusChangedAsync( Guid nodeId, string nodeName, NodeStatus status, CancellationToken cancellationToken ) { if (string.IsNullOrWhiteSpace(options.Value.BotToken)) return; var emoji = status == NodeStatus.Online ? "✅" : "🔴"; var statusLabel = status == NodeStatus.Online ? "снова в сети" : "недоступна"; var text = $"{emoji} Нода {Escape(nodeName)} {statusLabel}."; InlineKeyboardMarkup? keyboard = null; if (!string.IsNullOrWhiteSpace(options.Value.PublicSiteUrl)) { var url = $"{options.Value.PublicSiteUrl.TrimEnd('/')}/admin/nodes"; keyboard = new InlineKeyboardMarkup( new[] { InlineKeyboardButton.WithUrl("🌐 Открыть на сайте", url) } ); } foreach (var adminId in options.Value.ParseAdminTelegramUserIds()) { try { await botClient.SendMessage( adminId, text, parseMode: ParseMode.Html, replyMarkup: keyboard, cancellationToken: cancellationToken ); } catch { // Админ мог не запускать бота (нет чата с ботом) — пропускаем, не валим команду. } } } private static string Escape(string text) => text.Replace("&", "&").Replace("<", "<").Replace(">", ">"); }