Fix emoji sending and self push notifications
This commit is contained in:
+172
-7
@@ -468,6 +468,7 @@ async function ensurePage() {
|
||||
viewport: { width: 1280, height: 900 },
|
||||
args: ["--disable-dev-shm-usage", "--no-sandbox"]
|
||||
});
|
||||
await context.grantPermissions(["clipboard-read", "clipboard-write"], { origin: maxBaseUrl }).catch(() => {});
|
||||
page = context.pages()[0] || await context.newPage();
|
||||
attachNetworkCapture(page);
|
||||
if (!page.url() || page.url() === "about:blank") {
|
||||
@@ -2837,7 +2838,11 @@ async function sendTextAndConfirm(p, externalChatId, text) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const initialMatches = await countOutgoingTextMatches(p, externalChatId, text);
|
||||
const initialOutgoingMessages = await getOutgoingMessages(p, externalChatId);
|
||||
const initialOutgoingIds = new Set(initialOutgoingMessages.map((message) => message.externalId).filter(Boolean));
|
||||
const initialMatches = initialOutgoingMessages
|
||||
.filter((message) => messageTextMatches(message.text, wanted))
|
||||
.length;
|
||||
const attempts = [
|
||||
{ name: "send button", action: () => clickSendButton(p) },
|
||||
{ name: "Enter", action: async () => { await p.keyboard.press("Enter"); return true; } },
|
||||
@@ -2860,7 +2865,13 @@ async function sendTextAndConfirm(p, externalChatId, text) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const externalId = await findSentOutgoingExternalId(p, externalChatId, text, initialMatches, 10000);
|
||||
const externalId = await findSentOutgoingExternalId(
|
||||
p,
|
||||
externalChatId,
|
||||
text,
|
||||
initialMatches,
|
||||
initialOutgoingIds,
|
||||
10000);
|
||||
if (externalId) {
|
||||
return externalId;
|
||||
}
|
||||
@@ -2875,10 +2886,18 @@ async function sendTextAndConfirm(p, externalChatId, text) {
|
||||
}
|
||||
|
||||
async function fillComposerText(p, text, clearFirst) {
|
||||
if (clearFirst && prefersClipboardComposerInput(text) && await fillComposerViaClipboard(p, text, true)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (clearFirst && await fillComposerViaLocator(p, text)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (clearFirst && await fillComposerViaClipboard(p, text, true)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
for (let attempt = 0; attempt < 2; attempt++) {
|
||||
if (!(await focusComposer(p))) {
|
||||
break;
|
||||
@@ -2931,6 +2950,131 @@ async function fillComposerText(p, text, clearFirst) {
|
||||
return false;
|
||||
}
|
||||
|
||||
function prefersClipboardComposerInput(text) {
|
||||
return /[\p{Extended_Pictographic}\u200d\ufe0f]/u.test(String(text || ""));
|
||||
}
|
||||
|
||||
async function fillComposerViaClipboard(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");
|
||||
}
|
||||
|
||||
const wroteClipboard = await p.evaluate(async (value) => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(value);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}, text).catch(() => false);
|
||||
|
||||
if (wroteClipboard) {
|
||||
await p.keyboard.press(process.platform === "darwin" ? "Meta+V" : "Control+V");
|
||||
} else if (!(await insertComposerTextViaDom(p, text, false))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
await p.waitForTimeout(300);
|
||||
const currentText = await readComposerText(p);
|
||||
return currentText !== null && messageTextMatches(currentText, text);
|
||||
}
|
||||
|
||||
async function insertComposerTextViaDom(p, text, clearFirst = false) {
|
||||
return await p.evaluate(({ value, clear }) => {
|
||||
const cleanText = (textValue) => String(textValue || "")
|
||||
.replace(/\u00a0/g, " ")
|
||||
.replace(/[\u200b\u200c\u200d\ufeff]/g, "")
|
||||
.replace(/\s+/g, " ")
|
||||
.trim();
|
||||
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 isSearchLike = (element, rect) => {
|
||||
const textValue = cleanText([
|
||||
element.getAttribute("aria-label"),
|
||||
element.getAttribute("placeholder"),
|
||||
element.getAttribute("title"),
|
||||
element.getAttribute("data-testid"),
|
||||
element.className,
|
||||
element.id,
|
||||
element.getAttribute("name")
|
||||
].join(" ")).toLowerCase();
|
||||
return /search|\u043f\u043e\u0438\u0441\u043a/.test(textValue) ||
|
||||
(rect.left < window.innerWidth * 0.42 && rect.top < window.innerHeight * 0.32);
|
||||
};
|
||||
const selectors = [
|
||||
"[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]",
|
||||
"[role='textbox']",
|
||||
"[contenteditable='true']",
|
||||
"textarea"
|
||||
];
|
||||
const candidates = selectors
|
||||
.flatMap((selector) => Array.from(document.querySelectorAll(selector)))
|
||||
.filter((element) => isVisible(element))
|
||||
.filter((element) => {
|
||||
const rect = element.getBoundingClientRect();
|
||||
return !isSearchLike(element, rect) &&
|
||||
rect.left > window.innerWidth * 0.32 &&
|
||||
rect.top > window.innerHeight * 0.50;
|
||||
})
|
||||
.sort((a, b) => {
|
||||
const ar = a.getBoundingClientRect();
|
||||
const br = b.getBoundingClientRect();
|
||||
return br.bottom - ar.bottom || (br.width * br.height) - (ar.width * ar.height);
|
||||
});
|
||||
const composer = candidates[0];
|
||||
if (!(composer instanceof HTMLElement)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
composer.focus({ preventScroll: true });
|
||||
if (clear) {
|
||||
if ("value" in composer) {
|
||||
composer.value = "";
|
||||
} else {
|
||||
composer.textContent = "";
|
||||
}
|
||||
composer.dispatchEvent(new InputEvent("input", { bubbles: true, inputType: "deleteContentBackward", data: null }));
|
||||
}
|
||||
|
||||
const beforeInput = new InputEvent("beforeinput", {
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
inputType: "insertText",
|
||||
data: value
|
||||
});
|
||||
composer.dispatchEvent(beforeInput);
|
||||
if ("value" in composer) {
|
||||
composer.value = `${composer.value || ""}${value}`;
|
||||
} else if (!document.execCommand("insertText", false, value)) {
|
||||
composer.textContent = `${composer.textContent || ""}${value}`;
|
||||
}
|
||||
composer.dispatchEvent(new InputEvent("input", { bubbles: true, inputType: "insertText", data: value }));
|
||||
composer.dispatchEvent(new Event("change", { bubbles: true }));
|
||||
return true;
|
||||
}, { value: text, clear: clearFirst }).catch(() => false);
|
||||
}
|
||||
|
||||
async function fillComposerViaLocator(p, text) {
|
||||
const selectors = [
|
||||
"[data-testid='composer'] [role='textbox']",
|
||||
@@ -3145,32 +3289,53 @@ function messageTextMatches(value, wantedText) {
|
||||
));
|
||||
}
|
||||
|
||||
async function getOutgoingMessages(p, externalChatId) {
|
||||
const history = await extractOpenChatHistory(p, externalChatId).catch(() => null);
|
||||
return (history?.messages || []).filter((message) => message.isOutgoing);
|
||||
}
|
||||
|
||||
async function getOutgoingTextMatches(p, externalChatId, text) {
|
||||
const wanted = cleanComparableText(text, 1000);
|
||||
if (!wanted) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const history = await extractOpenChatHistory(p, externalChatId).catch(() => null);
|
||||
return (history?.messages || []).filter((message) =>
|
||||
message.isOutgoing && messageTextMatches(message.text, wanted));
|
||||
return (await getOutgoingMessages(p, externalChatId))
|
||||
.filter((message) => messageTextMatches(message.text, wanted));
|
||||
}
|
||||
|
||||
async function countOutgoingTextMatches(p, externalChatId, text) {
|
||||
return (await getOutgoingTextMatches(p, externalChatId, text)).length;
|
||||
}
|
||||
|
||||
async function findSentOutgoingExternalId(p, externalChatId, text, previousMatchCount = 0, timeoutMs = 15000) {
|
||||
async function findSentOutgoingExternalId(
|
||||
p,
|
||||
externalChatId,
|
||||
text,
|
||||
previousMatchCount = 0,
|
||||
initialOutgoingIds = new Set(),
|
||||
timeoutMs = 15000) {
|
||||
const knownIds = initialOutgoingIds instanceof Set
|
||||
? initialOutgoingIds
|
||||
: new Set(Array.from(initialOutgoingIds || []).filter(Boolean));
|
||||
const deadline = Date.now() + timeoutMs;
|
||||
while (Date.now() < deadline) {
|
||||
await p.waitForTimeout(500);
|
||||
const matches = await getOutgoingTextMatches(p, externalChatId, text);
|
||||
const outgoingMessages = await getOutgoingMessages(p, externalChatId);
|
||||
const matches = outgoingMessages.filter((message) => messageTextMatches(message.text, text));
|
||||
if (matches.length > previousMatchCount) {
|
||||
const latest = matches[matches.length - 1];
|
||||
if (latest?.externalId) {
|
||||
return latest.externalId;
|
||||
}
|
||||
}
|
||||
|
||||
const newOutgoing = outgoingMessages.filter((message) =>
|
||||
message.externalId &&
|
||||
!knownIds.has(message.externalId));
|
||||
if (newOutgoing.length > 0) {
|
||||
return newOutgoing[newOutgoing.length - 1].externalId;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
|
||||
Reference in New Issue
Block a user