Enhance Telegram bot integration and notification system
- Updated PnvBotUpdateHandler to improve message editing for login and activation requests, providing clearer feedback and removing lingering buttons. - Modified TelegramNotifier to include an optional button linking to the public site in user notifications. - Enhanced CloseTicketCommandHandler and ResolveTicketCommandHandler to notify users via Telegram when their support tickets are closed or resolved, improving user engagement. - Updated ITelegramNotifier interface documentation to reflect the new functionality of including a site link in user notifications.
This commit is contained in:
@@ -188,9 +188,11 @@ public sealed class PnvBotUpdateHandler(
|
|||||||
: await sender.Send(new RejectTelegramLoginCommand(requestId, fromId), cancellationToken);
|
: await sender.Send(new RejectTelegramLoginCommand(requestId, fromId), cancellationToken);
|
||||||
|
|
||||||
await botClient.AnswerCallbackQuery(callback.Id, result.IsSuccess ? "Готово" : result.Error.Message, cancellationToken: cancellationToken);
|
await botClient.AnswerCallbackQuery(callback.Id, result.IsSuccess ? "Готово" : result.Error.Message, cancellationToken: cancellationToken);
|
||||||
if (result.IsSuccess && callback.Message is not null)
|
if (callback.Message is not null)
|
||||||
{
|
{
|
||||||
var statusText = parts[1] == "approve" ? "✅ Вход подтверждён." : "❌ Вход отклонён.";
|
var statusText = result.IsSuccess
|
||||||
|
? (parts[1] == "approve" ? "✅ Вход подтверждён." : "❌ Вход отклонён.")
|
||||||
|
: $"⚠️ {result.Error.Message}";
|
||||||
var text = $"{Escape(callback.Message.Text ?? "")}\n\n{statusText}";
|
var text = $"{Escape(callback.Message.Text ?? "")}\n\n{statusText}";
|
||||||
await botClient.EditMessageText(
|
await botClient.EditMessageText(
|
||||||
chatId.Value, callback.Message.Id, text, parseMode: ParseMode.Html, cancellationToken: cancellationToken);
|
chatId.Value, callback.Message.Id, text, parseMode: ParseMode.Html, cancellationToken: cancellationToken);
|
||||||
@@ -211,11 +213,14 @@ public sealed class PnvBotUpdateHandler(
|
|||||||
: await sender.Send(new RejectActivationCommand(requestId, Reason: null), cancellationToken);
|
: await sender.Send(new RejectActivationCommand(requestId, Reason: null), cancellationToken);
|
||||||
|
|
||||||
await botClient.AnswerCallbackQuery(callback.Id, result.IsSuccess ? "Готово" : result.Error.Message, cancellationToken: cancellationToken);
|
await botClient.AnswerCallbackQuery(callback.Id, result.IsSuccess ? "Готово" : result.Error.Message, cancellationToken: cancellationToken);
|
||||||
if (result.IsSuccess && callback.Message is not null)
|
if (callback.Message is not null)
|
||||||
{
|
{
|
||||||
// Редактируем исходное сообщение с запросом вместо отдельного — иначе кнопки
|
// Редактируем исходное сообщение с запросом вместо отдельного — иначе кнопки
|
||||||
// «Активировать/Отклонить» остаются висеть под уже обработанным запросом.
|
// «Активировать/Отклонить» остаются висеть под уже обработанным запросом (в т.ч.
|
||||||
var statusText = parts[1] == "approve" ? "✅ Пользователь активирован." : "❌ Запрос отклонён.";
|
// если его обработали в другом месте — на сайте или из другого чата).
|
||||||
|
var statusText = result.IsSuccess
|
||||||
|
? (parts[1] == "approve" ? "✅ Пользователь активирован." : "❌ Запрос отклонён.")
|
||||||
|
: $"⚠️ {result.Error.Message}";
|
||||||
var text = $"{Escape(callback.Message.Text ?? "")}\n\n{statusText}";
|
var text = $"{Escape(callback.Message.Text ?? "")}\n\n{statusText}";
|
||||||
await botClient.EditMessageText(
|
await botClient.EditMessageText(
|
||||||
chatId.Value, callback.Message.Id, text, parseMode: ParseMode.Html, cancellationToken: cancellationToken);
|
chatId.Value, callback.Message.Id, text, parseMode: ParseMode.Html, cancellationToken: cancellationToken);
|
||||||
@@ -236,9 +241,11 @@ public sealed class PnvBotUpdateHandler(
|
|||||||
: await sender.Send(new RejectRoleRequestCommand(requestId, Reason: null), cancellationToken);
|
: await sender.Send(new RejectRoleRequestCommand(requestId, Reason: null), cancellationToken);
|
||||||
|
|
||||||
await botClient.AnswerCallbackQuery(callback.Id, result.IsSuccess ? "Готово" : result.Error.Message, cancellationToken: cancellationToken);
|
await botClient.AnswerCallbackQuery(callback.Id, result.IsSuccess ? "Готово" : result.Error.Message, cancellationToken: cancellationToken);
|
||||||
if (result.IsSuccess && callback.Message is not null)
|
if (callback.Message is not null)
|
||||||
{
|
{
|
||||||
var statusText = parts[1] == "approve" ? "✅ Заявка одобрена, роль выдана." : "❌ Заявка отклонена.";
|
var statusText = result.IsSuccess
|
||||||
|
? (parts[1] == "approve" ? "✅ Заявка одобрена, роль выдана." : "❌ Заявка отклонена.")
|
||||||
|
: $"⚠️ {result.Error.Message}";
|
||||||
var text = $"{Escape(callback.Message.Text ?? "")}\n\n{statusText}";
|
var text = $"{Escape(callback.Message.Text ?? "")}\n\n{statusText}";
|
||||||
await botClient.EditMessageText(
|
await botClient.EditMessageText(
|
||||||
chatId.Value, callback.Message.Id, text, parseMode: ParseMode.Html, cancellationToken: cancellationToken);
|
chatId.Value, callback.Message.Id, text, parseMode: ParseMode.Html, cancellationToken: cancellationToken);
|
||||||
|
|||||||
@@ -106,9 +106,13 @@ internal sealed class TelegramNotifier(ITelegramBotClient botClient, IIdentitySe
|
|||||||
if (!link.IsLinked || link.TelegramUserId is not { } telegramUserId)
|
if (!link.IsLinked || link.TelegramUserId is not { } telegramUserId)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
InlineKeyboardMarkup? keyboard = null;
|
||||||
|
if (!string.IsNullOrWhiteSpace(options.Value.PublicSiteUrl))
|
||||||
|
keyboard = new InlineKeyboardMarkup(new[] { InlineKeyboardButton.WithUrl("🌐 Открыть на сайте", options.Value.PublicSiteUrl) });
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await botClient.SendMessage(telegramUserId, message, cancellationToken: cancellationToken);
|
await botClient.SendMessage(telegramUserId, message, replyMarkup: keyboard, cancellationToken: cancellationToken);
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -9,7 +9,8 @@ using PnvPanel.Domain.Support;
|
|||||||
|
|
||||||
namespace PnvPanel.Application.Admin.Support;
|
namespace PnvPanel.Application.Admin.Support;
|
||||||
|
|
||||||
public sealed class CloseTicketCommandHandler(IAppDbContext dbContext, IRealtimeNotifier notifier, ICurrentUser currentUser)
|
public sealed class CloseTicketCommandHandler(
|
||||||
|
IAppDbContext dbContext, IRealtimeNotifier notifier, ITelegramNotifier telegramNotifier, ICurrentUser currentUser)
|
||||||
: ICommandHandler<CloseTicketCommand, Result>
|
: ICommandHandler<CloseTicketCommand, Result>
|
||||||
{
|
{
|
||||||
public async Task<Result> Handle(CloseTicketCommand command, CancellationToken cancellationToken)
|
public async Task<Result> Handle(CloseTicketCommand command, CancellationToken cancellationToken)
|
||||||
@@ -30,6 +31,7 @@ public sealed class CloseTicketCommandHandler(IAppDbContext dbContext, IRealtime
|
|||||||
adminId, "TicketClosed", "SupportTicket", ticket.Id.ToString(), metadata: null, AuditSource.Web));
|
adminId, "TicketClosed", "SupportTicket", ticket.Id.ToString(), metadata: null, AuditSource.Web));
|
||||||
|
|
||||||
await notifier.NotifyTicketUpdatedAsync(ticket.Id, ticket.UserId, cancellationToken);
|
await notifier.NotifyTicketUpdatedAsync(ticket.Id, ticket.UserId, cancellationToken);
|
||||||
|
await telegramNotifier.NotifyUserAsync(ticket.UserId, "🔒 Ваше обращение закрыто.", cancellationToken);
|
||||||
|
|
||||||
return Result.Success();
|
return Result.Success();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,8 @@ using PnvPanel.Domain.Support;
|
|||||||
|
|
||||||
namespace PnvPanel.Application.Admin.Support;
|
namespace PnvPanel.Application.Admin.Support;
|
||||||
|
|
||||||
public sealed class ResolveTicketCommandHandler(IAppDbContext dbContext, IRealtimeNotifier notifier, ICurrentUser currentUser)
|
public sealed class ResolveTicketCommandHandler(
|
||||||
|
IAppDbContext dbContext, IRealtimeNotifier notifier, ITelegramNotifier telegramNotifier, ICurrentUser currentUser)
|
||||||
: ICommandHandler<ResolveTicketCommand, Result>
|
: ICommandHandler<ResolveTicketCommand, Result>
|
||||||
{
|
{
|
||||||
public async Task<Result> Handle(ResolveTicketCommand command, CancellationToken cancellationToken)
|
public async Task<Result> Handle(ResolveTicketCommand command, CancellationToken cancellationToken)
|
||||||
@@ -30,6 +31,7 @@ public sealed class ResolveTicketCommandHandler(IAppDbContext dbContext, IRealti
|
|||||||
adminId, "TicketResolved", "SupportTicket", ticket.Id.ToString(), metadata: null, AuditSource.Web));
|
adminId, "TicketResolved", "SupportTicket", ticket.Id.ToString(), metadata: null, AuditSource.Web));
|
||||||
|
|
||||||
await notifier.NotifyTicketUpdatedAsync(ticket.Id, ticket.UserId, cancellationToken);
|
await notifier.NotifyTicketUpdatedAsync(ticket.Id, ticket.UserId, cancellationToken);
|
||||||
|
await telegramNotifier.NotifyUserAsync(ticket.UserId, "✅ Ваше обращение решено.", cancellationToken);
|
||||||
|
|
||||||
return Result.Success();
|
return Result.Success();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,8 @@ public interface ITelegramNotifier
|
|||||||
Task NotifyAdminsActivationRequestedAsync(
|
Task NotifyAdminsActivationRequestedAsync(
|
||||||
Guid requestId, string userName, string? comment, CancellationToken cancellationToken);
|
Guid requestId, string userName, string? comment, CancellationToken cancellationToken);
|
||||||
|
|
||||||
/// <summary>Личное сообщение пользователю, если у него привязан Telegram (иначе no-op).</summary>
|
/// <summary>Личное сообщение пользователю, если у него привязан Telegram (иначе no-op).
|
||||||
|
/// Если задан PublicSiteUrl — добавляет кнопку-ссылку на сайт.</summary>
|
||||||
Task NotifyUserAsync(Guid userId, string message, CancellationToken cancellationToken);
|
Task NotifyUserAsync(Guid userId, string message, CancellationToken cancellationToken);
|
||||||
|
|
||||||
/// <summary>Баг-репорт/предложение — только кнопка-ссылка на сайт (переписка и картинки — там),
|
/// <summary>Баг-репорт/предложение — только кнопка-ссылка на сайт (переписка и картинки — там),
|
||||||
|
|||||||
@@ -27,13 +27,13 @@ function AdminLayout() {
|
|||||||
return (
|
return (
|
||||||
<div className="mx-auto flex w-full max-w-5xl flex-col gap-6 px-6 py-10">
|
<div className="mx-auto flex w-full max-w-5xl flex-col gap-6 px-6 py-10">
|
||||||
<h1 className="text-2xl font-semibold tracking-tight">{t('nav.admin')}</h1>
|
<h1 className="text-2xl font-semibold tracking-tight">{t('nav.admin')}</h1>
|
||||||
<nav className="flex gap-1 overflow-x-auto border-b border-border">
|
<nav className="flex flex-wrap gap-1 border-b border-border">
|
||||||
{TABS.map((tab) => (
|
{TABS.map((tab) => (
|
||||||
<Link
|
<Link
|
||||||
key={tab.to}
|
key={tab.to}
|
||||||
to={tab.to}
|
to={tab.to}
|
||||||
activeOptions={{ exact: tab.to === '/admin' }}
|
activeOptions={{ exact: tab.to === '/admin' }}
|
||||||
className={cn('shrink-0 whitespace-nowrap px-3 py-2 text-sm text-muted-foreground hover:text-foreground')}
|
className={cn('whitespace-nowrap px-3 py-2 text-sm text-muted-foreground hover:text-foreground')}
|
||||||
activeProps={{ className: 'border-b-2 border-primary text-foreground font-medium' }}
|
activeProps={{ className: 'border-b-2 border-primary text-foreground font-medium' }}
|
||||||
>
|
>
|
||||||
{t(`admin.tabs.${tab.key}`)}
|
{t(`admin.tabs.${tab.key}`)}
|
||||||
|
|||||||
Reference in New Issue
Block a user