Fix emoji sending and self push notifications

This commit is contained in:
sevenhill
2026-07-04 21:47:06 +03:00
parent b3192d793d
commit 3e3d1538eb
4 changed files with 248 additions and 8 deletions
+172 -7
View File
@@ -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;