Prevent empty MAX media placeholders

This commit is contained in:
sevenhill
2026-07-06 14:40:25 +03:00
parent 057d4346fe
commit df429d5c43
2 changed files with 179 additions and 55 deletions
+111 -10
View File
@@ -327,7 +327,15 @@ public sealed class MaxBridgeSyncService(
foreach (var incomingAttachment in DistinctAttachments(incoming.Attachments ?? []))
{
var attachment = await CreateAttachmentAsync(incomingAttachment, maxBridgeClient, storage, cancellationToken);
message.Attachments.Add(attachment);
if (attachment is not null)
{
message.Attachments.Add(attachment);
}
}
if (ShouldDeferMediaOnlyMessage(message, incoming.Attachments ?? []))
{
continue;
}
db.Messages.Add(message);
@@ -898,12 +906,25 @@ public sealed class MaxBridgeSyncService(
continue;
}
if (HasAttachment(knownAttachments, incomingAttachment))
var existingAttachment = FindMatchingAttachment(knownAttachments, incomingAttachment);
if (existingAttachment is not null)
{
if (IsUncachedRemotePlaceholder(existingAttachment) &&
RequiresCachedRemoteMedia(existingAttachment.Kind) &&
await TryHydrateRemoteAttachmentAsync(existingAttachment, incomingAttachment, maxBridgeClient, storage, cancellationToken))
{
changed = true;
}
continue;
}
var attachment = await CreateAttachmentAsync(incomingAttachment, maxBridgeClient, storage, cancellationToken);
if (attachment is null)
{
continue;
}
attachment.MessageId = message.Id;
db.MessageAttachments.Add(attachment);
knownAttachments.Add(attachment);
@@ -997,7 +1018,7 @@ public sealed class MaxBridgeSyncService(
return $"fallback:{attachment.Kind}:{attachment.FileName}:{attachment.SortOrder}:{attachment.TextContent?.Length ?? 0}";
}
private static async Task<MessageAttachment> CreateAttachmentAsync(
private static async Task<MessageAttachment?> CreateAttachmentAsync(
MaxAttachmentUpdate incoming,
IMaxBridgeClient maxBridgeClient,
IAttachmentStorageService storage,
@@ -1068,7 +1089,12 @@ public sealed class MaxBridgeSyncService(
}
catch
{
// Keep the message visible even if a specific remote media item cannot be cached.
// Non-visual files can stay as deferred downloads; inline media must be cached before it is shown.
}
if (RequiresCachedRemoteMedia(kind))
{
return null;
}
}
@@ -1085,28 +1111,103 @@ public sealed class MaxBridgeSyncService(
};
}
private static bool HasAttachment(IEnumerable<MessageAttachment> attachments, MaxAttachmentUpdate incoming)
private static MessageAttachment? FindMatchingAttachment(IEnumerable<MessageAttachment> attachments, MaxAttachmentUpdate incoming)
{
if (!string.IsNullOrWhiteSpace(incoming.ExternalId) &&
attachments.Any(x => string.Equals(x.ExternalId, incoming.ExternalId, StringComparison.Ordinal)))
attachments.FirstOrDefault(x => string.Equals(x.ExternalId, incoming.ExternalId, StringComparison.Ordinal)) is { } externalMatch)
{
return true;
return externalMatch;
}
if (!string.IsNullOrWhiteSpace(incoming.RemoteUrl) &&
attachments.Any(x => string.Equals(x.RemoteUrl, incoming.RemoteUrl, StringComparison.Ordinal)))
attachments.FirstOrDefault(x => string.Equals(x.RemoteUrl, incoming.RemoteUrl, StringComparison.Ordinal)) is { } remoteMatch)
{
return true;
return remoteMatch;
}
var kind = ResolveKind(incoming) ?? AttachmentKind.File;
var normalizedFileName = AttachmentStorageService.NormalizeRemoteFileName(incoming.FileName, incoming.ContentType, kind);
return attachments.Any(x =>
return attachments.FirstOrDefault(x =>
x.SortOrder == incoming.SortOrder &&
x.Kind == kind &&
string.Equals(x.OriginalFileName, normalizedFileName, StringComparison.OrdinalIgnoreCase));
}
private static bool ShouldDeferMediaOnlyMessage(Message message, IReadOnlyList<MaxAttachmentUpdate> incomingAttachments)
{
return incomingAttachments.Count > 0 &&
message.Attachments.Count == 0 &&
string.IsNullOrWhiteSpace(message.Text) &&
incomingAttachments.Any(attachment => RequiresCachedRemoteMedia(ResolveKind(attachment)));
}
private static bool RequiresCachedRemoteMedia(AttachmentKind? kind)
{
return kind is AttachmentKind.Image or
AttachmentKind.Gif or
AttachmentKind.Video or
AttachmentKind.VoiceNote or
AttachmentKind.Sticker;
}
private static bool IsUncachedRemotePlaceholder(MessageAttachment attachment)
{
return !string.IsNullOrWhiteSpace(attachment.RemoteUrl) &&
attachment.StorageFileName.StartsWith("remote-", StringComparison.Ordinal) &&
(attachment.FileSizeBytes <= 0 || string.IsNullOrWhiteSpace(attachment.Sha256));
}
private static async Task<bool> TryHydrateRemoteAttachmentAsync(
MessageAttachment attachment,
MaxAttachmentUpdate incoming,
IMaxBridgeClient maxBridgeClient,
IAttachmentStorageService storage,
CancellationToken cancellationToken)
{
var remoteUrl = string.IsNullOrWhiteSpace(incoming.RemoteUrl) ? attachment.RemoteUrl : incoming.RemoteUrl;
if (string.IsNullOrWhiteSpace(remoteUrl))
{
return false;
}
var kind = ResolveKind(incoming) ?? attachment.Kind;
try
{
var download = await maxBridgeClient.DownloadMediaAsync(remoteUrl, cancellationToken);
if (download is null)
{
return false;
}
await using (download.Stream)
{
var stored = await storage.SaveRemoteAsync(
incoming.FileName,
download.ContentType,
download.Stream,
download.ContentLength ?? incoming.FileSizeBytes,
kind,
cancellationToken);
attachment.ExternalId = string.IsNullOrWhiteSpace(incoming.ExternalId) ? attachment.ExternalId : incoming.ExternalId;
attachment.OriginalFileName = stored.OriginalFileName;
attachment.StorageFileName = stored.StorageFileName;
attachment.ContentType = stored.ContentType;
attachment.FileSizeBytes = stored.FileSizeBytes;
attachment.Sha256 = stored.Sha256;
attachment.Kind = stored.Kind;
attachment.SortOrder = incoming.SortOrder;
attachment.RemoteUrl = remoteUrl;
}
return true;
}
catch
{
return false;
}
}
private static AttachmentKind? ResolveKind(MaxAttachmentUpdate attachment)
{
if (!string.IsNullOrWhiteSpace(attachment.Kind) &&