Add chat bulk actions and avatar cache
This commit is contained in:
@@ -49,5 +49,6 @@ public sealed record ForwardMessageRequest(Guid TargetChatId);
|
||||
public sealed record SetReactionRequest(string Emoji);
|
||||
public sealed record MessageDeletedDto(Guid ChatId, Guid MessageId);
|
||||
public sealed record CreateDirectChatRequest(string ExternalChatId, string Title, string? AvatarUrl = null);
|
||||
public sealed record ChatBulkActionRequest(IReadOnlyList<Guid> ChatIds);
|
||||
|
||||
public sealed record ChatPresenceDto(bool IsTyping, string? StatusText, DateTimeOffset UpdatedAt);
|
||||
|
||||
@@ -121,6 +121,86 @@ public sealed class ChatsController(
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
[HttpPost("clear-history")]
|
||||
public async Task<IActionResult> ClearHistories(ChatBulkActionRequest request, CancellationToken cancellationToken)
|
||||
{
|
||||
var chatIds = request.ChatIds.Distinct().ToArray();
|
||||
if (chatIds.Length == 0)
|
||||
{
|
||||
return BadRequest("chatIds are required.");
|
||||
}
|
||||
|
||||
var chats = await LoadChatsWithAttachmentsAsync(chatIds, cancellationToken);
|
||||
if (chats.Count != chatIds.Length)
|
||||
{
|
||||
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 files = AttachmentFiles(chats).ToArray();
|
||||
foreach (var chat in chats)
|
||||
{
|
||||
db.Messages.RemoveRange(chat.Messages);
|
||||
chat.LastMessageAt = null;
|
||||
chat.LastMessagePreview = null;
|
||||
chat.UnreadCount = 0;
|
||||
chat.UpdatedAt = DateTimeOffset.UtcNow;
|
||||
}
|
||||
|
||||
await db.SaveChangesAsync(cancellationToken);
|
||||
DeleteFiles(files);
|
||||
await hubContext.Clients.All.SendAsync("ChatListInvalidated", cancellationToken);
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
[HttpPost("delete")]
|
||||
public async Task<IActionResult> DeleteChats(ChatBulkActionRequest request, CancellationToken cancellationToken)
|
||||
{
|
||||
var chatIds = request.ChatIds.Distinct().ToArray();
|
||||
if (chatIds.Length == 0)
|
||||
{
|
||||
return BadRequest("chatIds are required.");
|
||||
}
|
||||
|
||||
var chats = await LoadChatsWithAttachmentsAsync(chatIds, cancellationToken);
|
||||
if (chats.Count != chatIds.Length)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
var files = AttachmentFiles(chats).ToArray();
|
||||
db.Chats.RemoveRange(chats);
|
||||
await db.SaveChangesAsync(cancellationToken);
|
||||
DeleteFiles(files);
|
||||
await hubContext.Clients.All.SendAsync("ChatListInvalidated", cancellationToken);
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
[HttpGet("{chatId:guid}/search")]
|
||||
public async Task<ActionResult<IReadOnlyList<MessageDto>>> SearchMessages(
|
||||
Guid chatId,
|
||||
@@ -656,6 +736,56 @@ public sealed class ChatsController(
|
||||
statusCode: StatusCodes.Status409Conflict);
|
||||
}
|
||||
|
||||
private async Task<ActionResult?> TryApplyMaxChatActionAsync(
|
||||
Chat chat,
|
||||
Func<string, string?, Task<MaxActionResult>> action,
|
||||
string actionName,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
if (string.IsNullOrWhiteSpace(chat.ExternalId))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var result = await action(chat.ExternalId, chat.WebUrl);
|
||||
if (result.Success)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return Problem(
|
||||
result.Error ?? $"MAX {actionName} action failed for {chat.Title}.",
|
||||
statusCode: StatusCodes.Status409Conflict);
|
||||
}
|
||||
|
||||
private async Task<List<Chat>> LoadChatsWithAttachmentsAsync(IReadOnlyCollection<Guid> chatIds, CancellationToken cancellationToken)
|
||||
{
|
||||
return await db.Chats
|
||||
.Include(x => x.Messages)
|
||||
.ThenInclude(x => x.Attachments)
|
||||
.Where(x => chatIds.Contains(x.Id))
|
||||
.ToListAsync(cancellationToken);
|
||||
}
|
||||
|
||||
private IEnumerable<string> AttachmentFiles(IEnumerable<Chat> chats)
|
||||
{
|
||||
return chats
|
||||
.SelectMany(x => x.Messages)
|
||||
.SelectMany(x => x.Attachments)
|
||||
.Select(x => storage.GetPath(x.StorageFileName))
|
||||
.Where(System.IO.File.Exists);
|
||||
}
|
||||
|
||||
private static void DeleteFiles(IEnumerable<string> files)
|
||||
{
|
||||
foreach (var file in files)
|
||||
{
|
||||
System.IO.File.Delete(file);
|
||||
}
|
||||
}
|
||||
|
||||
private static string? NormalizeReaction(string? value)
|
||||
{
|
||||
var emoji = value?.Trim();
|
||||
|
||||
@@ -9,6 +9,8 @@ public interface IMaxBridgeClient
|
||||
Task<IReadOnlyList<MaxChatUpdate>> FetchUpdatesAsync(CancellationToken cancellationToken);
|
||||
Task<MaxChatUpdate?> FetchChatHistoryAsync(string externalChatId, string? chatUrl, CancellationToken cancellationToken);
|
||||
Task<MaxChatPresence?> FetchChatPresenceAsync(string externalChatId, string? chatUrl, CancellationToken cancellationToken);
|
||||
Task<MaxActionResult> ClearChatHistoryAsync(string externalChatId, string? chatUrl, CancellationToken cancellationToken);
|
||||
Task<MaxActionResult> DeleteChatAsync(string externalChatId, string? chatUrl, CancellationToken cancellationToken);
|
||||
Task<MaxSendResult> SendTextAsync(string externalChatId, string? chatUrl, string text, CancellationToken cancellationToken);
|
||||
Task<MaxSendResult> SendAttachmentAsync(string externalChatId, string? chatUrl, string path, string caption, CancellationToken cancellationToken);
|
||||
Task<MaxActionResult> EditMessageAsync(string externalChatId, string externalMessageId, string? currentText, string text, CancellationToken cancellationToken);
|
||||
|
||||
@@ -56,6 +56,16 @@ public sealed class MockMaxBridgeClient : IMaxBridgeClient
|
||||
return Task.FromResult<MaxChatPresence?>(new MaxChatPresence(false, null, DateTimeOffset.UtcNow));
|
||||
}
|
||||
|
||||
public Task<MaxActionResult> ClearChatHistoryAsync(string externalChatId, string? chatUrl, CancellationToken cancellationToken)
|
||||
{
|
||||
return Task.FromResult(new MaxActionResult(true, null));
|
||||
}
|
||||
|
||||
public Task<MaxActionResult> DeleteChatAsync(string externalChatId, string? chatUrl, CancellationToken cancellationToken)
|
||||
{
|
||||
return Task.FromResult(new MaxActionResult(true, null));
|
||||
}
|
||||
|
||||
public Task<MaxSendResult> SendTextAsync(string externalChatId, string? chatUrl, string text, CancellationToken cancellationToken)
|
||||
{
|
||||
return Task.FromResult(new MaxSendResult(true, $"mock-out-{Guid.NewGuid():N}", null, MockChatUrl(externalChatId)));
|
||||
|
||||
@@ -53,6 +53,26 @@ public sealed class WorkerMaxBridgeClient(
|
||||
return await SendAsync<MaxChatPresence>(HttpMethod.Post, "/chat/presence", new { externalChatId, chatUrl }, cancellationToken);
|
||||
}
|
||||
|
||||
public async Task<MaxActionResult> ClearChatHistoryAsync(string externalChatId, string? chatUrl, CancellationToken cancellationToken)
|
||||
{
|
||||
return await SendAsync<MaxActionResult>(
|
||||
HttpMethod.Post,
|
||||
"/chat/clear-history",
|
||||
new { externalChatId, chatUrl },
|
||||
cancellationToken)
|
||||
?? new MaxActionResult(false, "Worker returned an empty clear history result.");
|
||||
}
|
||||
|
||||
public async Task<MaxActionResult> DeleteChatAsync(string externalChatId, string? chatUrl, CancellationToken cancellationToken)
|
||||
{
|
||||
return await SendAsync<MaxActionResult>(
|
||||
HttpMethod.Post,
|
||||
"/chat/delete",
|
||||
new { externalChatId, chatUrl },
|
||||
cancellationToken)
|
||||
?? new MaxActionResult(false, "Worker returned an empty delete chat result.");
|
||||
}
|
||||
|
||||
public async Task<MaxSendResult> SendTextAsync(string externalChatId, string? chatUrl, string text, CancellationToken cancellationToken)
|
||||
{
|
||||
return await SendAsync<MaxSendResult>(HttpMethod.Post, "/send/text", new { externalChatId, chatUrl, text }, cancellationToken)
|
||||
|
||||
Reference in New Issue
Block a user