This commit is contained in:
Boki 2026-02-26 20:45:24 -05:00
parent 53641fc8e7
commit 657d307485
28 changed files with 2045 additions and 161 deletions

View file

@ -68,6 +68,32 @@ function parseTradeItem(r) {
return { id, w, h, stashX, stashY, account };
}
function parseTradeItemWithPrice(r) {
const base = parseTradeItem(r);
let name = "";
let priceAmount = 0;
let priceCurrency = "";
if (r.item?.icon) {
try {
const pathname = new URL(r.item.icon).pathname;
const filename = pathname.split("/").pop() || "";
name = filename.replace(".png", "");
} catch {
// fallback: regex on last path segment
const match = r.item.icon.match(/\/([^/]+)\.png/);
if (match) name = match[1];
}
}
if (r.listing?.price) {
if (r.listing.price.amount != null) priceAmount = r.listing.price.amount;
if (r.listing.price.currency) priceCurrency = r.listing.price.currency;
}
return { ...base, name, priceAmount, priceCurrency };
}
async function waitForVisible(locator, timeoutMs) {
try {
await locator.waitFor({ state: "visible", timeout: timeoutMs });
@ -97,19 +123,8 @@ function handleWebSocket(ws, searchId) {
ws.on("framereceived", (frame) => {
if (pausedSearches.has(searchId)) return;
try {
const payload = typeof frame === "string" ? frame : frame.payload?.toString() ?? "";
const doc = JSON.parse(payload);
if (doc.new && Array.isArray(doc.new)) {
const ids = doc.new.filter((s) => s != null);
if (ids.length > 0) {
log(`New listings: ${searchId} (${ids.length} items)`);
sendEvent("newListings", { searchId, itemIds: ids });
}
}
} catch {
/* Non-JSON WebSocket frame */
}
// Note: trade site now sends JWTs, not raw item IDs.
// We rely on the fetch response interceptor instead.
});
ws.on("close", () => {
@ -160,14 +175,42 @@ async function cmdAddSearch(reqId, params) {
const page = await context.newPage();
searchPages.set(searchId, page);
// Register WebSocket handler BEFORE navigation so we catch connections during page load
page.on("websocket", (ws) => handleWebSocket(ws, searchId));
// Track whether live search is active — don't emit items from initial page load
let liveActive = false;
const seenIds = new Set();
// Intercept fetch responses to get full item data (WebSocket now sends JWTs, not raw IDs)
page.on("response", async (response) => {
if (!liveActive) return;
if (!response.url().includes("/api/trade2/fetch/")) return;
try {
const body = await response.text();
const doc = JSON.parse(body);
if (doc.result && Array.isArray(doc.result)) {
const items = doc.result
.map((r) => parseTradeItem(r))
.filter((i) => i.id && !seenIds.has(i.id));
items.forEach((i) => seenIds.add(i.id));
if (items.length > 0) {
log(`New listings (fetch): ${searchId} (${items.length} new items)`);
sendEvent("newListings", { searchId, items });
}
}
} catch {
/* Non-JSON trade response */
}
});
await page.goto(url, { waitUntil: "networkidle" });
await new Promise((r) => setTimeout(r, 2000)); // PageLoad delay
page.on("websocket", (ws) => handleWebSocket(ws, searchId));
try {
const liveBtn = page.locator(Selectors.LiveSearchButton).first();
await liveBtn.click({ timeout: 5000 });
liveActive = true;
log(`Live search activated: ${searchId}`);
} catch {
log(`Could not click Activate Live Search: ${searchId}`);
@ -222,6 +265,56 @@ async function cmdClickTravel(reqId, params) {
}
}
async function cmdAddDiamondSearch(reqId, params) {
if (!context) throw new Error("Browser not started");
const { url } = params;
const searchId = extractSearchId(url);
if (searchPages.has(searchId)) {
log(`Diamond search already open: ${searchId}`);
sendResponse(reqId, { searchId });
return;
}
log(`Adding diamond search: ${url} (${searchId})`);
const page = await context.newPage();
searchPages.set(searchId, page);
// Intercept fetch responses for item data + prices
page.on("response", async (response) => {
if (!response.url().includes("/api/trade2/fetch/")) return;
try {
const body = await response.text();
const doc = JSON.parse(body);
if (doc.result && Array.isArray(doc.result)) {
const items = doc.result.map((r) => parseTradeItemWithPrice(r));
if (items.length > 0) {
log(`Diamond listings: ${searchId} (${items.length} items)`);
sendEvent("diamondListings", { searchId, items });
}
}
} catch {
/* Non-JSON trade response */
}
});
// Hook WebSocket BEFORE navigation so we catch connections during page load
page.on("websocket", (ws) => handleWebSocket(ws, searchId));
await page.goto(url, { waitUntil: "networkidle" });
await new Promise((r) => setTimeout(r, 2000));
try {
const liveBtn = page.locator(Selectors.LiveSearchButton).first();
await liveBtn.click({ timeout: 5000 });
log(`Diamond live search activated: ${searchId}`);
} catch {
log(`Could not click Activate Live Search: ${searchId}`);
}
sendResponse(reqId, { searchId });
}
async function cmdOpenScrapPage(reqId, params) {
if (!context) throw new Error("Browser not started");
const { url } = params;
@ -339,6 +432,7 @@ async function cmdStop(reqId) {
const handlers = {
start: cmdStart,
addSearch: cmdAddSearch,
addDiamondSearch: cmdAddDiamondSearch,
pauseSearch: cmdPauseSearch,
clickTravel: cmdClickTravel,
openScrapPage: cmdOpenScrapPage,