Confirm outgoing MAX attachments

This commit is contained in:
sevenhill
2026-07-04 23:17:44 +03:00
parent 05f971c5c7
commit 8e16469d94
+182 -22
View File
@@ -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) {