From 8e16469d9464383908250903176f9fdf77163bf3 Mon Sep 17 00:00:00 2001 From: sevenhill Date: Sat, 4 Jul 2026 23:17:44 +0300 Subject: [PATCH] Confirm outgoing MAX attachments --- worker/src/server.js | 204 ++++++++++++++++++++++++++++++++++++++----- 1 file changed, 182 insertions(+), 22 deletions(-) diff --git a/worker/src/server.js b/worker/src/server.js index bd54789..eb440ad 100644 --- a/worker/src/server.js +++ b/worker/src/server.js @@ -413,18 +413,18 @@ app.post("/send/attachment", async (req, res) => { const result = await withPageLock(async () => { 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." }; } } - const uploaded = await uploadAttachmentFile(p, path, caption); - if (!uploaded) { - return { success: false, externalMessageId: null, error: "MAX attachment upload controls were not found or rejected the file." }; + const externalMessageId = await sendAttachmentAndConfirm(p, externalChatId, path, caption); + if (!externalMessageId) { + return { success: false, externalMessageId: null, error: "MAX did not confirm the outgoing attachment." }; } - return { success: true, externalMessageId: `web-file-${Date.now()}`, error: null }; + return { success: true, externalMessageId, error: null }; }); res.json(result); @@ -3020,6 +3020,38 @@ async function sendTextAndConfirm(p, externalChatId, text) { return null; } +async function sendAttachmentAndConfirm(p, externalChatId, path, caption) { + const initialOutgoingMessages = await getOutgoingMessages(p, externalChatId); + const initialOutgoingIds = new Set(initialOutgoingMessages.map((message) => message.externalId).filter(Boolean)); + const wantedCaption = cleanComparableText(caption, 1000); + const initialMatches = wantedCaption + ? initialOutgoingMessages.filter((message) => messageTextMatches(message.text, wantedCaption)).length + : 0; + + console.info(`[send/attachment] starting upload; chat=${externalChatId || "current"} captionLength=${String(caption || "").trim().length} path=${path}`); + const triggered = await uploadAttachmentFile(p, path, caption); + if (!triggered) { + console.warn(`[send/attachment] upload/send trigger failed; chat=${externalChatId || "current"} path=${path}`); + return null; + } + + await scrollOpenChatToBottom(p); + const externalId = await findSentOutgoingExternalId( + p, + externalChatId, + caption || "", + initialMatches, + initialOutgoingIds, + 20000); + if (externalId) { + console.info(`[send/attachment] confirmed outgoing attachment; externalId=${externalId}`); + return externalId; + } + + console.warn(`[send/attachment] MAX did not expose a new outgoing attachment; chat=${externalChatId || "current"} path=${path}`); + return null; +} + async function fillComposerText(p, text, clearFirst) { if (clearFirst && !(await clearComposerInput(p))) { console.warn("[send/text] composer did not report empty after clear; continuing with input fallback"); @@ -3600,25 +3632,15 @@ async function uploadAttachmentFile(p, path, caption) { try { const fileStat = await fs.stat(path); if (!fileStat.isFile()) { + console.warn(`[send/attachment] file is not a regular file; path=${path}`); return false; } } catch { + console.warn(`[send/attachment] file does not exist or is not readable; path=${path}`); return false; } - let attached = false; - const uploadButton = p.locator("[class*='composer' i] button").first(); - if ((await uploadButton.count()) > 0) { - try { - const fileChooserPromise = p.waitForEvent("filechooser", { timeout: 5000 }); - await uploadButton.click({ timeout: 3000 }); - const fileChooser = await fileChooserPromise; - await fileChooser.setFiles(path); - attached = true; - } catch { - attached = false; - } - } + let attached = await attachFileViaUploadButton(p, path); if (!attached) { try { @@ -3633,20 +3655,158 @@ async function uploadAttachmentFile(p, path, caption) { } if (!attached) { + console.warn(`[send/attachment] no usable upload control was found; path=${path}`); return false; } - await p.waitForTimeout(1200); + await waitForAttachmentPreview(p, 8000); if (caption.trim()) { await fillComposerText(p, caption.trim(), false); await p.waitForTimeout(300); } - if (!(await clickSendButton(p))) { + const sent = await clickSendButton(p) || await clickComposerRightmostButton(p); + if (!sent) { + console.warn(`[send/attachment] send button was not found after file selection; path=${path}`); return false; } - await p.waitForTimeout(1200); - return caption.trim() ? await waitForComposerToClear(p, 3000) : true; + + await p.waitForTimeout(1500); + return true; +} + +async function attachFileViaUploadButton(p, filePath) { + const point = 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 buttons = Array.from(document.querySelectorAll("button, [role='button']")) + .filter((button) => !button.disabled && isVisible(button)); + const candidates = buttons + .map((button) => { + const rect = button.getBoundingClientRect(); + const text = [ + button.getAttribute("aria-label"), + button.getAttribute("title"), + button.getAttribute("data-testid"), + button.getAttribute("name"), + button.className, + button.innerText, + button.textContent + ].join(" ").trim().toLowerCase(); + const explicit = /attach|attachment|upload|file|paperclip|\u043f\u0440\u0438\u043a\u0440\u0435\u043f|\u0437\u0430\u0433\u0440\u0443\u0437|\u0444\u0430\u0439\u043b|\u0441\u043a\u0440\u0435\u043f/.test(text); + const composerLike = rect.left > window.innerWidth * 0.32 && rect.top > window.innerHeight * 0.55; + return { button, rect, explicit, composerLike }; + }) + .filter((candidate) => candidate.explicit || candidate.composerLike) + .sort((a, b) => + Number(b.explicit) - Number(a.explicit) || + Number(b.composerLike) - Number(a.composerLike) || + a.rect.left - b.rect.left || + b.rect.top - a.rect.top); + const button = candidates[0]?.button || null; + if (!button) { + return null; + } + + const rect = button.getBoundingClientRect(); + return { + x: Math.round(rect.left + rect.width / 2), + y: Math.round(rect.top + rect.height / 2) + }; + }).catch(() => null); + + if (!point) { + return false; + } + + try { + const fileChooserPromise = p.waitForEvent("filechooser", { timeout: 5000 }).catch(() => null); + await p.mouse.click(point.x, point.y); + const fileChooser = await fileChooserPromise; + if (!fileChooser) { + return false; + } + + await fileChooser.setFiles(filePath); + return true; + } catch { + return false; + } +} + +async function waitForAttachmentPreview(p, timeoutMs = 8000) { + const deadline = Date.now() + timeoutMs; + while (Date.now() < deadline) { + const ready = await p.evaluate(() => { + const isVisible = (element) => { + if (!(element instanceof Element)) 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 bottomArea = (element) => { + const rect = element.getBoundingClientRect(); + return rect.left > window.innerWidth * 0.30 && rect.top > window.innerHeight * 0.45; + }; + const previewSelectors = [ + "[class*='preview' i]", + "[class*='attach' i]", + "[class*='upload' i]", + "[class*='file' i]", + "[class*='media' i]", + "img", + "video" + ]; + const hasPreview = previewSelectors + .flatMap((selector) => Array.from(document.querySelectorAll(selector))) + .some((element) => isVisible(element) && bottomArea(element)); + const sendButton = Array.from(document.querySelectorAll("button, [role='button']")) + .some((button) => { + if (button.disabled || !isVisible(button) || !bottomArea(button)) { + return false; + } + + const text = [ + button.getAttribute("aria-label"), + button.getAttribute("title"), + button.getAttribute("data-testid"), + button.className, + button.innerText, + button.textContent + ].join(" ").toLowerCase(); + return /send|submit|\u043e\u0442\u043f\u0440\u0430\u0432/.test(text); + }); + return hasPreview || sendButton; + }).catch(() => false); + + if (ready) { + return true; + } + + await p.waitForTimeout(250); + } + + console.warn("[send/attachment] attachment preview was not detected before sending; continuing with send attempt"); + return false; } async function clickSendButton(p) {