Add SQL outbox for MAX sends
This commit is contained in:
@@ -210,6 +210,37 @@ public sealed class MaxBridgeSyncService(
|
||||
continue;
|
||||
}
|
||||
|
||||
var pendingOutgoing = await FindPendingOutgoingEchoAsync(db, chat.Id, incoming, cancellationToken);
|
||||
if (pendingOutgoing is not null)
|
||||
{
|
||||
var merge = await MergeExistingMessageAsync(
|
||||
db,
|
||||
pendingOutgoing,
|
||||
incoming,
|
||||
maxBridgeClient,
|
||||
storage,
|
||||
cancellationToken);
|
||||
if (!string.Equals(pendingOutgoing.ExternalId, incoming.ExternalId, StringComparison.Ordinal))
|
||||
{
|
||||
pendingOutgoing.ExternalId = incoming.ExternalId;
|
||||
merge = merge with { Changed = true };
|
||||
}
|
||||
|
||||
if (merge.Changed)
|
||||
{
|
||||
updatedMessages.Add((chat, pendingOutgoing));
|
||||
if (chat.LastMessageAt is null || pendingOutgoing.SentAt >= chat.LastMessageAt)
|
||||
{
|
||||
chat.LastMessagePreview = LastMessagePreview(pendingOutgoing.Text, pendingOutgoing.Attachments.Concat(merge.AddedAttachments));
|
||||
chat.LastMessageAt = pendingOutgoing.SentAt;
|
||||
}
|
||||
|
||||
changed++;
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
var message = new Message
|
||||
{
|
||||
Chat = chat,
|
||||
@@ -528,6 +559,89 @@ public sealed class MaxBridgeSyncService(
|
||||
|
||||
private sealed record MessageMergeResult(bool Changed, IReadOnlyList<MessageAttachment> AddedAttachments);
|
||||
|
||||
private static async Task<Message?> FindPendingOutgoingEchoAsync(
|
||||
QMaxDbContext db,
|
||||
Guid chatId,
|
||||
MaxMessageUpdate incoming,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
if (!incoming.IsOutgoing || string.IsNullOrWhiteSpace(incoming.ExternalId))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var candidates = await db.Messages
|
||||
.Include(x => x.Attachments)
|
||||
.Where(x =>
|
||||
x.ChatId == chatId &&
|
||||
x.DeletedAt == null &&
|
||||
x.Direction == MessageDirection.Outgoing &&
|
||||
(x.DeliveryState == MessageDeliveryState.Sending ||
|
||||
x.DeliveryState == MessageDeliveryState.Failed) &&
|
||||
(x.ExternalId == null || x.ExternalId == ""))
|
||||
.ToListAsync(cancellationToken);
|
||||
if (candidates.Count == 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var incomingAttachments = DistinctAttachments(incoming.Attachments ?? []);
|
||||
var incomingText = MessageTextSanitizer.CleanMessageText(
|
||||
incoming.Text,
|
||||
incomingAttachments.Count > 0);
|
||||
|
||||
return candidates
|
||||
.Where(candidate => IsRecentOutgoingEchoCandidate(candidate, incoming))
|
||||
.Where(candidate => IsPendingOutgoingEchoMatch(candidate, incomingAttachments, incomingText))
|
||||
.OrderBy(candidate => Math.Abs((candidate.SentAt.ToUniversalTime() - incoming.SentAt.ToUniversalTime()).TotalSeconds))
|
||||
.FirstOrDefault();
|
||||
}
|
||||
|
||||
private static bool IsRecentOutgoingEchoCandidate(Message candidate, MaxMessageUpdate incoming)
|
||||
{
|
||||
var candidateAt = candidate.SentAt.ToUniversalTime();
|
||||
var incomingAt = incoming.SentAt.ToUniversalTime();
|
||||
return Math.Abs((candidateAt - incomingAt).TotalMinutes) <= 30 ||
|
||||
candidateAt >= DateTimeOffset.UtcNow.AddMinutes(-30);
|
||||
}
|
||||
|
||||
private static bool IsPendingOutgoingEchoMatch(
|
||||
Message candidate,
|
||||
IReadOnlyList<MaxAttachmentUpdate> incomingAttachments,
|
||||
string? incomingText)
|
||||
{
|
||||
var localAttachments = candidate.Attachments.OrderBy(x => x.SortOrder).ToArray();
|
||||
var localText = MessageTextSanitizer.CleanMessageText(candidate.Text, localAttachments.Length > 0);
|
||||
|
||||
if (incomingAttachments.Count == 0)
|
||||
{
|
||||
return localAttachments.Length == 0 &&
|
||||
string.Equals(localText, incomingText, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
if (localAttachments.Length == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(localText) &&
|
||||
!string.IsNullOrWhiteSpace(incomingText) &&
|
||||
!string.Equals(localText, incomingText, StringComparison.Ordinal))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return incomingAttachments.All(incomingAttachment =>
|
||||
{
|
||||
var kind = ResolveKind(incomingAttachment) ?? AttachmentKind.File;
|
||||
return localAttachments.Any(localAttachment =>
|
||||
localAttachment.Kind == kind &&
|
||||
(localAttachment.SortOrder == incomingAttachment.SortOrder ||
|
||||
localAttachments.Length == 1 ||
|
||||
incomingAttachments.Count == 1));
|
||||
});
|
||||
}
|
||||
|
||||
private static async Task<MessageMergeResult> MergeExistingMessageAsync(
|
||||
QMaxDbContext db,
|
||||
Message message,
|
||||
|
||||
Reference in New Issue
Block a user