Confirm outgoing MAX attachments
This commit is contained in:
+182
-22
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user