diff --git a/worker/src/server.js b/worker/src/server.js index f374f17..bd54789 100644 --- a/worker/src/server.js +++ b/worker/src/server.js @@ -2985,9 +2985,9 @@ async function sendTextAndConfirm(p, externalChatId, text) { 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}`); + const composerState = await readComposerState(p); + if (!composerStateMatchesText(composerState, text)) { + console.warn(`[send/text] composer mismatch after fill; attempt=${attempt.name} composerLength=${composerState?.text?.length ?? -1} richCount=${composerState?.richContentCount ?? -1} wantedLength=${wanted.length}`); continue; } @@ -3010,8 +3010,8 @@ async function sendTextAndConfirm(p, externalChatId, text) { return externalId; } - const currentComposerText = await readComposerText(p); - if (currentComposerText !== null && currentComposerText.length === 0) { + const currentComposerState = await readComposerState(p); + if (composerStateIsEmpty(currentComposerState)) { console.warn(`[send/text] MAX cleared composer but no outgoing message appeared; attempt=${attempt.name}`); return null; } @@ -3040,7 +3040,10 @@ async function fillComposerText(p, text, clearFirst) { await clearComposerInput(p); } if (await insertComposerTextViaDom(p, text, false)) { - return true; + await p.waitForTimeout(300); + if (composerStateMatchesText(await readComposerState(p), text)) { + return true; + } } if (clearFirst && await fillComposerViaLocator(p, text)) { @@ -3059,8 +3062,8 @@ async function fillComposerText(p, text, clearFirst) { await p.keyboard.insertText(text); await p.waitForTimeout(300); - const currentText = await readComposerText(p); - if (currentText !== null && messageTextMatches(currentText, text)) { + const state = await readComposerState(p); + if (composerStateMatchesText(state, text)) { return true; } } @@ -3087,8 +3090,8 @@ async function fillComposerText(p, text, clearFirst) { } await p.keyboard.insertText(text); await p.waitForTimeout(250); - const currentText = await readComposerText(p); - if (currentText !== null && messageTextMatches(currentText, text)) { + const state = await readComposerState(p); + if (composerStateMatchesText(state, text)) { return true; } } catch { @@ -3108,16 +3111,14 @@ async function clearComposerInput(p) { 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) { + if (composerStateIsEmpty(await readComposerState(p))) { return true; } } await insertComposerTextViaDom(p, "", true); await p.waitForTimeout(150); - const currentText = await readComposerText(p); - return currentText !== null && currentText.length === 0; + return composerStateIsEmpty(await readComposerState(p)); } async function fillComposerViaClipboard(p, text, clearFirst) { @@ -3142,8 +3143,7 @@ async function fillComposerViaClipboard(p, text, clearFirst) { if (wroteClipboard) { await p.keyboard.press(process.platform === "darwin" ? "Meta+V" : "Control+V"); await p.waitForTimeout(300); - const pastedText = await readComposerText(p); - if (pastedText !== null && messageTextMatches(pastedText, text)) { + if (composerStateMatchesText(await readComposerState(p), text)) { return true; } } @@ -3164,8 +3164,7 @@ async function fillComposerViaKeyboard(p, text, clearFirst) { await p.keyboard.insertText(text); await p.waitForTimeout(300); - const currentText = await readComposerText(p); - return currentText !== null && messageTextMatches(currentText, text); + return composerStateMatchesText(await readComposerState(p), text); } async function insertComposerTextViaDom(p, text, clearFirst = false) { @@ -3236,11 +3235,16 @@ async function insertComposerTextViaDom(p, text, clearFirst = false) { if ("value" in composer) { composer.value = ""; } else { - composer.textContent = ""; + composer.replaceChildren(); } composer.dispatchEvent(new InputEvent("input", { bubbles: true, inputType: "deleteContentBackward", data: null })); } + if (!value) { + composer.dispatchEvent(new Event("change", { bubbles: true })); + return true; + } + const beforeInput = new InputEvent("beforeinput", { bubbles: true, cancelable: true, @@ -3281,8 +3285,7 @@ async function fillComposerViaLocator(p, text) { await composer.fill("", { timeout: 3000 }); await composer.fill(text, { timeout: 3000 }); await p.waitForTimeout(200); - const currentText = await readComposerText(p); - if (currentText !== null && messageTextMatches(currentText, text)) { + if (composerStateMatchesText(await readComposerState(p), text)) { return true; } } catch { @@ -3385,8 +3388,7 @@ async function focusComposer(p) { async function waitForComposerToClear(p, timeoutMs = 4000) { const deadline = Date.now() + timeoutMs; while (Date.now() < deadline) { - const text = await readComposerText(p); - if (text !== null && text.length === 0) { + if (composerStateIsEmpty(await readComposerState(p))) { return true; } @@ -3397,6 +3399,11 @@ async function waitForComposerToClear(p, timeoutMs = 4000) { } async function readComposerText(p) { + const state = await readComposerState(p); + return state?.text ?? null; +} + +async function readComposerState(p) { return await p.evaluate(() => { const cleanText = (value) => String(value || "") .replace(/\u00a0/g, " ") @@ -3404,6 +3411,7 @@ async function readComposerText(p) { .replace(/\s+/g, " ") .trim(); const isVisible = (element) => { + if (!(element instanceof Element)) return false; const style = window.getComputedStyle(element); const rect = element.getBoundingClientRect(); return style.visibility !== "hidden" && @@ -3458,10 +3466,73 @@ async function readComposerText(p) { } const value = "value" in composer ? composer.value : ""; - return cleanText(value || composer.innerText || composer.textContent); + const text = cleanText(value || composer.innerText || composer.textContent); + const richContentCount = Array.from(composer.querySelectorAll([ + "img", + "canvas", + "svg", + "[class*='emoji' i]", + "[class*='sticker' i]", + "[class*='lottie' i]", + "[class*='emoticon' i]", + "[data-testid*='emoji' i]", + "[data-testid*='sticker' i]", + "[data-testid*='lottie' i]" + ].join(","))) + .filter((element) => isVisible(element)) + .filter((element) => !/placeholder/i.test(String(element.className || ""))) + .length; + return { text, richContentCount }; }).catch(() => null); } +function composerStateIsEmpty(state) { + return Boolean(state && !state.text && (state.richContentCount || 0) === 0); +} + +function composerStateMatchesText(state, wantedText) { + if (!state) { + return false; + } + + if (messageTextMatches(state.text, wantedText)) { + return true; + } + + if ((state.richContentCount || 0) === 0 || !containsEmojiSyntax(wantedText)) { + return false; + } + + if (isEmojiOnlyText(wantedText)) { + return true; + } + + const composerText = stripEmojiForComparison(state.text); + const wanted = stripEmojiForComparison(wantedText); + return Boolean(composerText && wanted && ( + composerText === wanted || + composerText.endsWith(wanted) || + wanted.endsWith(composerText) + )); +} + +function containsEmojiSyntax(value) { + return /[\p{Extended_Pictographic}\u200d\ufe0f\ufe0e]/u.test(String(value || "")); +} + +function isEmojiOnlyText(value) { + const text = cleanComparableText(value, 1000); + return Boolean(text && containsEmojiSyntax(text) && !stripEmojiForComparison(text)); +} + +function stripEmojiForComparison(value) { + return cleanComparableText(value, 1000) + .replace(/[\p{Extended_Pictographic}\u200d\ufe0f\ufe0e]/gu, "") + .replace(/\s+/g, " ") + .trim() + .toLowerCase(); +} + function messageTextMatches(value, wantedText) { const text = cleanComparableText(value, 1000).toLowerCase(); const wanted = cleanComparableText(wantedText, 1000).toLowerCase();