Queue chat bulk actions through SQL outbox
This commit is contained in:
@@ -18,6 +18,8 @@ public sealed class MaxOutboxService(
|
||||
{
|
||||
public async Task<int> ProcessOnceAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
var processed = await ProcessPendingChatActionsAsync(cancellationToken);
|
||||
|
||||
var candidates = await db.Messages
|
||||
.Include(message => message.Chat)
|
||||
.Include(message => message.Attachments)
|
||||
@@ -25,6 +27,8 @@ public sealed class MaxOutboxService(
|
||||
.Where(message =>
|
||||
message.Direction == MessageDirection.Outgoing &&
|
||||
message.DeliveryState == MessageDeliveryState.Sending &&
|
||||
message.Chat != null &&
|
||||
message.Chat.DeletedAt == null &&
|
||||
message.DeletedAt == null)
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
@@ -34,7 +38,6 @@ public sealed class MaxOutboxService(
|
||||
.Take(10)
|
||||
.ToArray();
|
||||
|
||||
var processed = 0;
|
||||
foreach (var message in pending)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
@@ -47,6 +50,147 @@ public sealed class MaxOutboxService(
|
||||
return processed;
|
||||
}
|
||||
|
||||
private async Task<int> ProcessPendingChatActionsAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
var pending = await db.Chats
|
||||
.Where(chat => chat.PendingMaxAction != null)
|
||||
.ToListAsync(cancellationToken);
|
||||
pending = pending
|
||||
.OrderBy(chat => chat.PendingMaxActionRequestedAt ?? chat.UpdatedAt)
|
||||
.ThenBy(chat => chat.Id)
|
||||
.Take(10)
|
||||
.ToList();
|
||||
|
||||
var processed = 0;
|
||||
foreach (var chat in pending)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
if (await ProcessChatActionAsync(chat, cancellationToken))
|
||||
{
|
||||
processed++;
|
||||
}
|
||||
}
|
||||
|
||||
return processed;
|
||||
}
|
||||
|
||||
private async Task<bool> ProcessChatActionAsync(Chat chat, CancellationToken cancellationToken)
|
||||
{
|
||||
var pendingAction = chat.PendingMaxAction;
|
||||
if (pendingAction is null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
MaxActionResult result;
|
||||
try
|
||||
{
|
||||
result = pendingAction.Value switch
|
||||
{
|
||||
ChatPendingMaxAction.ClearHistory => await ApplyMaxChatActionAsync(
|
||||
chat,
|
||||
(externalChatId, chatUrl) => maxBridge.ClearChatHistoryAsync(externalChatId, chatUrl, cancellationToken),
|
||||
cancellationToken),
|
||||
ChatPendingMaxAction.DeleteChat => await ApplyMaxChatActionAsync(
|
||||
chat,
|
||||
(externalChatId, chatUrl) => maxBridge.DeleteChatAsync(externalChatId, chatUrl, cancellationToken),
|
||||
cancellationToken),
|
||||
_ => new MaxActionResult(false, $"Unsupported chat action: {pendingAction.Value}.")
|
||||
};
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogWarning(ex, "MAX chat action {Action} failed for chat {ChatId}.", pendingAction.Value, chat.Id);
|
||||
result = new MaxActionResult(false, ex.Message);
|
||||
}
|
||||
|
||||
var now = DateTimeOffset.UtcNow;
|
||||
chat.PendingMaxActionLastAttemptAt = now;
|
||||
if (result.Success)
|
||||
{
|
||||
chat.PendingMaxAction = null;
|
||||
chat.PendingMaxActionRequestedAt = null;
|
||||
chat.PendingMaxActionError = null;
|
||||
}
|
||||
else
|
||||
{
|
||||
chat.PendingMaxActionAttempts++;
|
||||
chat.PendingMaxActionError = result.Error ?? $"MAX chat action {pendingAction.Value} failed.";
|
||||
}
|
||||
|
||||
chat.UpdatedAt = now;
|
||||
await db.SaveChangesAsync(cancellationToken);
|
||||
if (!result.Success)
|
||||
{
|
||||
logger.LogWarning(
|
||||
"MAX chat action {Action} failed for chat {ChatId}: {Error}",
|
||||
pendingAction.Value,
|
||||
chat.Id,
|
||||
chat.PendingMaxActionError);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private async Task<MaxActionResult> ApplyMaxChatActionAsync(
|
||||
Chat chat,
|
||||
Func<string, string?, Task<MaxActionResult>> action,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(chat.ExternalId))
|
||||
{
|
||||
return new MaxActionResult(true, null);
|
||||
}
|
||||
|
||||
var chatUrl = chat.WebUrl;
|
||||
if (string.IsNullOrWhiteSpace(chatUrl))
|
||||
{
|
||||
chatUrl = await ResolveAndPersistChatUrlAsync(chat, cancellationToken);
|
||||
}
|
||||
|
||||
var result = await action(chat.ExternalId, chatUrl);
|
||||
if (!result.Success && IsChatOpenFailure(result.Error))
|
||||
{
|
||||
var previousUrl = chatUrl;
|
||||
chatUrl = await ResolveAndPersistChatUrlAsync(chat, cancellationToken);
|
||||
if (!string.IsNullOrWhiteSpace(chatUrl) &&
|
||||
!string.Equals(previousUrl, chatUrl, StringComparison.Ordinal))
|
||||
{
|
||||
result = await action(chat.ExternalId, chatUrl);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private async Task<string?> ResolveAndPersistChatUrlAsync(Chat chat, CancellationToken cancellationToken)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(chat.ExternalId))
|
||||
{
|
||||
return chat.WebUrl;
|
||||
}
|
||||
|
||||
var result = await maxBridge.ResolveChatUrlAsync(chat.ExternalId, cancellationToken);
|
||||
if (!result.Success || string.IsNullOrWhiteSpace(result.ChatUrl))
|
||||
{
|
||||
return chat.WebUrl;
|
||||
}
|
||||
|
||||
var nextWebUrl = result.ChatUrl.Trim();
|
||||
if (!string.Equals(chat.WebUrl, nextWebUrl, StringComparison.Ordinal))
|
||||
{
|
||||
chat.WebUrl = nextWebUrl;
|
||||
}
|
||||
|
||||
return chat.WebUrl;
|
||||
}
|
||||
|
||||
private static bool IsChatOpenFailure(string? error)
|
||||
{
|
||||
return error?.Contains("not found or did not open", StringComparison.OrdinalIgnoreCase) == true ||
|
||||
error?.Contains("menu was not opened", StringComparison.OrdinalIgnoreCase) == true;
|
||||
}
|
||||
|
||||
private async Task<bool> ProcessMessageAsync(Message message, CancellationToken cancellationToken)
|
||||
{
|
||||
var chat = message.Chat;
|
||||
|
||||
Reference in New Issue
Block a user