Harden MAX text composer sending
This commit is contained in:
+202
-23
@@ -289,6 +289,65 @@ app.post("/chat/history", async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
app.post("/chat/clear-composer", async (req, res) => {
|
||||
try {
|
||||
const externalChatId = String(req.body?.externalChatId || "").trim();
|
||||
const result = await withPageLock(async () => {
|
||||
const p = await ensurePage();
|
||||
const currentStatus = await status();
|
||||
if (!currentStatus.isAuthorized) {
|
||||
return { success: false, error: "MAX is not authorized.", composerText: null };
|
||||
}
|
||||
|
||||
if (externalChatId) {
|
||||
const opened = await ensureDomChatOpen(p, externalChatId, 2000, true);
|
||||
if (!opened) {
|
||||
return { success: false, error: "MAX chat was not found or did not open.", composerText: null };
|
||||
}
|
||||
}
|
||||
|
||||
const success = await clearComposerInput(p);
|
||||
return { success, error: success ? null : "MAX composer was not cleared.", composerText: await readComposerText(p) };
|
||||
});
|
||||
|
||||
res.json(result);
|
||||
} catch (error) {
|
||||
res.status(500).json(errorStatus(error));
|
||||
}
|
||||
});
|
||||
|
||||
app.post("/chat/draft-text", async (req, res) => {
|
||||
try {
|
||||
const externalChatId = String(req.body?.externalChatId || "").trim();
|
||||
const text = String(req.body?.text || "");
|
||||
if (!text) {
|
||||
return res.json({ success: false, error: "Text is empty.", composerText: null });
|
||||
}
|
||||
|
||||
const result = await withPageLock(async () => {
|
||||
const p = await ensurePage();
|
||||
const currentStatus = await status();
|
||||
if (!currentStatus.isAuthorized) {
|
||||
return { success: false, error: "MAX is not authorized.", composerText: null };
|
||||
}
|
||||
|
||||
if (externalChatId) {
|
||||
const opened = await ensureDomChatOpen(p, externalChatId, 2000, true);
|
||||
if (!opened) {
|
||||
return { success: false, error: "MAX chat was not found or did not open.", composerText: null };
|
||||
}
|
||||
}
|
||||
|
||||
const success = await fillComposerText(p, text, true);
|
||||
return { success, error: success ? null : "MAX composer was not filled.", composerText: await readComposerText(p) };
|
||||
});
|
||||
|
||||
res.json(result);
|
||||
} catch (error) {
|
||||
res.status(500).json(errorStatus(error));
|
||||
}
|
||||
});
|
||||
|
||||
app.post("/chat/presence", async (req, res) => {
|
||||
try {
|
||||
const externalChatId = String(req.body?.externalChatId || "").trim();
|
||||
@@ -2915,26 +2974,30 @@ async function sendTextAndConfirm(p, externalChatId, text) {
|
||||
.length;
|
||||
const attempts = [
|
||||
{ name: "send button", action: () => clickSendButton(p) },
|
||||
{ name: "composer fallback button", action: () => clickComposerRightmostButton(p) },
|
||||
{ name: "Enter", action: async () => { await p.keyboard.press("Enter"); return true; } },
|
||||
{ name: "Control+Enter", action: async () => { await p.keyboard.press("Control+Enter"); return true; } },
|
||||
{ name: "composer fallback button", action: () => clickComposerRightmostButton(p) }
|
||||
{ name: "Control+Enter", action: async () => { await p.keyboard.press("Control+Enter"); return true; } }
|
||||
];
|
||||
|
||||
for (const attempt of attempts) {
|
||||
if (!(await fillComposerText(p, text, true))) {
|
||||
console.warn(`[send/text] composer fill failed; chat=${externalChatId || "current"} textLength=${String(text || "").length}`);
|
||||
return null;
|
||||
}
|
||||
|
||||
const composerText = await readComposerText(p);
|
||||
if (composerText !== null && !messageTextMatches(composerText, wanted)) {
|
||||
console.warn(`[send/text] composer mismatch after fill; attempt=${attempt.name} composerLength=${composerText.length} wantedLength=${wanted.length}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
const triggered = await attempt.action().catch(() => false);
|
||||
if (!triggered) {
|
||||
console.warn(`[send/text] send trigger was not found; attempt=${attempt.name}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
await scrollOpenChatToBottom(p);
|
||||
const externalId = await findSentOutgoingExternalId(
|
||||
p,
|
||||
externalChatId,
|
||||
@@ -2943,11 +3006,13 @@ async function sendTextAndConfirm(p, externalChatId, text) {
|
||||
initialOutgoingIds,
|
||||
10000);
|
||||
if (externalId) {
|
||||
console.info(`[send/text] confirmed outgoing message; attempt=${attempt.name} externalId=${externalId}`);
|
||||
return externalId;
|
||||
}
|
||||
|
||||
const currentComposerText = await readComposerText(p);
|
||||
if (currentComposerText !== null && currentComposerText.length === 0) {
|
||||
console.warn(`[send/text] MAX cleared composer but no outgoing message appeared; attempt=${attempt.name}`);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -2956,7 +3021,25 @@ async function sendTextAndConfirm(p, externalChatId, text) {
|
||||
}
|
||||
|
||||
async function fillComposerText(p, text, clearFirst) {
|
||||
if (clearFirst && prefersClipboardComposerInput(text) && await fillComposerViaClipboard(p, text, true)) {
|
||||
if (clearFirst && !(await clearComposerInput(p))) {
|
||||
console.warn("[send/text] composer did not report empty after clear; continuing with input fallback");
|
||||
}
|
||||
|
||||
if (await fillComposerViaClipboard(p, text, false)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (clearFirst) {
|
||||
await clearComposerInput(p);
|
||||
}
|
||||
if (await fillComposerViaKeyboard(p, text, false)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (clearFirst) {
|
||||
await clearComposerInput(p);
|
||||
}
|
||||
if (await insertComposerTextViaDom(p, text, false)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -2964,10 +3047,6 @@ async function fillComposerText(p, text, clearFirst) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (clearFirst && await fillComposerViaClipboard(p, text, true)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
for (let attempt = 0; attempt < 2; attempt++) {
|
||||
if (!(await focusComposer(p))) {
|
||||
break;
|
||||
@@ -3020,8 +3099,25 @@ async function fillComposerText(p, text, clearFirst) {
|
||||
return false;
|
||||
}
|
||||
|
||||
function prefersClipboardComposerInput(text) {
|
||||
return /[\p{Extended_Pictographic}\u200d\ufe0f]/u.test(String(text || ""));
|
||||
async function clearComposerInput(p) {
|
||||
if (!(await focusComposer(p))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (let attempt = 0; attempt < 2; attempt++) {
|
||||
await p.keyboard.press(process.platform === "darwin" ? "Meta+A" : "Control+A");
|
||||
await p.keyboard.press("Backspace");
|
||||
await p.waitForTimeout(120);
|
||||
const currentText = await readComposerText(p);
|
||||
if (currentText !== null && currentText.length === 0) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
await insertComposerTextViaDom(p, "", true);
|
||||
await p.waitForTimeout(150);
|
||||
const currentText = await readComposerText(p);
|
||||
return currentText !== null && currentText.length === 0;
|
||||
}
|
||||
|
||||
async function fillComposerViaClipboard(p, text, clearFirst) {
|
||||
@@ -3045,10 +3141,28 @@ async function fillComposerViaClipboard(p, text, clearFirst) {
|
||||
|
||||
if (wroteClipboard) {
|
||||
await p.keyboard.press(process.platform === "darwin" ? "Meta+V" : "Control+V");
|
||||
} else if (!(await insertComposerTextViaDom(p, text, false))) {
|
||||
await p.waitForTimeout(300);
|
||||
const pastedText = await readComposerText(p);
|
||||
if (pastedText !== null && messageTextMatches(pastedText, text)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
async function fillComposerViaKeyboard(p, text, clearFirst) {
|
||||
if (!(await focusComposer(p))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (clearFirst) {
|
||||
await p.keyboard.press(process.platform === "darwin" ? "Meta+A" : "Control+A");
|
||||
await p.keyboard.press("Backspace");
|
||||
await p.waitForTimeout(80);
|
||||
}
|
||||
|
||||
await p.keyboard.insertText(text);
|
||||
await p.waitForTimeout(300);
|
||||
const currentText = await readComposerText(p);
|
||||
return currentText !== null && messageTextMatches(currentText, text);
|
||||
@@ -3480,17 +3594,34 @@ async function clickSendButton(p) {
|
||||
};
|
||||
const buttons = Array.from(document.querySelectorAll("button, [role='button']"))
|
||||
.filter((button) => !button.disabled && isVisible(button));
|
||||
const match = buttons.find((button) => {
|
||||
const text = [
|
||||
button.getAttribute("aria-label"),
|
||||
button.getAttribute("title"),
|
||||
button.getAttribute("data-testid"),
|
||||
button.className,
|
||||
button.innerText,
|
||||
button.textContent
|
||||
].join(" ").trim().toLowerCase();
|
||||
return /send|submit|\u043e\u0442\u043f\u0440\u0430\u0432/.test(text);
|
||||
});
|
||||
const candidates = buttons
|
||||
.map((button) => {
|
||||
const rect = button.getBoundingClientRect();
|
||||
const text = [
|
||||
button.getAttribute("aria-label"),
|
||||
button.getAttribute("title"),
|
||||
button.getAttribute("data-testid"),
|
||||
button.className,
|
||||
button.innerText,
|
||||
button.textContent
|
||||
].join(" ").trim().toLowerCase();
|
||||
const exact = /send message|\u043e\u0442\u043f\u0440\u0430\u0432\u0438\u0442\u044c \u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u0435/.test(text);
|
||||
const generic = /send|submit|\u043e\u0442\u043f\u0440\u0430\u0432/.test(text);
|
||||
return {
|
||||
button,
|
||||
rect,
|
||||
exact,
|
||||
generic,
|
||||
bottomRight: rect.left > window.innerWidth * 0.32 && rect.top > window.innerHeight * 0.55
|
||||
};
|
||||
})
|
||||
.filter((candidate) => candidate.generic);
|
||||
const match = candidates
|
||||
.sort((a, b) =>
|
||||
Number(b.exact) - Number(a.exact) ||
|
||||
Number(b.bottomRight) - Number(a.bottomRight) ||
|
||||
b.rect.left - a.rect.left ||
|
||||
b.rect.top - a.rect.top)[0]?.button || null;
|
||||
|
||||
if (!match) {
|
||||
return null;
|
||||
@@ -3525,8 +3656,24 @@ async function clickComposerRightmostButton(p) {
|
||||
rect.top <= window.innerHeight &&
|
||||
rect.left <= window.innerWidth;
|
||||
};
|
||||
const buttons = Array.from(document.querySelectorAll("[class*='composer' i] button, [class*='composer' i] [role='button']"))
|
||||
.filter((button) => !button.disabled && isVisible(button))
|
||||
const isComposerButton = (button) => {
|
||||
const rect = button.getBoundingClientRect();
|
||||
if (rect.left < window.innerWidth * 0.32 || rect.top < window.innerHeight * 0.55) {
|
||||
return false;
|
||||
}
|
||||
const text = [
|
||||
button.getAttribute("aria-label"),
|
||||
button.getAttribute("title"),
|
||||
button.getAttribute("data-testid"),
|
||||
button.className,
|
||||
button.innerText,
|
||||
button.textContent
|
||||
].join(" ").trim().toLowerCase();
|
||||
return !/sticker|emoji|attach|upload|\u0441\u0442\u0438\u043a\u0435\u0440|\u044d\u043c\u043e\u0434\u0437\u0438|\u0444\u0430\u0439\u043b|\u0437\u0430\u0433\u0440\u0443\u0437/.test(text);
|
||||
};
|
||||
|
||||
const buttons = Array.from(document.querySelectorAll("button, [role='button']"))
|
||||
.filter((button) => !button.disabled && isVisible(button) && isComposerButton(button))
|
||||
.sort((a, b) => b.getBoundingClientRect().left - a.getBoundingClientRect().left);
|
||||
const button = buttons[0];
|
||||
if (!button) {
|
||||
@@ -3548,6 +3695,38 @@ async function clickComposerRightmostButton(p) {
|
||||
return true;
|
||||
}
|
||||
|
||||
async function scrollOpenChatToBottom(p) {
|
||||
await p.evaluate(() => {
|
||||
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 root = Array.from(document.querySelectorAll("main *, [role='main'] *, [class*='scroll' i]"))
|
||||
.filter((element) => element instanceof HTMLElement && isVisible(element))
|
||||
.filter((element) => {
|
||||
const rect = element.getBoundingClientRect();
|
||||
return rect.left > window.innerWidth * 0.32 &&
|
||||
rect.top < window.innerHeight * 0.82 &&
|
||||
element.scrollHeight > element.clientHeight + 40;
|
||||
})
|
||||
.sort((a, b) => (b.scrollHeight - b.clientHeight) - (a.scrollHeight - a.clientHeight))[0];
|
||||
if (root instanceof HTMLElement) {
|
||||
root.scrollTop = root.scrollHeight;
|
||||
root.dispatchEvent(new Event("scroll", { bubbles: true }));
|
||||
}
|
||||
}).catch(() => {});
|
||||
await p.waitForTimeout(120);
|
||||
}
|
||||
|
||||
async function editMessageInMax(p, externalChatId, externalMessageId, currentText, text) {
|
||||
const target = await locateMessageForAction(p, externalChatId, externalMessageId, currentText);
|
||||
if (!target) {
|
||||
|
||||
Reference in New Issue
Block a user