Handle MAX canvas stickers in sync
This commit is contained in:
@@ -267,12 +267,16 @@ public sealed class MaxBridgeSyncService(
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
var previewAt = ResolveListPreviewAt(update);
|
||||
chat.LastMessagePreview = preview;
|
||||
chat.LastMessageAt = previewAt;
|
||||
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 domDownloadPrefix = "qmax-dom-download:";
|
||||
const domAudioPrefix = "qmax-dom-audio:";
|
||||
const domStickerPrefix = "qmax-dom-sticker:";
|
||||
|
||||
let context;
|
||||
let page;
|
||||
@@ -247,6 +248,7 @@ app.get("/updates", async (_req, res) => {
|
||||
}
|
||||
|
||||
await clearSidebarSearch(p);
|
||||
await resetSidebarListToTop(p);
|
||||
return await extractUpdates(p);
|
||||
});
|
||||
res.json(result);
|
||||
@@ -272,6 +274,7 @@ app.post("/chat/history", async (req, res) => {
|
||||
}
|
||||
}
|
||||
|
||||
await scrollOpenChatToLatest(p);
|
||||
return await extractOpenChatHistory(p, externalChatId);
|
||||
});
|
||||
|
||||
@@ -525,7 +528,7 @@ function errorStatus(error) {
|
||||
function isAllowedMediaUrl(value) {
|
||||
const raw = String(value || "").trim();
|
||||
if (!raw) return false;
|
||||
if (isDomDownloadUrl(raw) || isDomAudioUrl(raw)) return true;
|
||||
if (isDomDownloadUrl(raw) || isDomAudioUrl(raw) || isDomStickerUrl(raw)) return true;
|
||||
if (raw.startsWith("blob:")) {
|
||||
try {
|
||||
const inner = new URL(raw.slice("blob:".length));
|
||||
@@ -551,6 +554,10 @@ function isDomAudioUrl(value) {
|
||||
return String(value || "").startsWith(domAudioPrefix);
|
||||
}
|
||||
|
||||
function isDomStickerUrl(value) {
|
||||
return String(value || "").startsWith(domStickerPrefix);
|
||||
}
|
||||
|
||||
function parseDomDownloadUrl(value) {
|
||||
const raw = String(value || "");
|
||||
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) {
|
||||
const host = String(hostname || "").toLowerCase();
|
||||
if (!host || host === "localhost" || host.endsWith(".localhost")) return true;
|
||||
@@ -597,6 +617,10 @@ async function fetchMediaThroughMaxSession(p, mediaUrl) {
|
||||
return await downloadDomAudioThroughPlay(p, parseDomAudioUrl(mediaUrl));
|
||||
}
|
||||
|
||||
if (isDomStickerUrl(mediaUrl)) {
|
||||
return await downloadDomStickerThroughScreenshot(p, parseDomStickerUrl(mediaUrl));
|
||||
}
|
||||
|
||||
if (mediaUrl.startsWith("blob:")) {
|
||||
const result = await p.evaluate(async (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}.`);
|
||||
}
|
||||
|
||||
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) {
|
||||
const ext = String(fileName || "").toLowerCase().split(".").pop();
|
||||
switch (ext) {
|
||||
@@ -931,18 +991,46 @@ async function inspectDom(p) {
|
||||
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)
|
||||
.map((element) => {
|
||||
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 {
|
||||
tag: element.tagName.toLowerCase(),
|
||||
tag,
|
||||
role: element.getAttribute("role"),
|
||||
id: element.id || null,
|
||||
className: cleanText(element.className, 180),
|
||||
testId: element.getAttribute("data-testid"),
|
||||
ariaLabel: element.getAttribute("aria-label"),
|
||||
href: element.getAttribute("href"),
|
||||
mediaSrc,
|
||||
text: cleanText(element.innerText || element.value || element.getAttribute("aria-label")),
|
||||
rect: {
|
||||
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);
|
||||
|
||||
return {
|
||||
@@ -1186,6 +1282,18 @@ async function extractUpdates(p) {
|
||||
rect.top <= window.innerHeight &&
|
||||
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 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) => {
|
||||
@@ -1267,7 +1375,7 @@ async function extractUpdates(p) {
|
||||
const seen = new Set();
|
||||
const chats = [];
|
||||
for (const element of Array.from(document.querySelectorAll(chatSelector))) {
|
||||
if (!isVisible(element)) continue;
|
||||
if (!isChatRowVisibleEnough(element)) continue;
|
||||
const rect = element.getBoundingClientRect();
|
||||
const text = cleanText(element.innerText || element.textContent, 400);
|
||||
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),
|
||||
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 messageTimeTextFromWrapper = (wrapper) => {
|
||||
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, "");
|
||||
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 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;
|
||||
@@ -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']"));
|
||||
for (const node of fileButtons) {
|
||||
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) {
|
||||
if (contactRoots.some((root) => root !== node && root.contains?.(node))) continue;
|
||||
const tagName = String(node.tagName || "").toLowerCase();
|
||||
@@ -1800,8 +1963,6 @@ async function extractOpenChatHistory(p, requestedExternalChatId) {
|
||||
node.textContent,
|
||||
160);
|
||||
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 = [
|
||||
tagName,
|
||||
node.getAttribute?.("role"),
|
||||
@@ -1810,6 +1971,8 @@ async function extractOpenChatHistory(p, requestedExternalChatId) {
|
||||
node.className?.baseVal || node.className,
|
||||
label
|
||||
].filter(Boolean).join(" ");
|
||||
const mediaTag = tagName === "source" ? String(node.parentElement?.tagName || "").toLowerCase() : tagName;
|
||||
const kind = kindFromMedia(mediaTag, url, `${label} ${semantic}`);
|
||||
const fileLike = tagName === "a" ||
|
||||
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);
|
||||
@@ -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);
|
||||
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 seen = new Set();
|
||||
for (const wrapper of wrappers) {
|
||||
@@ -2058,6 +2242,56 @@ async function clearSidebarSearch(p) {
|
||||
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) {
|
||||
const title = decodeDomChatTitle(externalChatId) || externalChatId;
|
||||
if (!title) {
|
||||
@@ -2065,8 +2299,9 @@ async function openDomChatByExternalId(p, externalChatId) {
|
||||
}
|
||||
|
||||
await clearSidebarSearch(p);
|
||||
await resetSidebarListToTop(p);
|
||||
|
||||
return await p.evaluate((requestedTitle) => {
|
||||
const point = await p.evaluate((requestedTitle) => {
|
||||
const cleanText = (value) => String(value || "")
|
||||
.replace(/\u00a0/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("button[aria-label]")?.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);
|
||||
if (currentTitles.some((current) => current === wanted || current.includes(wanted) || wanted.includes(current))) {
|
||||
return true;
|
||||
if (hasVisibleMessages && currentTitles.some((current) => current === wanted || current.includes(wanted) || wanted.includes(current))) {
|
||||
return { opened: true };
|
||||
}
|
||||
|
||||
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) {
|
||||
return false;
|
||||
return null;
|
||||
}
|
||||
|
||||
match.scrollIntoView({ block: "center", inline: "nearest" });
|
||||
match.click();
|
||||
return true;
|
||||
const rect = match.getBoundingClientRect();
|
||||
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);
|
||||
|
||||
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) {
|
||||
|
||||
Reference in New Issue
Block a user