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 ?? []))
|
foreach (var incomingAttachment in DistinctAttachments(incoming.Attachments ?? []))
|
||||||
{
|
{
|
||||||
var attachment = await CreateAttachmentAsync(incomingAttachment, maxBridgeClient, storage, cancellationToken);
|
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);
|
db.Messages.Add(message);
|
||||||
@@ -898,12 +906,25 @@ public sealed class MaxBridgeSyncService(
|
|||||||
continue;
|
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;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
var attachment = await CreateAttachmentAsync(incomingAttachment, maxBridgeClient, storage, cancellationToken);
|
var attachment = await CreateAttachmentAsync(incomingAttachment, maxBridgeClient, storage, cancellationToken);
|
||||||
|
if (attachment is null)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
attachment.MessageId = message.Id;
|
attachment.MessageId = message.Id;
|
||||||
db.MessageAttachments.Add(attachment);
|
db.MessageAttachments.Add(attachment);
|
||||||
knownAttachments.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}";
|
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,
|
MaxAttachmentUpdate incoming,
|
||||||
IMaxBridgeClient maxBridgeClient,
|
IMaxBridgeClient maxBridgeClient,
|
||||||
IAttachmentStorageService storage,
|
IAttachmentStorageService storage,
|
||||||
@@ -1068,7 +1089,12 @@ public sealed class MaxBridgeSyncService(
|
|||||||
}
|
}
|
||||||
catch
|
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) &&
|
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) &&
|
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 kind = ResolveKind(incoming) ?? AttachmentKind.File;
|
||||||
var normalizedFileName = AttachmentStorageService.NormalizeRemoteFileName(incoming.FileName, incoming.ContentType, kind);
|
var normalizedFileName = AttachmentStorageService.NormalizeRemoteFileName(incoming.FileName, incoming.ContentType, kind);
|
||||||
return attachments.Any(x =>
|
return attachments.FirstOrDefault(x =>
|
||||||
x.SortOrder == incoming.SortOrder &&
|
x.SortOrder == incoming.SortOrder &&
|
||||||
x.Kind == kind &&
|
x.Kind == kind &&
|
||||||
string.Equals(x.OriginalFileName, normalizedFileName, StringComparison.OrdinalIgnoreCase));
|
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)
|
private static AttachmentKind? ResolveKind(MaxAttachmentUpdate attachment)
|
||||||
{
|
{
|
||||||
if (!string.IsNullOrWhiteSpace(attachment.Kind) &&
|
if (!string.IsNullOrWhiteSpace(attachment.Kind) &&
|
||||||
|
|||||||
+68
-45
@@ -1302,10 +1302,10 @@ async function downloadDomStickerThroughScreenshot(p, request) {
|
|||||||
const resolved = [];
|
const resolved = [];
|
||||||
for (const handle of handles) {
|
for (const handle of handles) {
|
||||||
const visual = await visualHandleFor(handle);
|
const visual = await visualHandleFor(handle);
|
||||||
if (visual !== handle) {
|
|
||||||
await handle.dispose().catch(() => {});
|
|
||||||
}
|
|
||||||
resolved.push(visual);
|
resolved.push(visual);
|
||||||
|
if (visual !== handle) {
|
||||||
|
resolved.push(handle);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return resolved;
|
return resolved;
|
||||||
};
|
};
|
||||||
@@ -1557,10 +1557,14 @@ async function downloadDomStickerThroughScreenshot(p, request) {
|
|||||||
}
|
}
|
||||||
const sorted = (strict.length ? strict : candidates)
|
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);
|
.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 (sorted.length === 0) {
|
||||||
if (!target) {
|
|
||||||
return null;
|
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) => {
|
const screenshotClip = async (box) => {
|
||||||
if (!Number.isFinite(box?.x) ||
|
if (!Number.isFinite(box?.x) ||
|
||||||
@@ -1590,47 +1594,57 @@ async function downloadDomStickerThroughScreenshot(p, request) {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const metaBox = {
|
const captureErrors = [];
|
||||||
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 {
|
try {
|
||||||
await target.handle.scrollIntoViewIfNeeded({ timeout: 3000 }).catch(() => {});
|
for (const target of orderedTargets) {
|
||||||
const canvasCapture = await captureCanvasPng(target.handle);
|
const metaBox = {
|
||||||
if (canvasCapture.buffer) {
|
x: target.meta.x,
|
||||||
return await stickerCaptureResult(canvasCapture.buffer);
|
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 (captureErrors.length > 0) {
|
||||||
if (request?.acceptSmall) {
|
throw new Error(`${kindLabel} was not captured: ${captureErrors.slice(0, 5).join("; ")}`);
|
||||||
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) {
|
|
||||||
try {
|
return null;
|
||||||
buffer = await target.handle.screenshot({ type: "png", timeout: 5000 });
|
|
||||||
} catch (error) {
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return await stickerCaptureResult(buffer);
|
|
||||||
} finally {
|
} finally {
|
||||||
for (const item of candidates) {
|
for (const item of candidates) {
|
||||||
await item.handle.dispose().catch(() => {});
|
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;
|
const index = Number.isFinite(Number(request?.index)) ? Number(request.index) : 0;
|
||||||
|
let testIdCaptureError = null;
|
||||||
if (testId) {
|
if (testId) {
|
||||||
const handles = await p.$$(selectorForTestId(testId));
|
const handles = await p.$$(selectorForTestId(testId));
|
||||||
const result = await screenshotCandidate(await resolveVisualHandles(handles), 0);
|
try {
|
||||||
if (result) {
|
const result = await screenshotCandidate(await resolveVisualHandles(handles), 0);
|
||||||
return result;
|
if (result) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
testIdCaptureError = error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const handles = await p.$$(fallbackSelector);
|
const handles = await p.$$(fallbackSelector);
|
||||||
const result = await screenshotCandidate(await resolveVisualHandles(handles), index);
|
const result = await screenshotCandidate(await resolveVisualHandles(handles), index);
|
||||||
if (!result) {
|
if (!result) {
|
||||||
|
if (testIdCaptureError) {
|
||||||
|
throw testIdCaptureError;
|
||||||
|
}
|
||||||
|
|
||||||
throw new Error(`${kindLabel} was not found.`);
|
throw new Error(`${kindLabel} was not found.`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user