Store MAX chat URLs for direct opens
This commit is contained in:
+476
-22
@@ -25,6 +25,33 @@ const app = express();
|
||||
app.use(cors());
|
||||
app.use(express.json({ limit: "10mb" }));
|
||||
|
||||
function normalizeMaxChatUrl(value) {
|
||||
const raw = String(value || "").trim();
|
||||
if (!raw) return "";
|
||||
|
||||
try {
|
||||
const url = new URL(raw, maxBaseUrl);
|
||||
const base = new URL(maxBaseUrl);
|
||||
if (url.origin !== base.origin) {
|
||||
return "";
|
||||
}
|
||||
|
||||
return url.href;
|
||||
} catch {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
async function getCurrentChatUrl(p) {
|
||||
const current = normalizeMaxChatUrl(p.url());
|
||||
if (!current) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const base = new URL(maxBaseUrl).href;
|
||||
return current === base ? null : current;
|
||||
}
|
||||
|
||||
app.get("/health", (_req, res) => {
|
||||
res.json({ status: "ok", serverTime: new Date().toISOString() });
|
||||
});
|
||||
@@ -221,6 +248,24 @@ app.get("/inspect/network", async (_req, res) => {
|
||||
});
|
||||
});
|
||||
|
||||
app.get("/inspect/chat-urls", async (req, res) => {
|
||||
try {
|
||||
const limit = clampNumber(req.query?.limit, 1, 800, 600);
|
||||
const result = await withPageLock(async () => {
|
||||
const p = await ensurePage("incoming");
|
||||
const currentStatus = await status(null, p);
|
||||
if (!currentStatus.isAuthorized) {
|
||||
return { chats: [], error: "MAX is not authorized." };
|
||||
}
|
||||
|
||||
return { chats: await collectSidebarChatUrls(p, limit), error: null };
|
||||
}, "incoming");
|
||||
res.json(result);
|
||||
} catch (error) {
|
||||
res.status(500).json(errorStatus(error));
|
||||
}
|
||||
});
|
||||
|
||||
app.get("/media/fetch", async (req, res) => {
|
||||
try {
|
||||
const mediaUrl = String(req.query?.url || "").trim();
|
||||
@@ -263,6 +308,7 @@ app.get("/updates", async (_req, res) => {
|
||||
app.post("/chat/history", async (req, res) => {
|
||||
try {
|
||||
const externalChatId = String(req.body?.externalChatId || "").trim();
|
||||
const chatUrl = normalizeMaxChatUrl(req.body?.chatUrl);
|
||||
const result = await withPageLock(async () => {
|
||||
const p = await ensurePage("incoming");
|
||||
const currentStatus = await status(null, p);
|
||||
@@ -271,14 +317,15 @@ app.post("/chat/history", async (req, res) => {
|
||||
}
|
||||
|
||||
if (externalChatId) {
|
||||
const opened = await ensureDomChatOpen(p, externalChatId, 1000);
|
||||
const opened = await ensureDomChatOpen(p, externalChatId, 1000, false, chatUrl);
|
||||
if (!opened) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
await scrollOpenChatToLatest(p);
|
||||
return await extractOpenChatHistory(p, externalChatId);
|
||||
const update = await extractOpenChatHistory(p, externalChatId);
|
||||
return { ...update, chatUrl: await getCurrentChatUrl(p) };
|
||||
}, "incoming");
|
||||
|
||||
if (!result) {
|
||||
@@ -353,6 +400,7 @@ app.post("/chat/draft-text", async (req, res) => {
|
||||
app.post("/chat/presence", async (req, res) => {
|
||||
try {
|
||||
const externalChatId = String(req.body?.externalChatId || "").trim();
|
||||
const chatUrl = normalizeMaxChatUrl(req.body?.chatUrl);
|
||||
const result = await withPageLock(async () => {
|
||||
const p = await ensurePage("incoming");
|
||||
const currentStatus = await status(null, p);
|
||||
@@ -361,7 +409,7 @@ app.post("/chat/presence", async (req, res) => {
|
||||
}
|
||||
|
||||
if (externalChatId) {
|
||||
await ensureDomChatOpen(p, externalChatId, 600);
|
||||
await ensureDomChatOpen(p, externalChatId, 600, false, chatUrl);
|
||||
}
|
||||
|
||||
return await extractOpenChatPresence(p);
|
||||
@@ -376,6 +424,7 @@ app.post("/chat/presence", async (req, res) => {
|
||||
app.post("/send/text", async (req, res) => {
|
||||
try {
|
||||
const externalChatId = String(req.body?.externalChatId || "").trim();
|
||||
const chatUrl = normalizeMaxChatUrl(req.body?.chatUrl);
|
||||
const text = String(req.body?.text || "");
|
||||
if (!text) {
|
||||
return res.json({ success: false, externalMessageId: null, error: "Text is empty." });
|
||||
@@ -385,17 +434,17 @@ app.post("/send/text", async (req, res) => {
|
||||
const p = await ensurePage("outgoing");
|
||||
|
||||
if (externalChatId) {
|
||||
const opened = await ensureDomChatOpen(p, externalChatId, 8000, true);
|
||||
const opened = await ensureDomChatOpen(p, externalChatId, 8000, true, chatUrl);
|
||||
if (!opened) {
|
||||
return { success: false, externalMessageId: null, error: "MAX chat was not found or did not open." };
|
||||
return { success: false, externalMessageId: null, error: "MAX chat was not found or did not open.", chatUrl: null };
|
||||
}
|
||||
}
|
||||
|
||||
const externalMessageId = await sendTextAndConfirm(p, externalChatId, text);
|
||||
if (!externalMessageId) {
|
||||
return { success: false, externalMessageId: null, error: "MAX did not confirm the outgoing text message." };
|
||||
return { success: false, externalMessageId: null, error: "MAX did not confirm the outgoing text message.", chatUrl: await getCurrentChatUrl(p) };
|
||||
}
|
||||
return { success: true, externalMessageId, error: null };
|
||||
return { success: true, externalMessageId, error: null, chatUrl: await getCurrentChatUrl(p) };
|
||||
}, "outgoing");
|
||||
res.json(result);
|
||||
} catch (error) {
|
||||
@@ -406,6 +455,7 @@ app.post("/send/text", async (req, res) => {
|
||||
app.post("/send/attachment", async (req, res) => {
|
||||
try {
|
||||
const externalChatId = String(req.body?.externalChatId || "").trim();
|
||||
const chatUrl = normalizeMaxChatUrl(req.body?.chatUrl);
|
||||
const path = String(req.body?.path || "").trim();
|
||||
const caption = String(req.body?.caption || "");
|
||||
if (!path) {
|
||||
@@ -415,18 +465,18 @@ app.post("/send/attachment", async (req, res) => {
|
||||
const result = await withPageLock(async () => {
|
||||
const p = await ensurePage("outgoing");
|
||||
if (externalChatId) {
|
||||
const opened = await ensureDomChatOpen(p, externalChatId, 8000, true);
|
||||
const opened = await ensureDomChatOpen(p, externalChatId, 8000, true, chatUrl);
|
||||
if (!opened) {
|
||||
return { success: false, externalMessageId: null, error: "MAX chat was not found or did not open." };
|
||||
return { success: false, externalMessageId: null, error: "MAX chat was not found or did not open.", chatUrl: null };
|
||||
}
|
||||
}
|
||||
|
||||
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: false, externalMessageId: null, error: "MAX did not confirm the outgoing attachment.", chatUrl: await getCurrentChatUrl(p) };
|
||||
}
|
||||
|
||||
return { success: true, externalMessageId, error: null };
|
||||
return { success: true, externalMessageId, error: null, chatUrl: await getCurrentChatUrl(p) };
|
||||
}, "outgoing");
|
||||
|
||||
res.json(result);
|
||||
@@ -1622,6 +1672,30 @@ async function extractUpdates(p) {
|
||||
}
|
||||
return null;
|
||||
};
|
||||
const extractChatUrl = (element) => {
|
||||
const candidates = [
|
||||
element.getAttribute("href"),
|
||||
element.closest("a")?.getAttribute("href"),
|
||||
element.getAttribute("data-url"),
|
||||
element.getAttribute("data-href"),
|
||||
element.getAttribute("data-link"),
|
||||
element.getAttribute("data-path")
|
||||
];
|
||||
for (const attr of Array.from(element.attributes || [])) {
|
||||
if (/url|href|link|path/i.test(attr.name)) {
|
||||
candidates.push(attr.value);
|
||||
}
|
||||
}
|
||||
|
||||
for (const candidate of candidates) {
|
||||
const url = absoluteUrl(candidate);
|
||||
if (url) {
|
||||
return url;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
const extractTitle = (element, lines) => {
|
||||
const titleNode = element.querySelector("h3, [class*='title' i] [class*='name' i], [class*='name' i]");
|
||||
const title = cleanText(titleNode?.innerText || titleNode?.textContent, 120);
|
||||
@@ -1687,7 +1761,7 @@ async function extractUpdates(p) {
|
||||
(rect.left > 60 && rect.left < window.innerWidth * 0.55);
|
||||
if (!looksLikeChat) continue;
|
||||
const avatarUrl = extractAvatarUrl(element);
|
||||
const href = element.getAttribute("href") || element.closest("a")?.getAttribute("href") || null;
|
||||
const href = extractChatUrl(element);
|
||||
const key = `${title}|${avatarUrl || href || text}`;
|
||||
if (seen.has(key)) continue;
|
||||
seen.add(key);
|
||||
@@ -1705,7 +1779,7 @@ async function extractUpdates(p) {
|
||||
height: Math.round(rect.height)
|
||||
}
|
||||
});
|
||||
if (chats.length >= 160) break;
|
||||
if (chats.length >= 600) break;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1730,7 +1804,7 @@ async function extractUpdates(p) {
|
||||
await delay(120);
|
||||
|
||||
let previousTop = -1;
|
||||
for (let page = 0; page < 80 && chats.length < 160; page++) {
|
||||
for (let page = 0; page < 180 && chats.length < 600; page++) {
|
||||
collectVisibleChats();
|
||||
const bottom = maxTop();
|
||||
if (scrollRoot.scrollTop >= bottom - 4) break;
|
||||
@@ -1770,6 +1844,7 @@ async function extractUpdates(p) {
|
||||
externalId,
|
||||
title: chat.title,
|
||||
avatarUrl: chat.avatarUrl || null,
|
||||
chatUrl: chat.href || null,
|
||||
updatedAt: now,
|
||||
lastMessagePreview: messageText || null,
|
||||
lastMessageIsOutgoing: outgoing,
|
||||
@@ -2681,6 +2756,345 @@ async function resetSidebarListToTop(p) {
|
||||
await p.waitForTimeout(250);
|
||||
}
|
||||
|
||||
async function focusSidebarSearch(p) {
|
||||
const focused = await p.evaluate(async () => {
|
||||
const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
||||
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 textMeta = (element) => [
|
||||
element.getAttribute("aria-label"),
|
||||
element.getAttribute("placeholder"),
|
||||
element.getAttribute("title"),
|
||||
element.getAttribute("data-testid"),
|
||||
element.className,
|
||||
element.id,
|
||||
element.getAttribute("name"),
|
||||
element.innerText,
|
||||
element.textContent
|
||||
].join(" ").toLowerCase();
|
||||
const setNativeValue = (element, value) => {
|
||||
const descriptor = Object.getOwnPropertyDescriptor(element.constructor.prototype, "value");
|
||||
if (descriptor?.set) {
|
||||
descriptor.set.call(element, value);
|
||||
} else {
|
||||
element.value = value;
|
||||
}
|
||||
};
|
||||
const findInput = () => {
|
||||
const aside = document.querySelector("aside") || document.body;
|
||||
return Array.from(aside.querySelectorAll("input, textarea, [contenteditable='true'], [role='textbox']"))
|
||||
.filter(isVisible)
|
||||
.filter((element) => {
|
||||
const rect = element.getBoundingClientRect();
|
||||
const text = textMeta(element);
|
||||
const isSearchLike = /search|\u043f\u043e\u0438\u0441\u043a/.test(text);
|
||||
const isTopSidebarInput = rect.left < window.innerWidth * 0.42 &&
|
||||
rect.top < window.innerHeight * 0.34;
|
||||
return isSearchLike || isTopSidebarInput;
|
||||
})
|
||||
.sort((a, b) => {
|
||||
const aSearch = /search|\u043f\u043e\u0438\u0441\u043a/.test(textMeta(a)) ? 0 : 1;
|
||||
const bSearch = /search|\u043f\u043e\u0438\u0441\u043a/.test(textMeta(b)) ? 0 : 1;
|
||||
if (aSearch !== bSearch) return aSearch - bSearch;
|
||||
return a.getBoundingClientRect().top - b.getBoundingClientRect().top;
|
||||
})[0] || null;
|
||||
};
|
||||
let input = findInput();
|
||||
if (!input) {
|
||||
const aside = document.querySelector("aside") || document.body;
|
||||
const button = Array.from(aside.querySelectorAll("button, [role='button']"))
|
||||
.filter(isVisible)
|
||||
.find((element) => /search|\u043f\u043e\u0438\u0441\u043a/.test(textMeta(element)));
|
||||
if (button) {
|
||||
button.click();
|
||||
await delay(200);
|
||||
input = findInput();
|
||||
}
|
||||
}
|
||||
if (!input) return false;
|
||||
|
||||
input.focus();
|
||||
if ("value" in input) {
|
||||
setNativeValue(input, "");
|
||||
} else {
|
||||
input.textContent = "";
|
||||
}
|
||||
input.dispatchEvent(new InputEvent("input", { bubbles: true, inputType: "deleteContentBackward", data: null }));
|
||||
input.dispatchEvent(new Event("change", { bubbles: true }));
|
||||
return true;
|
||||
}).catch(() => false);
|
||||
|
||||
if (focused) {
|
||||
await p.waitForTimeout(100);
|
||||
}
|
||||
return Boolean(focused);
|
||||
}
|
||||
|
||||
async function searchSidebarChat(p, title) {
|
||||
await clearSidebarSearch(p);
|
||||
const focused = await focusSidebarSearch(p);
|
||||
if (!focused) {
|
||||
return false;
|
||||
}
|
||||
|
||||
await p.keyboard.insertText(title);
|
||||
await p.waitForTimeout(700);
|
||||
return true;
|
||||
}
|
||||
|
||||
async function waitForMaxChatListReady(p, timeoutMs = 15000) {
|
||||
const deadline = Date.now() + timeoutMs;
|
||||
while (Date.now() < deadline) {
|
||||
const ready = 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 > 20 &&
|
||||
rect.height > 12 &&
|
||||
rect.bottom >= 0 &&
|
||||
rect.right >= 0 &&
|
||||
rect.top <= window.innerHeight &&
|
||||
rect.left <= window.innerWidth;
|
||||
};
|
||||
|
||||
return Array.from(document.querySelectorAll("aside button.cell, aside button[class*='cell' i]"))
|
||||
.some(isVisible);
|
||||
}).catch(() => false);
|
||||
|
||||
if (ready) {
|
||||
return true;
|
||||
}
|
||||
|
||||
await p.waitForTimeout(250);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
async function readVisibleSidebarChatRows(p) {
|
||||
return await p.evaluate(() => {
|
||||
const cleanText = (value, limit = 500) => String(value || "")
|
||||
.replace(/\u00a0/g, " ")
|
||||
.replace(/\s+/g, " ")
|
||||
.trim()
|
||||
.slice(0, limit);
|
||||
const linesFrom = (value) => String(value || "")
|
||||
.split(/\n+/)
|
||||
.map((line) => cleanText(line, 180))
|
||||
.filter(Boolean);
|
||||
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 > 20 &&
|
||||
rect.height > 12 &&
|
||||
rect.bottom >= 0 &&
|
||||
rect.right >= 0 &&
|
||||
rect.top <= window.innerHeight &&
|
||||
rect.left <= window.innerWidth;
|
||||
};
|
||||
const absoluteUrl = (value) => {
|
||||
const raw = String(value || "").trim();
|
||||
if (!raw || raw.startsWith("data:")) return null;
|
||||
try {
|
||||
return new URL(raw, location.href).href;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
const cssBackgroundUrl = (element) => {
|
||||
const image = window.getComputedStyle(element).backgroundImage || "";
|
||||
const match = image.match(/url\(["']?(.+?)["']?\)/i);
|
||||
return match ? absoluteUrl(match[1]) : null;
|
||||
};
|
||||
const extractAvatarUrl = (element) => {
|
||||
const rowRect = element.getBoundingClientRect();
|
||||
const candidates = Array.from(element.querySelectorAll("img, image, [style*='background' i], [class*='avatar' i], [class*='photo' i]"));
|
||||
for (const node of candidates) {
|
||||
if (!(node instanceof Element)) continue;
|
||||
const rect = node.getBoundingClientRect();
|
||||
if (rect.width < 24 || rect.height < 24 || rect.width > 96 || rect.height > 96) continue;
|
||||
if (rect.left > rowRect.left + rowRect.width * 0.45) continue;
|
||||
const source = node.currentSrc ||
|
||||
node.src ||
|
||||
node.href?.baseVal ||
|
||||
node.getAttribute("src") ||
|
||||
node.getAttribute("href") ||
|
||||
cssBackgroundUrl(node);
|
||||
const url = absoluteUrl(source);
|
||||
if (url && !/pattern|sprite|emoji|sticker|logo/i.test(url)) {
|
||||
return url;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
const extractTitle = (element, lines) => {
|
||||
const titleNode = element.querySelector("h3, [class*='title' i] [class*='name' i], [class*='name' i]");
|
||||
const title = cleanText(titleNode?.innerText || titleNode?.textContent, 160);
|
||||
return title || lines.find((line) => line && !/^\d{1,3}$/.test(line)) || "";
|
||||
};
|
||||
const extractPreview = (element, title, lines) => {
|
||||
const timeText = cleanText(element.querySelector("[class*='time' i], [class*='meta' i]")?.innerText, 80);
|
||||
const ignored = new Set([
|
||||
title,
|
||||
timeText,
|
||||
cleanText(element.querySelector("[class*='badge' i], [class*='indicator' i], [class*='counter' i]")?.innerText, 80)
|
||||
].filter(Boolean));
|
||||
return lines.find((line) => line !== title && !ignored.has(line) && !/^\d{1,3}$/.test(line)) || null;
|
||||
};
|
||||
const extractTimeText = (element, lines) => {
|
||||
const direct = cleanText(element.querySelector("[class*='time' i], [class*='meta' i]")?.innerText, 80);
|
||||
if (direct) return direct;
|
||||
return lines.find((line) =>
|
||||
/^\d{1,2}:\d{2}$/.test(line) ||
|
||||
/^(\u0432\u0447\u0435\u0440\u0430|\u0441\u0435\u0433\u043e\u0434\u043d\u044f)$/i.test(line) ||
|
||||
/^\d{1,2}\s+[^\s.]+\.?$/i.test(line) ||
|
||||
/^(mon|tue|wed|thu|fri|sat|sun|\u043f\u043d|\u0432\u0442|\u0441\u0440|\u0447\u0442|\u043f\u0442|\u0441\u0431|\u0432\u0441)$/i.test(line)
|
||||
) || null;
|
||||
};
|
||||
const selector = [
|
||||
"aside button.cell",
|
||||
"aside button[class*='cell' i]",
|
||||
"aside [role='presentation'] button",
|
||||
"aside [class*='wrapper' i] button",
|
||||
"aside [data-testid*='chat' i]",
|
||||
"aside [data-testid*='dialog' i]",
|
||||
"aside [data-testid*='conversation' i]"
|
||||
].join(",");
|
||||
|
||||
return Array.from(document.querySelectorAll(selector))
|
||||
.filter(isVisible)
|
||||
.map((element) => {
|
||||
const rect = element.getBoundingClientRect();
|
||||
const text = cleanText(element.innerText || element.textContent, 500);
|
||||
const lines = linesFrom(element.innerText || element.textContent);
|
||||
const title = extractTitle(element, lines);
|
||||
const avatarUrl = extractAvatarUrl(element);
|
||||
return {
|
||||
title,
|
||||
text,
|
||||
preview: extractPreview(element, title, lines),
|
||||
timeText: extractTimeText(element, lines),
|
||||
avatarUrl,
|
||||
x: Math.round(rect.left + Math.min(rect.width - 8, Math.max(8, rect.width * 0.50))),
|
||||
y: Math.round(rect.top + rect.height / 2),
|
||||
top: Math.round(rect.top)
|
||||
};
|
||||
})
|
||||
.filter((row) => row.title && row.text)
|
||||
.sort((a, b) => a.top - b.top);
|
||||
}).catch(() => []);
|
||||
}
|
||||
|
||||
async function scrollSidebarForward(p) {
|
||||
return await p.evaluate(() => {
|
||||
const scrollRoot = Array.from(document.querySelectorAll("aside, aside *"))
|
||||
.filter((element) => element instanceof HTMLElement)
|
||||
.filter((element) => {
|
||||
const style = window.getComputedStyle(element);
|
||||
const rect = element.getBoundingClientRect();
|
||||
return style.display !== "none" &&
|
||||
style.visibility !== "hidden" &&
|
||||
rect.width > 180 &&
|
||||
rect.height > 160 &&
|
||||
element.scrollHeight > element.clientHeight + 80;
|
||||
})
|
||||
.sort((a, b) => (b.scrollHeight - b.clientHeight) - (a.scrollHeight - a.clientHeight))[0] || null;
|
||||
|
||||
if (!scrollRoot) return false;
|
||||
const bottom = Math.max(0, scrollRoot.scrollHeight - scrollRoot.clientHeight);
|
||||
if (scrollRoot.scrollTop >= bottom - 4) return false;
|
||||
const nextTop = Math.min(bottom, scrollRoot.scrollTop + Math.max(220, Math.floor(scrollRoot.clientHeight * 0.85)));
|
||||
if (nextTop === scrollRoot.scrollTop) return false;
|
||||
scrollRoot.scrollTop = nextTop;
|
||||
scrollRoot.dispatchEvent(new Event("scroll", { bubbles: true }));
|
||||
return true;
|
||||
}).catch(() => false);
|
||||
}
|
||||
|
||||
async function collectSidebarChatUrls(p, limit = 600) {
|
||||
await clearSidebarSearch(p);
|
||||
await resetSidebarListToTop(p);
|
||||
await waitForMaxChatListReady(p, 15000);
|
||||
|
||||
const results = [];
|
||||
const seen = new Set();
|
||||
let stagnantPages = 0;
|
||||
|
||||
for (let pageIndex = 0; pageIndex < 220 && results.length < limit; pageIndex++) {
|
||||
const rows = await readVisibleSidebarChatRows(p);
|
||||
let addedOnPage = 0;
|
||||
|
||||
for (const row of rows) {
|
||||
if (results.length >= limit) break;
|
||||
const key = `${row.title}|${row.avatarUrl || ""}|${row.text}`;
|
||||
if (seen.has(key)) continue;
|
||||
seen.add(key);
|
||||
addedOnPage++;
|
||||
|
||||
await p.mouse.click(row.x, row.y);
|
||||
const ready = await waitForDomChatReady(p, row.title, 5000, false);
|
||||
await p.waitForTimeout(150);
|
||||
const chatUrl = await getCurrentChatUrl(p);
|
||||
results.push({
|
||||
externalId: makeDomChatExternalId(row.title, row.text, "", results.length, row.avatarUrl),
|
||||
title: row.title,
|
||||
preview: row.preview,
|
||||
timeText: row.timeText,
|
||||
avatarUrl: row.avatarUrl || null,
|
||||
chatUrl,
|
||||
ready,
|
||||
rowText: row.text
|
||||
});
|
||||
}
|
||||
|
||||
const moved = await scrollSidebarForward(p);
|
||||
if (!moved) break;
|
||||
await p.waitForTimeout(250);
|
||||
|
||||
stagnantPages = addedOnPage === 0 ? stagnantPages + 1 : 0;
|
||||
if (stagnantPages >= 4) break;
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
async function openDomChatByUrl(p, chatUrl) {
|
||||
const targetUrl = normalizeMaxChatUrl(chatUrl);
|
||||
if (!targetUrl) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const currentUrl = normalizeMaxChatUrl(p.url());
|
||||
if (currentUrl === targetUrl) {
|
||||
return true;
|
||||
}
|
||||
|
||||
try {
|
||||
await p.goto(targetUrl, { waitUntil: "domcontentloaded", timeout: 30000 });
|
||||
await p.waitForTimeout(900);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function scrollOpenChatToLatest(p) {
|
||||
for (let attempt = 0; attempt < 2; attempt++) {
|
||||
await p.evaluate(() => {
|
||||
@@ -2712,15 +3126,18 @@ async function scrollOpenChatToLatest(p) {
|
||||
}
|
||||
}
|
||||
|
||||
async function openDomChatByExternalId(p, externalChatId) {
|
||||
async function openDomChatByExternalId(p, externalChatId, options = {}) {
|
||||
const descriptor = decodeDomChatDescriptor(externalChatId);
|
||||
const title = descriptor?.title || externalChatId;
|
||||
if (!title) {
|
||||
return false;
|
||||
}
|
||||
|
||||
await clearSidebarSearch(p);
|
||||
await resetSidebarListToTop(p);
|
||||
if (!options.preserveSidebarSearch) {
|
||||
await clearSidebarSearch(p);
|
||||
await resetSidebarListToTop(p);
|
||||
await waitForMaxChatListReady(p, 15000);
|
||||
}
|
||||
|
||||
const point = await p.evaluate(async (requested) => {
|
||||
const cleanText = (value) => String(value || "")
|
||||
@@ -2804,8 +3221,19 @@ async function openDomChatByExternalId(p, externalChatId) {
|
||||
}
|
||||
|
||||
const findMatch = () => {
|
||||
const buttons = Array.from(document.querySelectorAll("aside button.cell, aside button[class*='cell' i]"));
|
||||
const candidates = buttons.map((button) => {
|
||||
const chatSelector = [
|
||||
"aside button.cell",
|
||||
"aside button[class*='cell' i]",
|
||||
"aside [role='presentation'] button",
|
||||
"aside [class*='wrapper' i] button",
|
||||
"aside [data-testid*='chat' i]",
|
||||
"aside [data-testid*='dialog' i]",
|
||||
"aside [data-testid*='conversation' i]",
|
||||
"aside a[href*='chat' i]",
|
||||
"aside a[href*='dialog' i]"
|
||||
].join(",");
|
||||
const rows = Array.from(document.querySelectorAll(chatSelector));
|
||||
const candidates = rows.map((button) => {
|
||||
const titleNode = button.querySelector("h3, [class*='title' i], [class*='name' i]");
|
||||
const title = normalize(titleNode?.innerText || titleNode?.textContent);
|
||||
const text = normalize(button.innerText || button.textContent);
|
||||
@@ -2899,16 +3327,39 @@ async function openDomChatByExternalId(p, externalChatId) {
|
||||
return true;
|
||||
}
|
||||
|
||||
async function ensureDomChatOpen(p, externalChatId, waitMs = 600, requireComposer = false) {
|
||||
async function ensureDomChatOpen(p, externalChatId, waitMs = 600, requireComposer = false, chatUrl = null) {
|
||||
const descriptor = decodeDomChatDescriptor(externalChatId);
|
||||
const title = descriptor?.title || externalChatId;
|
||||
if (!title) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const directUrl = normalizeMaxChatUrl(chatUrl) || normalizeMaxChatUrl(descriptor?.href);
|
||||
for (let attempt = 0; attempt < 2; attempt++) {
|
||||
const opened = await openDomChatByExternalId(p, externalChatId);
|
||||
if (directUrl) {
|
||||
const openedByUrl = await openDomChatByUrl(p, directUrl);
|
||||
if (openedByUrl) {
|
||||
const readyByUrl = await waitForDomChatReady(
|
||||
p,
|
||||
title,
|
||||
Math.max(waitMs, requireComposer ? 8000 : 2500),
|
||||
requireComposer);
|
||||
if (readyByUrl) {
|
||||
await clearSidebarSearch(p);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let opened = await openDomChatByExternalId(p, externalChatId);
|
||||
if (!opened) {
|
||||
const searched = await searchSidebarChat(p, title);
|
||||
if (searched) {
|
||||
opened = await openDomChatByExternalId(p, externalChatId, { preserveSidebarSearch: true });
|
||||
}
|
||||
}
|
||||
if (!opened) {
|
||||
await clearSidebarSearch(p);
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -2918,8 +3369,11 @@ async function ensureDomChatOpen(p, externalChatId, waitMs = 600, requireCompose
|
||||
Math.max(waitMs, requireComposer ? 8000 : 2500),
|
||||
requireComposer);
|
||||
if (ready) {
|
||||
await clearSidebarSearch(p);
|
||||
return true;
|
||||
}
|
||||
|
||||
await clearSidebarSearch(p);
|
||||
}
|
||||
|
||||
return false;
|
||||
@@ -4444,7 +4898,7 @@ function makeDomChatExternalId(title, text, href, index, avatarUrl) {
|
||||
const descriptor = {
|
||||
title: cleanComparableText(title, 160),
|
||||
avatarUrl: normalizeDomChatFingerprint(avatarUrl),
|
||||
href: normalizeDomChatFingerprint(href)
|
||||
href: ""
|
||||
};
|
||||
const encodedDescriptor = Buffer.from(JSON.stringify(descriptor), "utf8")
|
||||
.toString("base64url");
|
||||
|
||||
Reference in New Issue
Block a user