Prevent empty MAX media placeholders
This commit is contained in:
@@ -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
@@ -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.`);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user