Notify via Telegram about new support ticket comments
Owner's reply goes to all admins (text preview + link to the ticket), admin's reply goes to the ticket owner as a DM. The comment author is never notified. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
0dcbd4e2b8
commit
1bc86dc426
5 files changed
+117
-5
No files matched your search
@@ -185,6 +185,47 @@ internal sealed class TelegramNotifier(
|
||||
}
|
||||
}
|
||||
|
||||
public async Task NotifyAdminsTicketCommentAddedAsync(
|
||||
Guid ticketId,
|
||||
string userName,
|
||||
string body,
|
||||
CancellationToken cancellationToken
|
||||
)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(options.Value.BotToken))
|
||||
return;
|
||||
|
||||
var preview = body.Length > 300 ? body[..300] + "…" : body;
|
||||
var text = $"💬 Новый комментарий в тикете от <b>{Escape(userName)}</b>\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 NotifyUsersNewsPublishedAsync(
|
||||
string title,
|
||||
string body,
|
||||
|
||||
@@ -50,6 +50,15 @@ public interface ITelegramNotifier
|
||||
CancellationToken cancellationToken
|
||||
);
|
||||
|
||||
/// <summary>Владелец тикета написал новый комментарий — превью текста + кнопка-ссылка на сайт,
|
||||
/// без инлайн-действий (ответ — только на сайте).</summary>
|
||||
Task NotifyAdminsTicketCommentAddedAsync(
|
||||
Guid ticketId,
|
||||
string userName,
|
||||
string body,
|
||||
CancellationToken cancellationToken
|
||||
);
|
||||
|
||||
/// <summary>Пользователь нажал «Я оплатил» — инлайн-кнопки «Подтвердить/Отклонить», решается
|
||||
/// полностью в Telegram. Period задан только для Kind.Subscription — для Kind.PlanChangeTopUp он
|
||||
/// null (доплата не привязана к тарифному периоду).</summary>
|
||||
|
||||
+24
-4
@@ -12,6 +12,7 @@ public sealed class AddTicketCommentCommandHandler(
|
||||
IIdentityService identityService,
|
||||
IFileStorage fileStorage,
|
||||
IRealtimeNotifier notifier,
|
||||
ITelegramNotifier telegramNotifier,
|
||||
ICurrentUser currentUser
|
||||
) : ICommandHandler<AddTicketCommentCommand, Result<TicketCommentDto>>
|
||||
{
|
||||
@@ -55,11 +56,30 @@ public sealed class AddTicketCommentCommandHandler(
|
||||
);
|
||||
dbContext.TicketAttachments.AddRange(attachments);
|
||||
|
||||
// Пушим автору тикета, только если комментирует не он сам (иначе он и так это видит у себя).
|
||||
if (ticket.UserId != userId)
|
||||
await notifier.NotifyTicketUpdatedAsync(ticket.Id, ticket.UserId, cancellationToken);
|
||||
|
||||
var userName = currentUser.UserName ?? userId.ToString();
|
||||
|
||||
// Ответ админа — пушим автору тикета (сайт + Telegram); реплика самого автора — админам в
|
||||
// Telegram. Себя не уведомляем (админ в собственном тикете — тоже никого).
|
||||
if (ticket.UserId != userId)
|
||||
{
|
||||
await notifier.NotifyTicketUpdatedAsync(ticket.Id, ticket.UserId, cancellationToken);
|
||||
await telegramNotifier.NotifyUserAsync(
|
||||
ticket.UserId,
|
||||
"💬 Новый ответ в вашем обращении.",
|
||||
$"/support?ticket={ticket.Id}",
|
||||
cancellationToken
|
||||
);
|
||||
}
|
||||
else if (!TicketAuthorization.IsAdmin(profile))
|
||||
{
|
||||
await telegramNotifier.NotifyAdminsTicketCommentAddedAsync(
|
||||
ticket.Id,
|
||||
userName,
|
||||
comment.Body,
|
||||
cancellationToken
|
||||
);
|
||||
}
|
||||
|
||||
var dto = new TicketCommentDto(
|
||||
comment.Id,
|
||||
userId,
|
||||
|
||||
+37
@@ -14,6 +14,7 @@ public class AddTicketCommentCommandHandlerTests
|
||||
private readonly IIdentityService _identityService = Substitute.For<IIdentityService>();
|
||||
private readonly IFileStorage _fileStorage = Substitute.For<IFileStorage>();
|
||||
private readonly IRealtimeNotifier _notifier = Substitute.For<IRealtimeNotifier>();
|
||||
private readonly ITelegramNotifier _telegramNotifier = Substitute.For<ITelegramNotifier>();
|
||||
|
||||
private static CurrentUserProfile Profile(Guid userId, string role) =>
|
||||
new(
|
||||
@@ -51,6 +52,7 @@ public class AddTicketCommentCommandHandlerTests
|
||||
_identityService,
|
||||
_fileStorage,
|
||||
_notifier,
|
||||
_telegramNotifier,
|
||||
currentUser
|
||||
);
|
||||
|
||||
@@ -68,6 +70,22 @@ public class AddTicketCommentCommandHandlerTests
|
||||
Arg.Any<Guid>(),
|
||||
Arg.Any<CancellationToken>()
|
||||
);
|
||||
await _telegramNotifier
|
||||
.DidNotReceive()
|
||||
.NotifyUserAsync(
|
||||
Arg.Any<Guid>(),
|
||||
Arg.Any<string>(),
|
||||
Arg.Any<string?>(),
|
||||
Arg.Any<CancellationToken>()
|
||||
);
|
||||
await _telegramNotifier
|
||||
.Received(1)
|
||||
.NotifyAdminsTicketCommentAddedAsync(
|
||||
ticket.Id,
|
||||
"alice",
|
||||
"апдейт",
|
||||
Arg.Any<CancellationToken>()
|
||||
);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -90,6 +108,7 @@ public class AddTicketCommentCommandHandlerTests
|
||||
_identityService,
|
||||
_fileStorage,
|
||||
_notifier,
|
||||
_telegramNotifier,
|
||||
currentUser
|
||||
);
|
||||
|
||||
@@ -102,6 +121,22 @@ public class AddTicketCommentCommandHandlerTests
|
||||
await _notifier
|
||||
.Received(1)
|
||||
.NotifyTicketUpdatedAsync(ticket.Id, ownerId, Arg.Any<CancellationToken>());
|
||||
await _telegramNotifier
|
||||
.Received(1)
|
||||
.NotifyUserAsync(
|
||||
ownerId,
|
||||
Arg.Any<string>(),
|
||||
$"/support?ticket={ticket.Id}",
|
||||
Arg.Any<CancellationToken>()
|
||||
);
|
||||
await _telegramNotifier
|
||||
.DidNotReceive()
|
||||
.NotifyAdminsTicketCommentAddedAsync(
|
||||
Arg.Any<Guid>(),
|
||||
Arg.Any<string>(),
|
||||
Arg.Any<string>(),
|
||||
Arg.Any<CancellationToken>()
|
||||
);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -124,6 +159,7 @@ public class AddTicketCommentCommandHandlerTests
|
||||
_identityService,
|
||||
_fileStorage,
|
||||
_notifier,
|
||||
_telegramNotifier,
|
||||
currentUser
|
||||
);
|
||||
|
||||
@@ -156,6 +192,7 @@ public class AddTicketCommentCommandHandlerTests
|
||||
_identityService,
|
||||
_fileStorage,
|
||||
_notifier,
|
||||
_telegramNotifier,
|
||||
currentUser
|
||||
);
|
||||
|
||||
|
||||
Reference in new issue
Block a user