Fix MAX emoji attachments
This commit is contained in:
+193
-11
@@ -11,6 +11,7 @@ const headless = (process.env.MAX_HEADLESS || "true").toLowerCase() !== "false";
|
||||
const domDownloadPrefix = "qmax-dom-download:";
|
||||
const domAudioPrefix = "qmax-dom-audio:";
|
||||
const domStickerPrefix = "qmax-dom-sticker:";
|
||||
const domEmojiPrefix = "qmax-dom-emoji:";
|
||||
|
||||
let context;
|
||||
let page;
|
||||
@@ -528,7 +529,7 @@ function errorStatus(error) {
|
||||
function isAllowedMediaUrl(value) {
|
||||
const raw = String(value || "").trim();
|
||||
if (!raw) return false;
|
||||
if (isDomDownloadUrl(raw) || isDomAudioUrl(raw) || isDomStickerUrl(raw)) return true;
|
||||
if (isDomDownloadUrl(raw) || isDomAudioUrl(raw) || isDomStickerUrl(raw) || isDomEmojiUrl(raw)) return true;
|
||||
if (raw.startsWith("blob:")) {
|
||||
try {
|
||||
const inner = new URL(raw.slice("blob:".length));
|
||||
@@ -558,6 +559,10 @@ function isDomStickerUrl(value) {
|
||||
return String(value || "").startsWith(domStickerPrefix);
|
||||
}
|
||||
|
||||
function isDomEmojiUrl(value) {
|
||||
return String(value || "").startsWith(domEmojiPrefix);
|
||||
}
|
||||
|
||||
function parseDomDownloadUrl(value) {
|
||||
const raw = String(value || "");
|
||||
if (!isDomDownloadUrl(raw)) {
|
||||
@@ -597,6 +602,19 @@ function parseDomStickerUrl(value) {
|
||||
}
|
||||
}
|
||||
|
||||
function parseDomEmojiUrl(value) {
|
||||
const raw = String(value || "");
|
||||
if (!isDomEmojiUrl(raw)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
return JSON.parse(decodeURIComponent(raw.slice(domEmojiPrefix.length)));
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function isPrivateHost(hostname) {
|
||||
const host = String(hostname || "").toLowerCase();
|
||||
if (!host || host === "localhost" || host.endsWith(".localhost")) return true;
|
||||
@@ -621,6 +639,17 @@ async function fetchMediaThroughMaxSession(p, mediaUrl) {
|
||||
return await downloadDomStickerThroughScreenshot(p, parseDomStickerUrl(mediaUrl));
|
||||
}
|
||||
|
||||
if (isDomEmojiUrl(mediaUrl)) {
|
||||
return await downloadDomStickerThroughScreenshot(p, {
|
||||
...parseDomEmojiUrl(mediaUrl),
|
||||
selector: "[data-testid*='emoji' i], [class*='emoji' i], [class*='lottie' i], [class*='emoticon' i], canvas, svg",
|
||||
kindLabel: "MAX emoji canvas",
|
||||
acceptSmall: true,
|
||||
maxClipSize: 256,
|
||||
requireTimeMatch: true
|
||||
});
|
||||
}
|
||||
|
||||
if (mediaUrl.startsWith("blob:")) {
|
||||
const result = await p.evaluate(async (url) => {
|
||||
const response = await fetch(url);
|
||||
@@ -876,12 +905,16 @@ async function downloadDomAudioThroughPlay(p, request) {
|
||||
async function downloadDomStickerThroughScreenshot(p, request) {
|
||||
const testId = String(request?.testId || "").trim();
|
||||
const timeText = String(request?.timeText || "").trim();
|
||||
const fallbackSelector = String(request?.selector || "[data-testid^='sticker-'], button[class*='sticker' i], [class*='sticker' i] canvas, canvas");
|
||||
const kindLabel = String(request?.kindLabel || "MAX sticker canvas");
|
||||
const selectorForTestId = (value) => `[data-testid="${value.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"]`;
|
||||
const screenshotCandidate = async (handles, requestedIndex) => {
|
||||
const candidates = [];
|
||||
for (const handle of handles) {
|
||||
const meta = await handle.evaluate((node, requestedTimeText) => {
|
||||
const element = node instanceof HTMLElement ? node : node.parentElement;
|
||||
const meta = await handle.evaluate((node, options) => {
|
||||
const requestedTimeText = String(options?.timeText || "");
|
||||
const acceptSmall = Boolean(options?.acceptSmall);
|
||||
const element = node instanceof Element ? node : node.parentElement;
|
||||
if (!element) {
|
||||
return null;
|
||||
}
|
||||
@@ -924,27 +957,29 @@ async function downloadDomStickerThroughScreenshot(p, request) {
|
||||
wrapper.className,
|
||||
wrapperText
|
||||
].filter(Boolean).join(" ").toLowerCase();
|
||||
const looksLikeSticker = /sticker|\u0441\u0442\u0438\u043a\u0435\u0440/.test(semantic) ||
|
||||
const looksLikeSticker = /sticker|\u0441\u0442\u0438\u043a\u0435\u0440|emoji|\u044d\u043c\u043e\u0434\u0437\u0438|lottie|emoticon/.test(semantic) ||
|
||||
Boolean(element.querySelector("canvas,img,svg")) ||
|
||||
element.matches("canvas,img,svg");
|
||||
const area = rect.width * rect.height;
|
||||
let score = 0;
|
||||
if (looksLikeSticker) score += 4;
|
||||
if (timeMatches) score += 40;
|
||||
if (rect.width >= 72 && rect.height >= 72) score += 16;
|
||||
if (acceptSmall && element.matches("canvas,img,svg")) score += 8;
|
||||
if (rect.width >= (acceptSmall ? 20 : 72) && rect.height >= (acceptSmall ? 20 : 72)) score += 16;
|
||||
if (rect.width >= 120 && rect.height >= 120) score += 8;
|
||||
score += Math.min(12, area / 2500);
|
||||
if (rect.width < 40 || rect.height < 40) score -= 30;
|
||||
if (area < 4096) score -= 20;
|
||||
if (rect.width < (acceptSmall ? 12 : 40) || rect.height < (acceptSmall ? 12 : 40)) score -= 30;
|
||||
if (area < (acceptSmall ? 256 : 4096)) score -= 20;
|
||||
return {
|
||||
score,
|
||||
x: rect.x,
|
||||
width: rect.width,
|
||||
height: rect.height,
|
||||
y: rect.y,
|
||||
area,
|
||||
timeMatches: Boolean(timeMatches)
|
||||
};
|
||||
}, timeText).catch(() => null);
|
||||
}, { timeText, acceptSmall: Boolean(request?.acceptSmall) }).catch(() => null);
|
||||
|
||||
if (!meta || meta.score <= 0) {
|
||||
await handle.dispose().catch(() => {});
|
||||
@@ -957,6 +992,12 @@ async function downloadDomStickerThroughScreenshot(p, request) {
|
||||
const strict = timeText
|
||||
? candidates.filter((candidate) => candidate.meta.timeMatches)
|
||||
: candidates;
|
||||
if (timeText && request?.requireTimeMatch && strict.length === 0) {
|
||||
for (const item of candidates) {
|
||||
await item.handle.dispose().catch(() => {});
|
||||
}
|
||||
return null;
|
||||
}
|
||||
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))];
|
||||
@@ -964,8 +1005,68 @@ async function downloadDomStickerThroughScreenshot(p, request) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const screenshotClip = async (box) => {
|
||||
if (!Number.isFinite(box?.x) ||
|
||||
!Number.isFinite(box?.y) ||
|
||||
!Number.isFinite(box?.width) ||
|
||||
!Number.isFinite(box?.height)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const viewport = p.viewportSize() || { width: 1280, height: 720 };
|
||||
const x = Math.max(0, Math.floor(box.x));
|
||||
const y = Math.max(0, Math.floor(box.y));
|
||||
const width = Math.min(Math.ceil(box.width), Math.max(1, viewport.width - x));
|
||||
const height = Math.min(Math.ceil(box.height), Math.max(1, viewport.height - y));
|
||||
if (width <= 0 || height <= 0) {
|
||||
return null;
|
||||
}
|
||||
const maxClipSize = Number(request?.maxClipSize || 0);
|
||||
if (maxClipSize > 0 && (width > maxClipSize || height > maxClipSize)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return await p.screenshot({
|
||||
type: "png",
|
||||
clip: { x, y, width, height },
|
||||
timeout: 10000
|
||||
});
|
||||
};
|
||||
|
||||
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;
|
||||
};
|
||||
await target.handle.scrollIntoViewIfNeeded({ timeout: 3000 }).catch(() => {});
|
||||
const buffer = await target.handle.screenshot({ type: "png", timeout: 10000 });
|
||||
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) {
|
||||
try {
|
||||
buffer = await target.handle.screenshot({ type: "png", timeout: 5000 });
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
for (const item of candidates) {
|
||||
await item.handle.dispose().catch(() => {});
|
||||
}
|
||||
@@ -981,10 +1082,10 @@ async function downloadDomStickerThroughScreenshot(p, request) {
|
||||
}
|
||||
}
|
||||
|
||||
const handles = await p.$$("[data-testid^='sticker-'], button[class*='sticker' i], [class*='sticker' i] canvas, canvas");
|
||||
const handles = await p.$$(fallbackSelector);
|
||||
const result = await screenshotCandidate(handles, index);
|
||||
if (!result) {
|
||||
throw new Error("MAX sticker canvas was not found.");
|
||||
throw new Error(`${kindLabel} was not found.`);
|
||||
}
|
||||
|
||||
return result;
|
||||
@@ -1730,6 +1831,11 @@ async function extractOpenChatHistory(p, requestedExternalChatId) {
|
||||
timeText: cleanText(timeText, 40),
|
||||
index
|
||||
}))}`;
|
||||
const domEmojiUrlFromCanvas = (testId, timeText, index) => `qmax-dom-emoji:${encodeURIComponent(JSON.stringify({
|
||||
testId: cleanText(testId, 120),
|
||||
timeText: cleanText(timeText, 40),
|
||||
index
|
||||
}))}`;
|
||||
const durationFromText = (value) => cleanText(value, 120).match(/\b\d{1,2}:\d{2}(?::\d{2})?\b/)?.[0] || "";
|
||||
const messageTimeTextFromWrapper = (wrapper) => {
|
||||
const metaNodes = Array.from(wrapper.querySelectorAll("[class*='meta' i], time"))
|
||||
@@ -1760,6 +1866,10 @@ async function extractOpenChatHistory(p, requestedExternalChatId) {
|
||||
const suffix = cleanText(testId, 80).replace(/[^a-z0-9_-]+/gi, "-").replace(/^-+|-+$/g, "");
|
||||
return `max-sticker-${suffix || index + 1}.png`;
|
||||
};
|
||||
const safeEmojiFileName = (testId, index) => {
|
||||
const suffix = cleanText(testId, 80).replace(/[^a-z0-9_-]+/gi, "-").replace(/^-+|-+$/g, "");
|
||||
return `max-emoji-${suffix || index + 1}.png`;
|
||||
};
|
||||
const fileSizeBytesFromLabel = (label) => {
|
||||
const match = cleanText(label, 300).match(/(\d+(?:[.,]\d+)?)\s*(b|kb|mb|gb|\u0431|\u043a\u0431|\u043c\u0431|\u0433\u0431)\b/i);
|
||||
if (!match) return null;
|
||||
@@ -1966,6 +2076,78 @@ async function extractOpenChatHistory(p, requestedExternalChatId) {
|
||||
});
|
||||
}
|
||||
|
||||
const emojiSelector = "[data-testid*='emoji' i], [class*='emoji' i], [class*='lottie' i], [class*='emoticon' i], canvas";
|
||||
const emojiRoots = [
|
||||
...(bubble.matches?.(emojiSelector) ? [bubble] : []),
|
||||
...Array.from(bubble.querySelectorAll(emojiSelector))
|
||||
];
|
||||
const seenEmojiKeys = new Set();
|
||||
for (const root of emojiRoots) {
|
||||
if (!(root instanceof HTMLElement)) continue;
|
||||
if (contactRoots.some((contactRoot) => contactRoot !== root && contactRoot.contains?.(root))) continue;
|
||||
if (root.closest?.("[data-testid^='sticker-'], [class*='sticker' i]")) continue;
|
||||
|
||||
const rootText = cleanText(
|
||||
root.innerText ||
|
||||
root.textContent ||
|
||||
root.getAttribute?.("aria-label") ||
|
||||
root.getAttribute?.("title"),
|
||||
300);
|
||||
const semantic = [
|
||||
root.getAttribute?.("data-testid"),
|
||||
root.getAttribute?.("aria-label"),
|
||||
root.getAttribute?.("title"),
|
||||
root.className?.baseVal || root.className,
|
||||
rootText,
|
||||
bubbleText
|
||||
].filter(Boolean).join(" ");
|
||||
const hasVisual = root.matches("canvas,svg,img,image") || Boolean(root.querySelector("canvas,svg,img,image"));
|
||||
const looksLikeEmoji = /emoji|\u044d\u043c\u043e\u0434\u0437\u0438|lottie|emoticon/i.test(semantic);
|
||||
if (!looksLikeEmoji || !hasVisual) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const directNode = root.matches("img,image,source")
|
||||
? root
|
||||
: root.querySelector("img,image,source");
|
||||
const directSource = directNode?.currentSrc ||
|
||||
directNode?.src ||
|
||||
directNode?.href?.baseVal ||
|
||||
directNode?.getAttribute?.("src") ||
|
||||
directNode?.getAttribute?.("href") ||
|
||||
directNode?.getAttribute?.("xlink:href") ||
|
||||
srcFromSrcset(directNode?.getAttribute?.("srcset")) ||
|
||||
cssBackgroundUrl(root);
|
||||
const directUrl = absoluteUrl(directSource);
|
||||
if (directUrl && !directUrl.startsWith("blob:")) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const wrapper = bubble.closest("[class*='messageWrapper' i]") || bubble;
|
||||
const messageTimeText = messageTimeTextFromWrapper(wrapper);
|
||||
const testId = root.getAttribute("data-testid") ||
|
||||
root.querySelector("[data-testid*='emoji' i], [data-testid*='lottie' i]")?.getAttribute("data-testid") ||
|
||||
"";
|
||||
const rect = root.getBoundingClientRect();
|
||||
if (rect.width < 12 || rect.height < 12) {
|
||||
continue;
|
||||
}
|
||||
const emojiKey = testId || `${Math.round(rect.x)}:${Math.round(rect.y)}:${Math.round(rect.width)}:${Math.round(rect.height)}`;
|
||||
if (seenEmojiKeys.has(emojiKey)) {
|
||||
continue;
|
||||
}
|
||||
seenEmojiKeys.add(emojiKey);
|
||||
const label = cleanText(root.getAttribute("aria-label") || rootText || "\u042d\u043c\u043e\u0434\u0437\u0438", 120);
|
||||
candidates.push({
|
||||
url: domEmojiUrlFromCanvas(testId, messageTimeText, candidates.length),
|
||||
label,
|
||||
kind: "Sticker",
|
||||
fileName: safeEmojiFileName(testId, candidates.length),
|
||||
contentType: "image/png",
|
||||
fileSizeBytes: null
|
||||
});
|
||||
}
|
||||
|
||||
const stickerRoots = [
|
||||
...(bubble.matches?.("[data-testid^='sticker-'], button[class*='sticker' i], [class*='sticker' i]") ? [bubble] : []),
|
||||
...Array.from(bubble.querySelectorAll("[data-testid^='sticker-'], button[class*='sticker' i], [class*='sticker' i]"))
|
||||
|
||||
Reference in New Issue
Block a user