Handle MAX canvas stickers in sync
This commit is contained in:
@@ -267,12 +267,16 @@ public sealed class MaxBridgeSyncService(
|
|||||||
}
|
}
|
||||||
|
|
||||||
var previousPreview = chat.LastMessagePreview;
|
var previousPreview = chat.LastMessagePreview;
|
||||||
if (string.Equals(previousPreview, preview, StringComparison.Ordinal))
|
var previousPreviewAt = chat.LastMessageAt;
|
||||||
|
var previewAt = ResolveListPreviewAt(update);
|
||||||
|
var samePreview = string.Equals(previousPreview, preview, StringComparison.Ordinal);
|
||||||
|
var samePreviewAt = previousPreviewAt is not null &&
|
||||||
|
Math.Abs((previousPreviewAt.Value - previewAt).TotalSeconds) < 1;
|
||||||
|
if (samePreview && samePreviewAt)
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
var previewAt = ResolveListPreviewAt(update);
|
|
||||||
chat.LastMessagePreview = preview;
|
chat.LastMessagePreview = preview;
|
||||||
chat.LastMessageAt = previewAt;
|
chat.LastMessageAt = previewAt;
|
||||||
chat.UpdatedAt = DateTimeOffset.UtcNow;
|
chat.UpdatedAt = DateTimeOffset.UtcNow;
|
||||||
|
|||||||
+270
-15
@@ -10,6 +10,7 @@ const userDataDir = process.env.MAX_USER_DATA_DIR || "/data/max-profile";
|
|||||||
const headless = (process.env.MAX_HEADLESS || "true").toLowerCase() !== "false";
|
const headless = (process.env.MAX_HEADLESS || "true").toLowerCase() !== "false";
|
||||||
const domDownloadPrefix = "qmax-dom-download:";
|
const domDownloadPrefix = "qmax-dom-download:";
|
||||||
const domAudioPrefix = "qmax-dom-audio:";
|
const domAudioPrefix = "qmax-dom-audio:";
|
||||||
|
const domStickerPrefix = "qmax-dom-sticker:";
|
||||||
|
|
||||||
let context;
|
let context;
|
||||||
let page;
|
let page;
|
||||||
@@ -247,6 +248,7 @@ app.get("/updates", async (_req, res) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
await clearSidebarSearch(p);
|
await clearSidebarSearch(p);
|
||||||
|
await resetSidebarListToTop(p);
|
||||||
return await extractUpdates(p);
|
return await extractUpdates(p);
|
||||||
});
|
});
|
||||||
res.json(result);
|
res.json(result);
|
||||||
@@ -272,6 +274,7 @@ app.post("/chat/history", async (req, res) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await scrollOpenChatToLatest(p);
|
||||||
return await extractOpenChatHistory(p, externalChatId);
|
return await extractOpenChatHistory(p, externalChatId);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -525,7 +528,7 @@ function errorStatus(error) {
|
|||||||
function isAllowedMediaUrl(value) {
|
function isAllowedMediaUrl(value) {
|
||||||
const raw = String(value || "").trim();
|
const raw = String(value || "").trim();
|
||||||
if (!raw) return false;
|
if (!raw) return false;
|
||||||
if (isDomDownloadUrl(raw) || isDomAudioUrl(raw)) return true;
|
if (isDomDownloadUrl(raw) || isDomAudioUrl(raw) || isDomStickerUrl(raw)) return true;
|
||||||
if (raw.startsWith("blob:")) {
|
if (raw.startsWith("blob:")) {
|
||||||
try {
|
try {
|
||||||
const inner = new URL(raw.slice("blob:".length));
|
const inner = new URL(raw.slice("blob:".length));
|
||||||
@@ -551,6 +554,10 @@ function isDomAudioUrl(value) {
|
|||||||
return String(value || "").startsWith(domAudioPrefix);
|
return String(value || "").startsWith(domAudioPrefix);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isDomStickerUrl(value) {
|
||||||
|
return String(value || "").startsWith(domStickerPrefix);
|
||||||
|
}
|
||||||
|
|
||||||
function parseDomDownloadUrl(value) {
|
function parseDomDownloadUrl(value) {
|
||||||
const raw = String(value || "");
|
const raw = String(value || "");
|
||||||
if (!isDomDownloadUrl(raw)) {
|
if (!isDomDownloadUrl(raw)) {
|
||||||
@@ -577,6 +584,19 @@ function parseDomAudioUrl(value) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function parseDomStickerUrl(value) {
|
||||||
|
const raw = String(value || "");
|
||||||
|
if (!isDomStickerUrl(raw)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return JSON.parse(decodeURIComponent(raw.slice(domStickerPrefix.length)));
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function isPrivateHost(hostname) {
|
function isPrivateHost(hostname) {
|
||||||
const host = String(hostname || "").toLowerCase();
|
const host = String(hostname || "").toLowerCase();
|
||||||
if (!host || host === "localhost" || host.endsWith(".localhost")) return true;
|
if (!host || host === "localhost" || host.endsWith(".localhost")) return true;
|
||||||
@@ -597,6 +617,10 @@ async function fetchMediaThroughMaxSession(p, mediaUrl) {
|
|||||||
return await downloadDomAudioThroughPlay(p, parseDomAudioUrl(mediaUrl));
|
return await downloadDomAudioThroughPlay(p, parseDomAudioUrl(mediaUrl));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isDomStickerUrl(mediaUrl)) {
|
||||||
|
return await downloadDomStickerThroughScreenshot(p, parseDomStickerUrl(mediaUrl));
|
||||||
|
}
|
||||||
|
|
||||||
if (mediaUrl.startsWith("blob:")) {
|
if (mediaUrl.startsWith("blob:")) {
|
||||||
const result = await p.evaluate(async (url) => {
|
const result = await p.evaluate(async (url) => {
|
||||||
const response = await fetch(url);
|
const response = await fetch(url);
|
||||||
@@ -849,6 +873,42 @@ async function downloadDomAudioThroughPlay(p, request) {
|
|||||||
throw new Error(`MAX voice media response was not captured for ${label}.`);
|
throw new Error(`MAX voice media response was not captured for ${label}.`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function downloadDomStickerThroughScreenshot(p, request) {
|
||||||
|
const testId = String(request?.testId || "").trim();
|
||||||
|
if (testId) {
|
||||||
|
const selector = `[data-testid="${testId.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"]`;
|
||||||
|
const sticker = p.locator(selector).first();
|
||||||
|
if (await sticker.count()) {
|
||||||
|
await sticker.scrollIntoViewIfNeeded({ timeout: 3000 }).catch(() => {});
|
||||||
|
const buffer = await sticker.screenshot({ type: "png", timeout: 10000 });
|
||||||
|
return { contentType: "image/png", buffer };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const index = Number.isFinite(Number(request?.index)) ? Number(request.index) : 0;
|
||||||
|
const handles = await p.$$("[data-testid^='sticker-'], button[class*='sticker' i], [class*='sticker' i] canvas, canvas");
|
||||||
|
const visible = [];
|
||||||
|
for (const handle of handles) {
|
||||||
|
const box = await handle.boundingBox().catch(() => null);
|
||||||
|
if (box && box.width >= 24 && box.height >= 24) {
|
||||||
|
visible.push({ handle, box });
|
||||||
|
} else {
|
||||||
|
await handle.dispose().catch(() => {});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const target = visible[Math.max(0, Math.min(index, visible.length - 1))];
|
||||||
|
if (!target) {
|
||||||
|
throw new Error("MAX sticker canvas was not found.");
|
||||||
|
}
|
||||||
|
|
||||||
|
const buffer = await target.handle.screenshot({ type: "png", timeout: 10000 });
|
||||||
|
for (const item of visible) {
|
||||||
|
await item.handle.dispose().catch(() => {});
|
||||||
|
}
|
||||||
|
return { contentType: "image/png", buffer };
|
||||||
|
}
|
||||||
|
|
||||||
function contentTypeFromFileName(fileName) {
|
function contentTypeFromFileName(fileName) {
|
||||||
const ext = String(fileName || "").toLowerCase().split(".").pop();
|
const ext = String(fileName || "").toLowerCase().split(".").pop();
|
||||||
switch (ext) {
|
switch (ext) {
|
||||||
@@ -931,18 +991,46 @@ async function inspectDom(p) {
|
|||||||
rect.left <= window.innerWidth;
|
rect.left <= window.innerWidth;
|
||||||
};
|
};
|
||||||
|
|
||||||
const elements = Array.from(document.querySelectorAll("a,button,input,textarea,[role],[data-testid],[class]"))
|
const cssImageUrl = (element) => {
|
||||||
|
const style = window.getComputedStyle(element);
|
||||||
|
const value = [
|
||||||
|
style.backgroundImage,
|
||||||
|
style.maskImage,
|
||||||
|
style.webkitMaskImage
|
||||||
|
].find((item) => item && item !== "none") || "";
|
||||||
|
const match = value.match(/url\(["']?(.+?)["']?\)/i);
|
||||||
|
if (!match) return null;
|
||||||
|
try {
|
||||||
|
return new URL(match[1], location.href).href;
|
||||||
|
} catch {
|
||||||
|
return match[1];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const elements = Array.from(document.querySelectorAll("a,button,input,textarea,img,picture,video,audio,canvas,svg,[role],[data-testid],[class]"))
|
||||||
.filter(isVisible)
|
.filter(isVisible)
|
||||||
.map((element) => {
|
.map((element) => {
|
||||||
const rect = element.getBoundingClientRect();
|
const rect = element.getBoundingClientRect();
|
||||||
|
const tag = element.tagName.toLowerCase();
|
||||||
|
const mediaSrc = element.currentSrc ||
|
||||||
|
element.src ||
|
||||||
|
element.poster ||
|
||||||
|
element.href?.baseVal ||
|
||||||
|
element.getAttribute("src") ||
|
||||||
|
element.getAttribute("href") ||
|
||||||
|
element.getAttribute("xlink:href") ||
|
||||||
|
element.getAttribute("data-url") ||
|
||||||
|
element.getAttribute("data-src") ||
|
||||||
|
cssImageUrl(element);
|
||||||
return {
|
return {
|
||||||
tag: element.tagName.toLowerCase(),
|
tag,
|
||||||
role: element.getAttribute("role"),
|
role: element.getAttribute("role"),
|
||||||
id: element.id || null,
|
id: element.id || null,
|
||||||
className: cleanText(element.className, 180),
|
className: cleanText(element.className, 180),
|
||||||
testId: element.getAttribute("data-testid"),
|
testId: element.getAttribute("data-testid"),
|
||||||
ariaLabel: element.getAttribute("aria-label"),
|
ariaLabel: element.getAttribute("aria-label"),
|
||||||
href: element.getAttribute("href"),
|
href: element.getAttribute("href"),
|
||||||
|
mediaSrc,
|
||||||
text: cleanText(element.innerText || element.value || element.getAttribute("aria-label")),
|
text: cleanText(element.innerText || element.value || element.getAttribute("aria-label")),
|
||||||
rect: {
|
rect: {
|
||||||
x: Math.round(rect.x),
|
x: Math.round(rect.x),
|
||||||
@@ -952,7 +1040,15 @@ async function inspectDom(p) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
.filter((element) => element.text || element.ariaLabel || element.testId || element.role)
|
.filter((element) => {
|
||||||
|
const haystack = `${element.tag} ${element.className} ${element.mediaSrc || ""}`.toLowerCase();
|
||||||
|
return element.text ||
|
||||||
|
element.ariaLabel ||
|
||||||
|
element.testId ||
|
||||||
|
element.role ||
|
||||||
|
element.mediaSrc ||
|
||||||
|
/message|bubble|sticker|emoji|attach|media|image|picture|canvas|svg/.test(haystack);
|
||||||
|
})
|
||||||
.slice(0, 400);
|
.slice(0, 400);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -1186,6 +1282,18 @@ async function extractUpdates(p) {
|
|||||||
rect.top <= window.innerHeight &&
|
rect.top <= window.innerHeight &&
|
||||||
rect.left <= window.innerWidth;
|
rect.left <= window.innerWidth;
|
||||||
};
|
};
|
||||||
|
const isChatRowVisibleEnough = (element) => {
|
||||||
|
const style = window.getComputedStyle(element);
|
||||||
|
const rect = element.getBoundingClientRect();
|
||||||
|
return style.visibility !== "hidden" &&
|
||||||
|
style.display !== "none" &&
|
||||||
|
rect.width > 20 &&
|
||||||
|
rect.height > 12 &&
|
||||||
|
rect.bottom >= -140 &&
|
||||||
|
rect.right >= 0 &&
|
||||||
|
rect.top <= window.innerHeight &&
|
||||||
|
rect.left <= window.innerWidth;
|
||||||
|
};
|
||||||
const badText = /(\u0432\u043e\u0439\u0442\u0438|qr|\u043d\u0435 \u0440\u043e\u0431\u043e\u0442|\u043d\u043e\u043c\u0435\u0440 \u0442\u0435\u043b\u0435\u0444\u043e\u043d\u0430|\u043f\u043e\u043b\u0438\u0442\u0438\u043a|\u0441\u043e\u0433\u043b\u0430\u0448\u0435\u043d|\u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c \u043d\u043e\u0432\u044b\u0439 \u043a\u043e\u0434|\u0432\u0432\u0435\u0434\u0438\u0442\u0435 \u043a\u043e\u0434|\u043a\u043e\u0434 max|\u0441\u043c\u0441|sms)/i;
|
const badText = /(\u0432\u043e\u0439\u0442\u0438|qr|\u043d\u0435 \u0440\u043e\u0431\u043e\u0442|\u043d\u043e\u043c\u0435\u0440 \u0442\u0435\u043b\u0435\u0444\u043e\u043d\u0430|\u043f\u043e\u043b\u0438\u0442\u0438\u043a|\u0441\u043e\u0433\u043b\u0430\u0448\u0435\u043d|\u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c \u043d\u043e\u0432\u044b\u0439 \u043a\u043e\u0434|\u0432\u0432\u0435\u0434\u0438\u0442\u0435 \u043a\u043e\u0434|\u043a\u043e\u0434 max|\u0441\u043c\u0441|sms)/i;
|
||||||
const isGenericMediaText = (value) => /^(\u0444\u043e\u0442\u043e|photo|image|gif|\u0432\u0438\u0434\u0435\u043e|video|\u0430\u0443\u0434\u0438\u043e|audio|\u0433\u043e\u043b\u043e\u0441\u043e\u0432\u043e\u0435|voice|\u0441\u0442\u0438\u043a\u0435\u0440|sticker|\u044d\u043c\u043e\u0434\u0437\u0438|emoji|\u043f\u0440\u0438\u043a\u0440\u0435\u043f\u043b[\u0435\u0451]\u043d(?:\u043d\u044b\u0435|\u043d\u043e\u0435)?\s+\u0444\u043e\u0442\u043e)$/i.test(cleanText(value, 120));
|
const isGenericMediaText = (value) => /^(\u0444\u043e\u0442\u043e|photo|image|gif|\u0432\u0438\u0434\u0435\u043e|video|\u0430\u0443\u0434\u0438\u043e|audio|\u0433\u043e\u043b\u043e\u0441\u043e\u0432\u043e\u0435|voice|\u0441\u0442\u0438\u043a\u0435\u0440|sticker|\u044d\u043c\u043e\u0434\u0437\u0438|emoji|\u043f\u0440\u0438\u043a\u0440\u0435\u043f\u043b[\u0435\u0451]\u043d(?:\u043d\u044b\u0435|\u043d\u043e\u0435)?\s+\u0444\u043e\u0442\u043e)$/i.test(cleanText(value, 120));
|
||||||
const absoluteUrl = (value) => {
|
const absoluteUrl = (value) => {
|
||||||
@@ -1267,7 +1375,7 @@ async function extractUpdates(p) {
|
|||||||
const seen = new Set();
|
const seen = new Set();
|
||||||
const chats = [];
|
const chats = [];
|
||||||
for (const element of Array.from(document.querySelectorAll(chatSelector))) {
|
for (const element of Array.from(document.querySelectorAll(chatSelector))) {
|
||||||
if (!isVisible(element)) continue;
|
if (!isChatRowVisibleEnough(element)) continue;
|
||||||
const rect = element.getBoundingClientRect();
|
const rect = element.getBoundingClientRect();
|
||||||
const text = cleanText(element.innerText || element.textContent, 400);
|
const text = cleanText(element.innerText || element.textContent, 400);
|
||||||
if (!text || text.length < 2 || text.length > 380 || badText.test(text)) continue;
|
if (!text || text.length < 2 || text.length > 380 || badText.test(text)) continue;
|
||||||
@@ -1487,6 +1595,11 @@ async function extractOpenChatHistory(p, requestedExternalChatId) {
|
|||||||
duration: cleanText(duration, 40),
|
duration: cleanText(duration, 40),
|
||||||
timeText: cleanText(timeText, 40)
|
timeText: cleanText(timeText, 40)
|
||||||
}))}`;
|
}))}`;
|
||||||
|
const domStickerUrlFromCanvas = (testId, timeText, index) => `qmax-dom-sticker:${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 durationFromText = (value) => cleanText(value, 120).match(/\b\d{1,2}:\d{2}(?::\d{2})?\b/)?.[0] || "";
|
||||||
const messageTimeTextFromWrapper = (wrapper) => {
|
const messageTimeTextFromWrapper = (wrapper) => {
|
||||||
const metaNodes = Array.from(wrapper.querySelectorAll("[class*='meta' i], time"))
|
const metaNodes = Array.from(wrapper.querySelectorAll("[class*='meta' i], time"))
|
||||||
@@ -1513,6 +1626,10 @@ async function extractOpenChatHistory(p, requestedExternalChatId) {
|
|||||||
const suffix = cleanText(duration, 40).replace(/[^0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
const suffix = cleanText(duration, 40).replace(/[^0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
||||||
return `max-voice-${suffix || index + 1}.ogg`;
|
return `max-voice-${suffix || index + 1}.ogg`;
|
||||||
};
|
};
|
||||||
|
const safeStickerFileName = (testId, index) => {
|
||||||
|
const suffix = cleanText(testId, 80).replace(/[^a-z0-9_-]+/gi, "-").replace(/^-+|-+$/g, "");
|
||||||
|
return `max-sticker-${suffix || index + 1}.png`;
|
||||||
|
};
|
||||||
const fileSizeBytesFromLabel = (label) => {
|
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);
|
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;
|
if (!match) return null;
|
||||||
@@ -1719,6 +1836,48 @@ async function extractOpenChatHistory(p, requestedExternalChatId) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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]"))
|
||||||
|
];
|
||||||
|
const seenStickerKeys = new Set();
|
||||||
|
for (const root of stickerRoots) {
|
||||||
|
if (!(root instanceof HTMLElement)) continue;
|
||||||
|
if (contactRoots.some((contactRoot) => contactRoot !== root && contactRoot.contains?.(root))) continue;
|
||||||
|
const semantic = [
|
||||||
|
root.getAttribute("data-testid"),
|
||||||
|
root.getAttribute("aria-label"),
|
||||||
|
root.getAttribute("title"),
|
||||||
|
root.className,
|
||||||
|
root.innerText || root.textContent
|
||||||
|
].filter(Boolean).join(" ");
|
||||||
|
const hasCanvas = root.matches("canvas") || Boolean(root.querySelector("canvas"));
|
||||||
|
if (!hasCanvas && !/sticker|\u0441\u0442\u0438\u043a\u0435\u0440/i.test(semantic)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const wrapper = bubble.closest("[class*='messageWrapper' i]") || bubble;
|
||||||
|
const messageTimeText = messageTimeTextFromWrapper(wrapper);
|
||||||
|
const testId = root.getAttribute("data-testid") ||
|
||||||
|
root.querySelector("[data-testid^='sticker-']")?.getAttribute("data-testid") ||
|
||||||
|
"";
|
||||||
|
const rect = root.getBoundingClientRect();
|
||||||
|
const stickerKey = testId || `${Math.round(rect.x)}:${Math.round(rect.y)}:${Math.round(rect.width)}:${Math.round(rect.height)}`;
|
||||||
|
if (seenStickerKeys.has(stickerKey)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
seenStickerKeys.add(stickerKey);
|
||||||
|
const label = cleanText(root.getAttribute("aria-label") || root.innerText || root.textContent || "\u0421\u0442\u0438\u043a\u0435\u0440", 120);
|
||||||
|
candidates.push({
|
||||||
|
url: domStickerUrlFromCanvas(testId, messageTimeText, candidates.length),
|
||||||
|
label,
|
||||||
|
kind: "Sticker",
|
||||||
|
fileName: safeStickerFileName(testId, candidates.length),
|
||||||
|
contentType: "image/png",
|
||||||
|
fileSizeBytes: null
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const fileButtons = Array.from(bubble.querySelectorAll("button,[role='button']"));
|
const fileButtons = Array.from(bubble.querySelectorAll("button,[role='button']"));
|
||||||
for (const node of fileButtons) {
|
for (const node of fileButtons) {
|
||||||
if (!(node instanceof HTMLElement)) continue;
|
if (!(node instanceof HTMLElement)) continue;
|
||||||
@@ -1744,7 +1903,11 @@ async function extractOpenChatHistory(p, requestedExternalChatId) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const nodes = Array.from(bubble.querySelectorAll("img, image, picture, video, audio, source, a[href], button, [role='button'], [download], [style*='background' i], [class*='sticker' i], [class*='emoji' i], [class*='media' i], [class*='photo' i], [class*='image' i], [class*='file' i], [class*='document' i], [class*='attachment' i], [class*='download' i]"));
|
const mediaSelector = "img, image, picture, video, audio, source, a[href], button, [role='button'], [download], [style*='background' i], [class*='sticker' i], [class*='emoji' i], [class*='media' i], [class*='photo' i], [class*='image' i], [class*='file' i], [class*='document' i], [class*='attachment' i], [class*='download' i]";
|
||||||
|
const nodes = [
|
||||||
|
...(bubble.matches?.(mediaSelector) ? [bubble] : []),
|
||||||
|
...Array.from(bubble.querySelectorAll(mediaSelector))
|
||||||
|
];
|
||||||
for (const node of nodes) {
|
for (const node of nodes) {
|
||||||
if (contactRoots.some((root) => root !== node && root.contains?.(node))) continue;
|
if (contactRoots.some((root) => root !== node && root.contains?.(node))) continue;
|
||||||
const tagName = String(node.tagName || "").toLowerCase();
|
const tagName = String(node.tagName || "").toLowerCase();
|
||||||
@@ -1800,8 +1963,6 @@ async function extractOpenChatHistory(p, requestedExternalChatId) {
|
|||||||
node.textContent,
|
node.textContent,
|
||||||
160);
|
160);
|
||||||
if (/profile|avatar|\u0430\u0432\u0430\u0442\u0430\u0440/i.test(label)) continue;
|
if (/profile|avatar|\u0430\u0432\u0430\u0442\u0430\u0440/i.test(label)) continue;
|
||||||
const mediaTag = tagName === "source" ? String(node.parentElement?.tagName || "").toLowerCase() : tagName;
|
|
||||||
const kind = kindFromMedia(mediaTag, url, label);
|
|
||||||
const semantic = [
|
const semantic = [
|
||||||
tagName,
|
tagName,
|
||||||
node.getAttribute?.("role"),
|
node.getAttribute?.("role"),
|
||||||
@@ -1810,6 +1971,8 @@ async function extractOpenChatHistory(p, requestedExternalChatId) {
|
|||||||
node.className?.baseVal || node.className,
|
node.className?.baseVal || node.className,
|
||||||
label
|
label
|
||||||
].filter(Boolean).join(" ");
|
].filter(Boolean).join(" ");
|
||||||
|
const mediaTag = tagName === "source" ? String(node.parentElement?.tagName || "").toLowerCase() : tagName;
|
||||||
|
const kind = kindFromMedia(mediaTag, url, `${label} ${semantic}`);
|
||||||
const fileLike = tagName === "a" ||
|
const fileLike = tagName === "a" ||
|
||||||
tagName === "button" ||
|
tagName === "button" ||
|
||||||
/file|document|attachment|download|\u0444\u0430\u0439\u043b|\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442|\u0441\u043a\u0430\u0447\u0430\u0442\u044c/i.test(semantic);
|
/file|document|attachment|download|\u0444\u0430\u0439\u043b|\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442|\u0441\u043a\u0430\u0447\u0430\u0442\u044c/i.test(semantic);
|
||||||
@@ -1838,8 +2001,29 @@ async function extractOpenChatHistory(p, requestedExternalChatId) {
|
|||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
|
|
||||||
const wrappers = Array.from(opened.querySelectorAll("[class*='messageWrapper' i]"))
|
const existingWrappers = Array.from(opened.querySelectorAll("[class*='messageWrapper' i]"))
|
||||||
.filter(isVisible);
|
.filter(isVisible);
|
||||||
|
const standaloneMediaRoots = Array.from(opened.querySelectorAll("img, image, picture, video, canvas, svg, [class*='sticker' i], [class*='emoji' i], [class*='lottie' i]"))
|
||||||
|
.filter(isVisible)
|
||||||
|
.map((node) => node.closest?.("[class*='messageWrapper' i], [class*='item' i], [class*='bubbleContent' i], [class*='bordersWrapper' i], [class*='attach' i], [class*='sticker' i], [class*='emoji' i]") || node)
|
||||||
|
.filter((node) => node instanceof HTMLElement)
|
||||||
|
.filter((node) => {
|
||||||
|
if (existingWrappers.some((wrapper) => wrapper === node || wrapper.contains(node) || node.contains(wrapper))) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (node.closest?.("aside, header, [class*='header' i], [class*='composer' i], [class*='toolbar' i]")) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const rect = node.getBoundingClientRect();
|
||||||
|
return rect.width >= 24 &&
|
||||||
|
rect.height >= 24 &&
|
||||||
|
rect.left > openedRect.left + 8 &&
|
||||||
|
rect.top > openedRect.top + 48 &&
|
||||||
|
rect.bottom < window.innerHeight - 24;
|
||||||
|
});
|
||||||
|
const wrappers = [...existingWrappers, ...standaloneMediaRoots]
|
||||||
|
.filter((wrapper, index, all) => all.findIndex((other) => other === wrapper || other.contains?.(wrapper)) === index)
|
||||||
|
.sort((a, b) => a.getBoundingClientRect().top - b.getBoundingClientRect().top);
|
||||||
const messages = [];
|
const messages = [];
|
||||||
const seen = new Set();
|
const seen = new Set();
|
||||||
for (const wrapper of wrappers) {
|
for (const wrapper of wrappers) {
|
||||||
@@ -2058,6 +2242,56 @@ async function clearSidebarSearch(p) {
|
|||||||
await p.waitForTimeout(150);
|
await p.waitForTimeout(150);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function resetSidebarListToTop(p) {
|
||||||
|
await p.evaluate(() => {
|
||||||
|
const aside = document.querySelector("aside");
|
||||||
|
if (!aside) return;
|
||||||
|
|
||||||
|
const candidates = [
|
||||||
|
aside,
|
||||||
|
...Array.from(aside.querySelectorAll("[class*='scroll' i], [class*='contentOverflow' i], [class*='cropped' i], [class*='list' i]"))
|
||||||
|
].filter((element) => element instanceof HTMLElement);
|
||||||
|
|
||||||
|
for (const element of candidates) {
|
||||||
|
if (element.scrollHeight <= element.clientHeight + 2) continue;
|
||||||
|
element.scrollTop = 0;
|
||||||
|
element.dispatchEvent(new Event("scroll", { bubbles: true }));
|
||||||
|
}
|
||||||
|
}).catch(() => {});
|
||||||
|
await p.waitForTimeout(250);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function scrollOpenChatToLatest(p) {
|
||||||
|
for (let attempt = 0; attempt < 2; attempt++) {
|
||||||
|
await p.evaluate(() => {
|
||||||
|
const root = document.querySelector("[class*='openedChat' i]") ||
|
||||||
|
document.querySelector("main") ||
|
||||||
|
document.body;
|
||||||
|
if (!(root instanceof HTMLElement)) return;
|
||||||
|
|
||||||
|
const rootRect = root.getBoundingClientRect();
|
||||||
|
const candidates = [
|
||||||
|
root,
|
||||||
|
...Array.from(root.querySelectorAll("[class*='scroll' i], [class*='contentOverflow' i], [class*='cropped' i], [class*='messages' i], [class*='messageList' i], [class*='list' i]"))
|
||||||
|
].filter((element) => {
|
||||||
|
if (!(element instanceof HTMLElement)) return false;
|
||||||
|
const rect = element.getBoundingClientRect();
|
||||||
|
return rect.width > 120 &&
|
||||||
|
rect.height > 120 &&
|
||||||
|
rect.right >= rootRect.left &&
|
||||||
|
rect.left <= rootRect.right &&
|
||||||
|
element.scrollHeight > element.clientHeight + 2;
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const element of candidates) {
|
||||||
|
element.scrollTop = element.scrollHeight;
|
||||||
|
element.dispatchEvent(new Event("scroll", { bubbles: true }));
|
||||||
|
}
|
||||||
|
}).catch(() => {});
|
||||||
|
await p.waitForTimeout(250);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function openDomChatByExternalId(p, externalChatId) {
|
async function openDomChatByExternalId(p, externalChatId) {
|
||||||
const title = decodeDomChatTitle(externalChatId) || externalChatId;
|
const title = decodeDomChatTitle(externalChatId) || externalChatId;
|
||||||
if (!title) {
|
if (!title) {
|
||||||
@@ -2065,8 +2299,9 @@ async function openDomChatByExternalId(p, externalChatId) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
await clearSidebarSearch(p);
|
await clearSidebarSearch(p);
|
||||||
|
await resetSidebarListToTop(p);
|
||||||
|
|
||||||
return await p.evaluate((requestedTitle) => {
|
const point = await p.evaluate((requestedTitle) => {
|
||||||
const cleanText = (value) => String(value || "")
|
const cleanText = (value) => String(value || "")
|
||||||
.replace(/\u00a0/g, " ")
|
.replace(/\u00a0/g, " ")
|
||||||
.replace(/\s+/g, " ")
|
.replace(/\s+/g, " ")
|
||||||
@@ -2085,9 +2320,14 @@ async function openDomChatByExternalId(p, externalChatId) {
|
|||||||
header?.querySelector("h1,h2,h3,[class*='title' i],[class*='name' i]")?.innerText ||
|
header?.querySelector("h1,h2,h3,[class*='title' i],[class*='name' i]")?.innerText ||
|
||||||
header?.querySelector("button[aria-label]")?.innerText);
|
header?.querySelector("button[aria-label]")?.innerText);
|
||||||
const selectedTitle = normalize(document.querySelector("aside button[class*='selected' i] h3")?.innerText);
|
const selectedTitle = normalize(document.querySelector("aside button[class*='selected' i] h3")?.innerText);
|
||||||
|
const hasVisibleMessages = Array.from(document.querySelectorAll("[class*='messageWrapper' i]"))
|
||||||
|
.some((node) => {
|
||||||
|
const rect = node.getBoundingClientRect?.();
|
||||||
|
return rect && rect.width > 40 && rect.height > 20 && rect.right > window.innerWidth * 0.35;
|
||||||
|
});
|
||||||
const currentTitles = [selectedTitle, headerTitle].filter(Boolean);
|
const currentTitles = [selectedTitle, headerTitle].filter(Boolean);
|
||||||
if (currentTitles.some((current) => current === wanted || current.includes(wanted) || wanted.includes(current))) {
|
if (hasVisibleMessages && currentTitles.some((current) => current === wanted || current.includes(wanted) || wanted.includes(current))) {
|
||||||
return true;
|
return { opened: true };
|
||||||
}
|
}
|
||||||
|
|
||||||
const buttons = Array.from(document.querySelectorAll("aside button.cell, aside button[class*='cell' i]"));
|
const buttons = Array.from(document.querySelectorAll("aside button.cell, aside button[class*='cell' i]"));
|
||||||
@@ -2102,13 +2342,28 @@ async function openDomChatByExternalId(p, externalChatId) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (!match) {
|
if (!match) {
|
||||||
return false;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
match.scrollIntoView({ block: "center", inline: "nearest" });
|
match.scrollIntoView({ block: "center", inline: "nearest" });
|
||||||
match.click();
|
const rect = match.getBoundingClientRect();
|
||||||
return true;
|
return {
|
||||||
|
opened: false,
|
||||||
|
x: Math.round(rect.left + Math.min(rect.width - 8, Math.max(8, rect.width * 0.35))),
|
||||||
|
y: Math.round(rect.top + rect.height / 2)
|
||||||
|
};
|
||||||
}, title).catch(() => false);
|
}, title).catch(() => false);
|
||||||
|
|
||||||
|
if (!point) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (point.opened) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
await p.mouse.click(point.x, point.y);
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function ensureDomChatOpen(p, externalChatId, waitMs = 600) {
|
async function ensureDomChatOpen(p, externalChatId, waitMs = 600) {
|
||||||
|
|||||||
Reference in New Issue
Block a user