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) &&
+68 -45
View File
@@ -1302,10 +1302,10 @@ async function downloadDomStickerThroughScreenshot(p, request) {
const resolved = [];
for (const handle of handles) {
const visual = await visualHandleFor(handle);
if (visual !== handle) {
await handle.dispose().catch(() => {});
}
resolved.push(visual);
if (visual !== handle) {
resolved.push(handle);
}
}
return resolved;
};
@@ -1557,10 +1557,14 @@ async function downloadDomStickerThroughScreenshot(p, request) {
}
const sorted = (strict.length ? strict : candidates)
.sort((a, b) => b.meta.score - a.meta.score || b.meta.area - a.meta.area || b.meta.y - a.meta.y);
const target = sorted[Math.max(0, Math.min(requestedIndex, sorted.length - 1))];
if (!target) {
if (sorted.length === 0) {
return null;
}
const selectedIndex = Math.max(0, Math.min(requestedIndex, sorted.length - 1));
const orderedTargets = [
...sorted.slice(selectedIndex),
...sorted.slice(0, selectedIndex)
];
const screenshotClip = async (box) => {
if (!Number.isFinite(box?.x) ||
@@ -1590,47 +1594,57 @@ async function downloadDomStickerThroughScreenshot(p, request) {
});
};
const metaBox = {
x: target.meta.x,
y: target.meta.y,
width: target.meta.width,
height: target.meta.height
};
const currentBoxOrMeta = async () => {
const box = await target.handle.boundingBox().catch(() => null);
return box && box.width > 0 && box.height > 0 ? box : metaBox;
};
const captureErrors = [];
try {
await target.handle.scrollIntoViewIfNeeded({ timeout: 3000 }).catch(() => {});
const canvasCapture = await captureCanvasPng(target.handle);
if (canvasCapture.buffer) {
return await stickerCaptureResult(canvasCapture.buffer);
for (const target of orderedTargets) {
const metaBox = {
x: target.meta.x,
y: target.meta.y,
width: target.meta.width,
height: target.meta.height
};
const currentBoxOrMeta = async () => {
const box = await target.handle.boundingBox().catch(() => null);
return box && box.width > 0 && box.height > 0 ? box : metaBox;
};
try {
await target.handle.scrollIntoViewIfNeeded({ timeout: 3000 }).catch(() => {});
const canvasCapture = await captureCanvasPng(target.handle);
if (canvasCapture.buffer) {
return await stickerCaptureResult(canvasCapture.buffer);
}
let buffer;
if (request?.acceptSmall) {
buffer = await screenshotClip(await currentBoxOrMeta());
if (!buffer) {
throw new Error(`${kindLabel} clip was not visible.`);
}
} else {
try {
buffer = await target.handle.screenshot({ type: "png", timeout: 5000 });
} catch (error) {
buffer = await screenshotClip(await currentBoxOrMeta());
if (!buffer) {
throw error;
}
}
}
if (!buffer) {
buffer = await target.handle.screenshot({ type: "png", timeout: 5000 });
}
return await stickerCaptureResult(buffer);
} catch (error) {
captureErrors.push(error?.message || String(error));
}
}
let buffer;
if (request?.acceptSmall) {
buffer = await screenshotClip(await currentBoxOrMeta());
if (!buffer) {
throw new Error(`${kindLabel} clip was not visible.`);
}
} else {
try {
buffer = await target.handle.screenshot({ type: "png", timeout: 5000 });
} catch (error) {
buffer = await screenshotClip(await currentBoxOrMeta());
if (!buffer) {
throw error;
}
}
if (captureErrors.length > 0) {
throw new Error(`${kindLabel} was not captured: ${captureErrors.slice(0, 5).join("; ")}`);
}
if (!buffer) {
try {
buffer = await target.handle.screenshot({ type: "png", timeout: 5000 });
} catch (error) {
throw error;
}
}
return await stickerCaptureResult(buffer);
return null;
} finally {
for (const item of candidates) {
await item.handle.dispose().catch(() => {});
@@ -1639,17 +1653,26 @@ async function downloadDomStickerThroughScreenshot(p, request) {
};
const index = Number.isFinite(Number(request?.index)) ? Number(request.index) : 0;
let testIdCaptureError = null;
if (testId) {
const handles = await p.$$(selectorForTestId(testId));
const result = await screenshotCandidate(await resolveVisualHandles(handles), 0);
if (result) {
return result;
try {
const result = await screenshotCandidate(await resolveVisualHandles(handles), 0);
if (result) {
return result;
}
} catch (error) {
testIdCaptureError = error;
}
}
const handles = await p.$$(fallbackSelector);
const result = await screenshotCandidate(await resolveVisualHandles(handles), index);
if (!result) {
if (testIdCaptureError) {
throw testIdCaptureError;
}
throw new Error(`${kindLabel} was not found.`);
}