Require opened MAX chat before sending
This commit is contained in:
+83
-13
@@ -324,7 +324,7 @@ app.post("/send/text", async (req, res) => {
|
||||
const p = await ensurePage();
|
||||
|
||||
if (externalChatId) {
|
||||
const opened = await ensureDomChatOpen(p, externalChatId, 600);
|
||||
const opened = await ensureDomChatOpen(p, externalChatId, 8000, true);
|
||||
if (!opened) {
|
||||
return { success: false, externalMessageId: null, error: "MAX chat was not found or did not open." };
|
||||
}
|
||||
@@ -2792,44 +2792,114 @@ async function openDomChatByExternalId(p, externalChatId) {
|
||||
return true;
|
||||
}
|
||||
|
||||
async function ensureDomChatOpen(p, externalChatId, waitMs = 600) {
|
||||
async function ensureDomChatOpen(p, externalChatId, waitMs = 600, requireComposer = false) {
|
||||
const descriptor = decodeDomChatDescriptor(externalChatId);
|
||||
const title = descriptor?.title || externalChatId;
|
||||
if (!title) {
|
||||
return true;
|
||||
}
|
||||
|
||||
for (let attempt = 0; attempt < 2; attempt++) {
|
||||
const opened = await openDomChatByExternalId(p, externalChatId);
|
||||
if (!opened) {
|
||||
return false;
|
||||
}
|
||||
|
||||
await p.waitForTimeout(waitMs);
|
||||
const verified = await p.evaluate((requestedTitle) => {
|
||||
const ready = await waitForDomChatReady(
|
||||
p,
|
||||
title,
|
||||
Math.max(waitMs, requireComposer ? 8000 : 2500),
|
||||
requireComposer);
|
||||
if (ready) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
async function waitForDomChatReady(p, title, timeoutMs = 5000, requireComposer = false) {
|
||||
const deadline = Date.now() + timeoutMs;
|
||||
while (Date.now() < deadline) {
|
||||
const state = await p.evaluate(({ requestedTitle, needsComposer }) => {
|
||||
const cleanText = (value) => String(value || "")
|
||||
.replace(/\u00a0/g, " ")
|
||||
.replace(/\s+/g, " ")
|
||||
.trim();
|
||||
const normalize = (value) => cleanText(value).toLowerCase();
|
||||
const wanted = normalize(requestedTitle);
|
||||
const isVisible = (element) => {
|
||||
if (!(element instanceof HTMLElement)) return false;
|
||||
const style = window.getComputedStyle(element);
|
||||
const rect = element.getBoundingClientRect();
|
||||
return style.visibility !== "hidden" &&
|
||||
style.display !== "none" &&
|
||||
rect.width > 0 &&
|
||||
rect.height > 0 &&
|
||||
rect.bottom >= 0 &&
|
||||
rect.right >= 0 &&
|
||||
rect.top <= window.innerHeight &&
|
||||
rect.left <= window.innerWidth;
|
||||
};
|
||||
const normalizeClassName = (value) => typeof value === "string" ? value : String(value?.baseVal || "");
|
||||
const opened = document.querySelector("[class*='openedChat' i]") ||
|
||||
document.querySelector("main") ||
|
||||
document.body;
|
||||
const selectedTitle = cleanText(document.querySelector("aside button[class*='selected' i] h3")?.innerText, 160);
|
||||
const header = opened.querySelector("[class*='header' i]");
|
||||
const rightPaneLeft = window.innerWidth * 0.32;
|
||||
const headerCandidates = Array.from(opened.querySelectorAll("[class*='header' i], header"))
|
||||
.filter((node) => isVisible(node))
|
||||
.filter((node) => node.getBoundingClientRect().left > rightPaneLeft);
|
||||
const header = headerCandidates[0] || null;
|
||||
const headerTitle = cleanText(
|
||||
header?.querySelector("h1,h2,h3,[class*='title' i],[class*='name' i]")?.innerText ||
|
||||
header?.querySelector("button[aria-label]")?.innerText,
|
||||
header?.querySelector("button[aria-label]")?.innerText ||
|
||||
header?.innerText,
|
||||
160);
|
||||
const current = normalize(selectedTitle || headerTitle);
|
||||
if (!current) {
|
||||
return null;
|
||||
const selectedTitle = cleanText(document.querySelector("aside button[class*='selected' i] h3")?.innerText, 160);
|
||||
const composerSelectors = [
|
||||
"[data-testid='composer'] [role='textbox']",
|
||||
"[data-testid='composer'] [contenteditable='true']",
|
||||
"[class*='composer' i] [role='textbox']",
|
||||
"[class*='composer' i] [contenteditable='true']",
|
||||
"[class*='composer' i] [class*='contenteditable' i]",
|
||||
"[role='textbox'][class*='contenteditable' i]",
|
||||
"textarea"
|
||||
];
|
||||
const hasComposer = composerSelectors
|
||||
.flatMap((selector) => Array.from(document.querySelectorAll(selector)))
|
||||
.some((node) => {
|
||||
if (!isVisible(node)) return false;
|
||||
const rect = node.getBoundingClientRect();
|
||||
const text = normalize([
|
||||
node.getAttribute("aria-label"),
|
||||
node.getAttribute("placeholder"),
|
||||
node.getAttribute("title"),
|
||||
node.getAttribute("data-testid"),
|
||||
normalizeClassName(node.className),
|
||||
node.id,
|
||||
node.getAttribute("name")
|
||||
].join(" "));
|
||||
const searchLike = /search|\u043f\u043e\u0438\u0441\u043a/.test(text) ||
|
||||
(rect.left < window.innerWidth * 0.42 && rect.top < window.innerHeight * 0.32);
|
||||
return !searchLike &&
|
||||
rect.left > rightPaneLeft &&
|
||||
rect.top > window.innerHeight * 0.45;
|
||||
});
|
||||
const current = normalize(headerTitle || (hasComposer ? selectedTitle : ""));
|
||||
const titleMatches = !wanted ||
|
||||
(current && (current === wanted || current.includes(wanted) || wanted.includes(current)));
|
||||
const ready = Boolean(titleMatches && header && (!needsComposer || hasComposer));
|
||||
return { ready, headerTitle, selectedTitle, hasHeader: Boolean(header), hasComposer };
|
||||
}, { requestedTitle: title, needsComposer: requireComposer }).catch(() => null);
|
||||
|
||||
if (state?.ready) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return current === wanted || current.includes(wanted) || wanted.includes(current);
|
||||
}, title).catch(() => null);
|
||||
await p.waitForTimeout(250);
|
||||
}
|
||||
|
||||
return verified === null ? opened : verified;
|
||||
return false;
|
||||
}
|
||||
|
||||
async function sendTextAndConfirm(p, externalChatId, text) {
|
||||
|
||||
Reference in New Issue
Block a user