Queue chat bulk actions through SQL outbox
This commit is contained in:
@@ -26,7 +26,9 @@ public sealed class ChatsController(
|
||||
[HttpGet]
|
||||
public async Task<ActionResult<IReadOnlyList<ChatDto>>> GetChats(CancellationToken cancellationToken)
|
||||
{
|
||||
var chats = (await db.Chats.ToArrayAsync(cancellationToken))
|
||||
var chats = (await db.Chats
|
||||
.Where(x => x.DeletedAt == null)
|
||||
.ToArrayAsync(cancellationToken))
|
||||
.OrderByDescending(x => x.IsPinned)
|
||||
.ThenByDescending(x => x.UnreadCount > 0)
|
||||
.ThenByDescending(x => x.LastMessageAt ?? x.UpdatedAt)
|
||||
@@ -59,7 +61,7 @@ public sealed class ChatsController(
|
||||
[HttpGet("{chatId:guid}/messages")]
|
||||
public async Task<ActionResult<IReadOnlyList<MessageDto>>> GetMessages(Guid chatId, int take = 80, DateTimeOffset? before = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var chat = await db.Chats.FirstOrDefaultAsync(x => x.Id == chatId, cancellationToken);
|
||||
var chat = await db.Chats.FirstOrDefaultAsync(x => x.Id == chatId && x.DeletedAt == null, cancellationToken);
|
||||
if (chat is null)
|
||||
{
|
||||
return NotFound();
|
||||
@@ -69,7 +71,7 @@ public sealed class ChatsController(
|
||||
{
|
||||
await syncService.SyncChatHistoryAsync(chat.ExternalId, chat.WebUrl, cancellationToken);
|
||||
db.ChangeTracker.Clear();
|
||||
chat = await db.Chats.FirstOrDefaultAsync(x => x.Id == chatId, cancellationToken);
|
||||
chat = await db.Chats.FirstOrDefaultAsync(x => x.Id == chatId && x.DeletedAt == null, cancellationToken);
|
||||
if (chat is null)
|
||||
{
|
||||
return NotFound();
|
||||
@@ -104,7 +106,7 @@ public sealed class ChatsController(
|
||||
[HttpPost("{chatId:guid}/read")]
|
||||
public async Task<IActionResult> MarkRead(Guid chatId, CancellationToken cancellationToken)
|
||||
{
|
||||
var chat = await db.Chats.FirstOrDefaultAsync(x => x.Id == chatId, cancellationToken);
|
||||
var chat = await db.Chats.FirstOrDefaultAsync(x => x.Id == chatId && x.DeletedAt == null, cancellationToken);
|
||||
if (chat is null)
|
||||
{
|
||||
return NotFound();
|
||||
@@ -136,19 +138,7 @@ public sealed class ChatsController(
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
foreach (var chat in chats)
|
||||
{
|
||||
var maxFailure = await TryApplyMaxChatActionAsync(
|
||||
chat,
|
||||
(externalChatId, chatUrl) => maxBridge.ClearChatHistoryAsync(externalChatId, chatUrl, cancellationToken),
|
||||
"clear history",
|
||||
cancellationToken);
|
||||
if (maxFailure is not null)
|
||||
{
|
||||
return maxFailure;
|
||||
}
|
||||
}
|
||||
|
||||
var now = DateTimeOffset.UtcNow;
|
||||
var files = AttachmentFiles(chats).ToArray();
|
||||
foreach (var chat in chats)
|
||||
{
|
||||
@@ -156,7 +146,12 @@ public sealed class ChatsController(
|
||||
chat.LastMessageAt = null;
|
||||
chat.LastMessagePreview = null;
|
||||
chat.UnreadCount = 0;
|
||||
chat.UpdatedAt = DateTimeOffset.UtcNow;
|
||||
chat.HistoryClearedAt = now;
|
||||
chat.UpdatedAt = now;
|
||||
if (chat.DeletedAt is null)
|
||||
{
|
||||
QueuePendingMaxAction(chat, ChatPendingMaxAction.ClearHistory, now);
|
||||
}
|
||||
}
|
||||
|
||||
await db.SaveChangesAsync(cancellationToken);
|
||||
@@ -180,21 +175,20 @@ public sealed class ChatsController(
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
var now = DateTimeOffset.UtcNow;
|
||||
var files = AttachmentFiles(chats).ToArray();
|
||||
foreach (var chat in chats)
|
||||
{
|
||||
var maxFailure = await TryApplyMaxChatActionAsync(
|
||||
chat,
|
||||
(externalChatId, chatUrl) => maxBridge.DeleteChatAsync(externalChatId, chatUrl, cancellationToken),
|
||||
"delete chat",
|
||||
cancellationToken);
|
||||
if (maxFailure is not null)
|
||||
{
|
||||
return maxFailure;
|
||||
}
|
||||
db.Messages.RemoveRange(chat.Messages);
|
||||
chat.DeletedAt ??= now;
|
||||
chat.HistoryClearedAt = now;
|
||||
chat.LastMessageAt = null;
|
||||
chat.LastMessagePreview = null;
|
||||
chat.UnreadCount = 0;
|
||||
chat.UpdatedAt = now;
|
||||
QueuePendingMaxAction(chat, ChatPendingMaxAction.DeleteChat, now);
|
||||
}
|
||||
|
||||
var files = AttachmentFiles(chats).ToArray();
|
||||
db.Chats.RemoveRange(chats);
|
||||
await db.SaveChangesAsync(cancellationToken);
|
||||
DeleteFiles(files);
|
||||
await hubContext.Clients.All.SendAsync("ChatListInvalidated", cancellationToken);
|
||||
@@ -214,7 +208,7 @@ public sealed class ChatsController(
|
||||
return BadRequest("Search query is required.");
|
||||
}
|
||||
|
||||
var chatExists = await db.Chats.AnyAsync(x => x.Id == chatId, cancellationToken);
|
||||
var chatExists = await db.Chats.AnyAsync(x => x.Id == chatId && x.DeletedAt == null, cancellationToken);
|
||||
if (!chatExists)
|
||||
{
|
||||
return NotFound();
|
||||
@@ -242,7 +236,7 @@ public sealed class ChatsController(
|
||||
{
|
||||
var chat = await db.Chats
|
||||
.AsNoTracking()
|
||||
.FirstOrDefaultAsync(x => x.Id == chatId, cancellationToken);
|
||||
.FirstOrDefaultAsync(x => x.Id == chatId && x.DeletedAt == null, cancellationToken);
|
||||
|
||||
if (chat is null || string.IsNullOrWhiteSpace(chat.AvatarUrl))
|
||||
{
|
||||
@@ -263,7 +257,7 @@ public sealed class ChatsController(
|
||||
{
|
||||
var chat = await db.Chats
|
||||
.AsNoTracking()
|
||||
.FirstOrDefaultAsync(x => x.Id == chatId, cancellationToken);
|
||||
.FirstOrDefaultAsync(x => x.Id == chatId && x.DeletedAt == null, cancellationToken);
|
||||
|
||||
if (chat is null)
|
||||
{
|
||||
@@ -285,7 +279,7 @@ public sealed class ChatsController(
|
||||
[HttpPost("{chatId:guid}/messages")]
|
||||
public async Task<ActionResult<MessageDto>> SendMessage(Guid chatId, SendMessageRequest request, CancellationToken cancellationToken)
|
||||
{
|
||||
var chat = await db.Chats.FindAsync([chatId], cancellationToken);
|
||||
var chat = await db.Chats.FirstOrDefaultAsync(x => x.Id == chatId && x.DeletedAt == null, cancellationToken);
|
||||
if (chat is null)
|
||||
{
|
||||
return NotFound();
|
||||
@@ -527,7 +521,7 @@ public sealed class ChatsController(
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
var target = await db.Chats.FindAsync([request.TargetChatId], cancellationToken);
|
||||
var target = await db.Chats.FirstOrDefaultAsync(x => x.Id == request.TargetChatId && x.DeletedAt == null, cancellationToken);
|
||||
if (target is null)
|
||||
{
|
||||
return NotFound();
|
||||
@@ -600,7 +594,7 @@ public sealed class ChatsController(
|
||||
[FromForm] Guid? replyToMessageId,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var chat = await db.Chats.FindAsync([chatId], cancellationToken);
|
||||
var chat = await db.Chats.FirstOrDefaultAsync(x => x.Id == chatId && x.DeletedAt == null, cancellationToken);
|
||||
if (chat is null)
|
||||
{
|
||||
return NotFound();
|
||||
@@ -809,6 +803,15 @@ public sealed class ChatsController(
|
||||
error?.Contains("menu was not opened", StringComparison.OrdinalIgnoreCase) == true;
|
||||
}
|
||||
|
||||
private static void QueuePendingMaxAction(Chat chat, ChatPendingMaxAction action, DateTimeOffset requestedAt)
|
||||
{
|
||||
chat.PendingMaxAction = action;
|
||||
chat.PendingMaxActionRequestedAt = requestedAt;
|
||||
chat.PendingMaxActionLastAttemptAt = null;
|
||||
chat.PendingMaxActionAttempts = 0;
|
||||
chat.PendingMaxActionError = null;
|
||||
}
|
||||
|
||||
private async Task<List<Chat>> LoadChatsWithAttachmentsAsync(IReadOnlyCollection<Guid> chatIds, CancellationToken cancellationToken)
|
||||
{
|
||||
return await db.Chats
|
||||
|
||||
Reference in New Issue
Block a user